Merge pull request #735 from tink-crypto:dependabot/pip/python/examples/idna-3.7

PiperOrigin-RevId: 625622842
diff --git a/cc/BUILD.bazel b/cc/BUILD.bazel
index 85db708..aea68c5 100644
--- a/cc/BUILD.bazel
+++ b/cc/BUILD.bazel
@@ -365,8 +365,6 @@
         ":keyset_reader",
         "//proto:tink_cc_proto",
         "//util:enums",
-        "//util:errors",
-        "//util:protobuf_helper",
         "//util:status",
         "//util:statusor",
         "@com_google_absl//absl/memory",
@@ -903,9 +901,11 @@
     srcs = ["core/json_keyset_reader_test.cc"],
     deps = [
         ":json_keyset_reader",
+        ":keyset_reader",
         "//proto:aes_eax_cc_proto",
         "//proto:aes_gcm_cc_proto",
         "//proto:tink_cc_proto",
+        "//util:statusor",
         "//util:test_matchers",
         "//util:test_util",
         "@com_google_absl//absl/status",
diff --git a/cc/CMakeLists.txt b/cc/CMakeLists.txt
index 93122c3..2f4a3db 100644
--- a/cc/CMakeLists.txt
+++ b/cc/CMakeLists.txt
@@ -343,8 +343,6 @@
     absl::strings
     rapidjson
     tink::util::enums
-    tink::util::errors
-    tink::util::protobuf_helper
     tink::util::status
     tink::util::statusor
     tink::proto::tink_cc_proto
@@ -847,9 +845,11 @@
     core/json_keyset_reader_test.cc
   DEPS
     tink::core::json_keyset_reader
+    tink::core::keyset_reader
     gmock
     absl::status
     absl::strings
+    tink::util::statusor
     tink::util::test_matchers
     tink::util::test_util
     tink::proto::aes_eax_cc_proto
diff --git a/cc/core/json_keyset_reader.cc b/cc/core/json_keyset_reader.cc
index 84c1d8b..d15ebc0 100644
--- a/cc/core/json_keyset_reader.cc
+++ b/cc/core/json_keyset_reader.cc
@@ -31,6 +31,7 @@
 #include "include/rapidjson/document.h"
 #include "include/rapidjson/error/en.h"
 #include "include/rapidjson/rapidjson.h"
+#include "include/rapidjson/reader.h"
 #include "tink/keyset_reader.h"
 #include "tink/util/enums.h"
 #include "tink/util/status.h"
@@ -242,7 +243,8 @@
     serialized_keyset = &serialized_keyset_from_stream;
   }
   rapidjson::Document json_doc(rapidjson::kObjectType);
-  if (json_doc.Parse(serialized_keyset->c_str()).HasParseError()) {
+  if (json_doc.Parse<rapidjson::kParseIterativeFlag>(serialized_keyset->c_str())
+          .HasParseError()) {
     return util::Status(
         absl::StatusCode::kInvalidArgument,
         absl::StrCat(
@@ -268,7 +270,8 @@
     serialized_keyset = &serialized_keyset_from_stream;
   }
   rapidjson::Document json_doc;
-  if (json_doc.Parse(serialized_keyset->c_str()).HasParseError()) {
+  if (json_doc.Parse<rapidjson::kParseIterativeFlag>(serialized_keyset->c_str())
+          .HasParseError()) {
     return util::Status(
         absl::StatusCode::kInvalidArgument,
         absl::StrCat("Invalid JSON EncryptedKeyset: Error (offset ",
diff --git a/cc/core/json_keyset_reader_test.cc b/cc/core/json_keyset_reader_test.cc
index 6fc0ba8..e9725ee 100644
--- a/cc/core/json_keyset_reader_test.cc
+++ b/cc/core/json_keyset_reader_test.cc
@@ -29,11 +29,13 @@
 #include "absl/status/status.h"
 #include "absl/strings/escaping.h"
 #include "absl/strings/substitute.h"
+#include "tink/util/statusor.h"
 #include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 #include "proto/aes_eax.pb.h"
 #include "proto/aes_gcm.pb.h"
 #include "proto/tink.pb.h"
+#include "tink/keyset_reader.h"
 
 namespace crypto {
 namespace tink {
@@ -397,6 +399,23 @@
   EXPECT_THAT(read_result, Not(IsOk()));
 }
 
+
+TEST_F(JsonKeysetReaderTest, parseRecursiveJsonStringFails) {
+  std::string recursive_json;
+  for (int i = 0; i < 1000000; i++) {
+    recursive_json.append("{\"a\":");
+  }
+  recursive_json.append("1");
+  for (int i = 0; i < 1000000; i++) {
+    recursive_json.append("}");
+  }
+  util::StatusOr<std::unique_ptr<KeysetReader>> reader =
+      JsonKeysetReader::New(recursive_json);
+  ASSERT_THAT(reader, IsOk());
+  util::StatusOr<std::unique_ptr<Keyset>> keyset = (*reader)->Read();
+  EXPECT_THAT(keyset, Not(IsOk()));
+}
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/core/keyset_handle_builder_test.cc b/cc/core/keyset_handle_builder_test.cc
index 2e0de9d..88be9bb 100644
--- a/cc/core/keyset_handle_builder_test.cc
+++ b/cc/core/keyset_handle_builder_test.cc
@@ -617,6 +617,45 @@
   ASSERT_THAT(handle.status(), IsOk());
 }
 
+TEST_F(KeysetHandleBuilderTest,
+       MergeTwoKeysetsWithTheSameIdButNoIdRequirementWorks) {
+  util::StatusOr<AesCmacParameters> params = AesCmacParameters::Create(
+      /*key_size_in_bytes=*/32, /*cryptographic_tag_size_in_bytes=*/16,
+      AesCmacParameters::Variant::kNoPrefix);
+  ASSERT_THAT(params, IsOk());
+
+  KeysetHandleBuilder::Entry entry1 =
+      KeysetHandleBuilder::Entry::CreateFromParams(
+          absl::make_unique<AesCmacParameters>(std::move(*params)),
+          KeyStatus::kEnabled, /*is_primary=*/true);
+  entry1.SetFixedId(123);
+  util::StatusOr<KeysetHandle> handle1 =
+      KeysetHandleBuilder().AddEntry(std::move(entry1)).Build();
+  ASSERT_THAT(handle1.status(), IsOk());
+
+  KeysetHandleBuilder::Entry entry2 =
+      KeysetHandleBuilder::Entry::CreateFromParams(
+          absl::make_unique<AesCmacParameters>(std::move(*params)),
+          KeyStatus::kEnabled, /*is_primary=*/true);
+  entry2.SetFixedId(123);
+  util::StatusOr<KeysetHandle> handle2 =
+      KeysetHandleBuilder().AddEntry(std::move(entry2)).Build();
+  ASSERT_THAT(handle2.status(), IsOk());
+
+  // handle1 and handle2 each contain one key with the same ID, but no ID
+  // requirement. We can add them to a new keyset because they will get new,
+  // random and distinct IDs.
+  util::StatusOr<KeysetHandle> handle12 =
+      KeysetHandleBuilder()
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromKey(
+              (*handle1)[0].GetKey(), KeyStatus::kEnabled, /*is_primary=*/true))
+          .AddEntry(KeysetHandleBuilder::Entry::CreateFromKey(
+              (*handle2)[0].GetKey(), KeyStatus::kEnabled,
+              /*is_primary=*/false))
+          .Build();
+  ASSERT_THAT(handle12.status(), IsOk());
+}
+
 TEST_F(KeysetHandleBuilderTest, CreateBuilderEntryFromCopyableKey) {
   Keyset keyset;
   Keyset::Key key;
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_parameters.cc b/cc/experimental/pqcrypto/signature/slh_dsa_parameters.cc
new file mode 100644
index 0000000..b859681
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_parameters.cc
@@ -0,0 +1,71 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/experimental/pqcrypto/signature/slh_dsa_parameters.h"
+
+#include "absl/status/status.h"
+#include "tink/parameters.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+util::StatusOr<SlhDsaParameters> SlhDsaParameters::Create(
+    HashType hash_type, int private_key_size_in_bytes,
+    SignatureType signature_type, Variant variant) {
+  // Validate HashType - only SHA2 is currently supported.
+  if (hash_type != HashType::kSha2) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create Slh-DSA parameters with unknown HashType.");
+  }
+
+  if (private_key_size_in_bytes != 64) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Invalid private key size. Only 64-bytes keys are "
+                        "currently supported.");
+  }
+
+  // Validate SignatureType - only SmallSignature is currently supported.
+  if (signature_type != SignatureType::kSmallSignature) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create SLH-DSA parameters with unknown SignatureType.");
+  }
+
+  // Validate Variant.
+  if (variant != Variant::kTink && variant != Variant::kNoPrefix) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create SLH-DSA parameters with unknown Variant.");
+  }
+  return SlhDsaParameters(hash_type, private_key_size_in_bytes, signature_type,
+                          variant);
+}
+
+bool SlhDsaParameters::operator==(const Parameters& other) const {
+  const SlhDsaParameters* that = dynamic_cast<const SlhDsaParameters*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  return hash_type_ == that->hash_type_ &&
+         private_key_size_in_bytes_ == that->private_key_size_in_bytes_ &&
+         signature_type_ == that->signature_type_ && variant_ == that->variant_;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_parameters.h b/cc/experimental/pqcrypto/signature/slh_dsa_parameters.h
new file mode 100644
index 0000000..1134f43
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_parameters.h
@@ -0,0 +1,102 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PARAMETERS_H_
+#define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PARAMETERS_H_
+
+#include "tink/parameters.h"
+#include "tink/signature/signature_parameters.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Representation of the parameters sets for the Stateless Hash-Based Digital
+// Signature Standard (SLH-DSA) described at
+// https://csrc.nist.gov/pubs/fips/205/ipd.
+//
+// Note that only the SLH-DSA-SHA2-128s set is currently supported.
+class SlhDsaParameters : public SignatureParameters {
+ public:
+  // Describes the output prefix prepended to the signature.
+  enum class Variant : int {
+    // Prepends '0x01<big endian key id>' to signature.
+    kTink = 1,
+    // Does not prepend any prefix (i.e., keys must have no ID requirement).
+    kNoPrefix = 2,
+    // Added to guard from failures that may be caused by future expansions.
+    kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+  };
+
+  // Description of the hash function used for this algorithm.
+  enum class HashType : int {
+    // The 128-bit security level variant uses SHA256. The 192-bit and 256-bit
+    // variants require both SHA-256 and SHA-512 in their implementation.
+    kSha2 = 1,
+    kShake = 2,
+    kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+  };
+
+  // Description of the signature type. kFastSigning parameters sets
+  // have significantly faster signing, but kSmallSignature come with faster
+  // verification and smaller signatures.
+  enum class SignatureType : int {
+    kFastSigning = 1,
+    kSmallSignature = 2,
+    kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+  };
+
+  // Copyable and movable.
+  SlhDsaParameters(const SlhDsaParameters& other) = default;
+  SlhDsaParameters& operator=(const SlhDsaParameters& other) = default;
+  SlhDsaParameters(SlhDsaParameters&& other) = default;
+  SlhDsaParameters& operator=(SlhDsaParameters&& other) = default;
+
+  // Creates SLH-DSA parameters instances.
+  static util::StatusOr<SlhDsaParameters> Create(HashType hash_type,
+                                                 int private_key_size_in_bytes,
+                                                 SignatureType signature_type,
+                                                 Variant variant);
+
+  HashType GetHashType() const { return hash_type_; }
+  int GetPrivateKeySizeInBytes() const { return private_key_size_in_bytes_; }
+  SignatureType GetSignatureType() const { return signature_type_; }
+  Variant GetVariant() const { return variant_; }
+
+  bool HasIdRequirement() const override {
+    return variant_ != Variant::kNoPrefix;
+  }
+
+  bool operator==(const Parameters& other) const override;
+
+ private:
+  explicit SlhDsaParameters(HashType hash_type, int private_key_size_in_bytes,
+                            SignatureType signature_type, Variant variant)
+      : hash_type_(hash_type),
+        private_key_size_in_bytes_(private_key_size_in_bytes),
+        signature_type_(signature_type),
+        variant_(variant) {}
+
+  HashType hash_type_;
+  int private_key_size_in_bytes_;
+  SignatureType signature_type_;
+  Variant variant_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PARAMETERS_H_
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_parameters_test.cc b/cc/experimental/pqcrypto/signature/slh_dsa_parameters_test.cc
new file mode 100644
index 0000000..b265c0e
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_parameters_test.cc
@@ -0,0 +1,211 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/experimental/pqcrypto/signature/slh_dsa_parameters.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.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::Eq;
+using ::testing::IsTrue;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct VariantTestCase {
+  SlhDsaParameters::Variant variant;
+  bool has_id_requirement;
+};
+
+using SlhDsaParametersTest = TestWithParam<VariantTestCase>;
+
+INSTANTIATE_TEST_SUITE_P(
+    SlhDsaParametersTestSuite, SlhDsaParametersTest,
+    Values(VariantTestCase{SlhDsaParameters::Variant::kTink,
+                           /*has_id_requirement=*/true},
+           VariantTestCase{SlhDsaParameters::Variant::kNoPrefix,
+                           /*has_id_requirement=*/false}));
+
+TEST_P(SlhDsaParametersTest, CreateSlhDsa128Sha2SmallSignatureWorks) {
+  VariantTestCase test_case = GetParam();
+
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  EXPECT_THAT(parameters->GetHashType(), Eq(SlhDsaParameters::HashType::kSha2));
+  EXPECT_THAT(parameters->GetPrivateKeySizeInBytes(), Eq(64));
+  EXPECT_THAT(parameters->GetSignatureType(),
+              Eq(SlhDsaParameters::SignatureType::kSmallSignature));
+  EXPECT_THAT(parameters->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(parameters->HasIdRequirement(), Eq(test_case.has_id_requirement));
+}
+
+TEST(SlhDsaParametersTest, CreateWithInvalidVariantFails) {
+  EXPECT_THAT(
+      SlhDsaParameters::Create(
+          SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+          SlhDsaParameters::SignatureType::kSmallSignature,
+          SlhDsaParameters::Variant::
+              kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements)
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(SlhDsaParametersTest, CreateWithInvalidHashTypeFails) {
+  EXPECT_THAT(SlhDsaParameters::Create(
+                  SlhDsaParameters::HashType::
+                      kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements,
+                  /*private_key_size_in_bytes=*/64,
+                  SlhDsaParameters::SignatureType::kSmallSignature,
+                  SlhDsaParameters::Variant::kTink)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(SlhDsaParametersTest, CreateWithUnsupportedHashTypeFails) {
+  EXPECT_THAT(
+      SlhDsaParameters::Create(SlhDsaParameters::HashType::kShake,
+                               /*private_key_size_in_bytes=*/64,
+                               SlhDsaParameters::SignatureType::kSmallSignature,
+                               SlhDsaParameters::Variant::kTink)
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(SlhDsaParametersTest, CreateWithInvalidSignatureTypeFails) {
+  EXPECT_THAT(SlhDsaParameters::Create(
+                  SlhDsaParameters::HashType::kSha2,
+                  /*private_key_size_in_bytes=*/64,
+                  SlhDsaParameters::SignatureType::
+                      kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements,
+                  SlhDsaParameters::Variant::kTink)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(SlhDsaParametersTest, CreateWithUnsupportedSignatureTypeFails) {
+  EXPECT_THAT(
+      SlhDsaParameters::Create(SlhDsaParameters::HashType::kSha2,
+                               /*private_key_size_in_bytes=*/64,
+                               SlhDsaParameters::SignatureType::kFastSigning,
+                               SlhDsaParameters::Variant::kTink)
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(SlhDsaParametersTest, CreateWithInvalidKeySizeFails) {
+  EXPECT_THAT(
+      SlhDsaParameters::Create(SlhDsaParameters::HashType::kSha2,
+                               /*private_key_size_in_bytes=*/31,
+                               SlhDsaParameters::SignatureType::kSmallSignature,
+                               SlhDsaParameters::Variant::kTink)
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(SlhDsaParametersTest, CreateWithUnsupportedKeySizeFails) {
+  EXPECT_THAT(
+      SlhDsaParameters::Create(SlhDsaParameters::HashType::kSha2,
+                               /*private_key_size_in_bytes=*/128,
+                               SlhDsaParameters::SignatureType::kSmallSignature,
+                               SlhDsaParameters::Variant::kNoPrefix)
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST(SlhDsaParametersTest, CopyConstructor) {
+  util::StatusOr<SlhDsaParameters> parameters =
+      SlhDsaParameters::Create(SlhDsaParameters::HashType::kSha2,
+                               /*private_key_size_in_bytes=*/64,
+                               SlhDsaParameters::SignatureType::kSmallSignature,
+                               SlhDsaParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  SlhDsaParameters copy(*parameters);
+
+  EXPECT_THAT(copy.GetHashType(), Eq(SlhDsaParameters::HashType::kSha2));
+  EXPECT_THAT(copy.GetPrivateKeySizeInBytes(), Eq(64));
+  EXPECT_THAT(copy.GetSignatureType(),
+              Eq(SlhDsaParameters::SignatureType::kSmallSignature));
+  EXPECT_THAT(copy.GetVariant(), Eq(SlhDsaParameters::Variant::kTink));
+  EXPECT_THAT(copy.HasIdRequirement(), IsTrue());
+}
+
+TEST(SlhDsaParametersTest, CopyAssignment) {
+  util::StatusOr<SlhDsaParameters> parameters =
+      SlhDsaParameters::Create(SlhDsaParameters::HashType::kSha2,
+                               /*private_key_size_in_bytes=*/64,
+                               SlhDsaParameters::SignatureType::kSmallSignature,
+                               SlhDsaParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  SlhDsaParameters copy = *parameters;
+  EXPECT_THAT(copy.GetHashType(), Eq(SlhDsaParameters::HashType::kSha2));
+  EXPECT_THAT(copy.GetPrivateKeySizeInBytes(), Eq(64));
+  EXPECT_THAT(copy.GetSignatureType(),
+              Eq(SlhDsaParameters::SignatureType::kSmallSignature));
+  EXPECT_THAT(copy.GetVariant(), Eq(SlhDsaParameters::Variant::kTink));
+  EXPECT_THAT(copy.HasIdRequirement(), IsTrue());
+}
+
+TEST_P(SlhDsaParametersTest, ParametersEquals) {
+  VariantTestCase test_case = GetParam();
+
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<SlhDsaParameters> other_parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature, test_case.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(SlhDsaParametersTest, DifferentVariantNotEqual) {
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature,
+      SlhDsaParameters::Variant::kNoPrefix);
+
+  util::StatusOr<SlhDsaParameters> other_parameters =
+      SlhDsaParameters::Create(SlhDsaParameters::HashType::kSha2,
+                               /*private_key_size_in_bytes=*/64,
+                               SlhDsaParameters::SignatureType::kSmallSignature,
+                               SlhDsaParameters::Variant::kTink);
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_private_key.cc b/cc/experimental/pqcrypto/signature/slh_dsa_private_key.cc
new file mode 100644
index 0000000..e92889a
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_private_key.cc
@@ -0,0 +1,92 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/experimental/pqcrypto/signature/slh_dsa_private_key.h"
+
+#include <cstdint>
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "openssl/boringssl/src/include/openssl/mem.h"
+#define OPENSSL_UNSTABLE_EXPERIMENTAL_SPX
+#include "openssl/experimental/spx.h"
+#undef OPENSSL_UNSTABLE_EXPERIMENTAL_SPX
+#include "tink/experimental/pqcrypto/signature/slh_dsa_public_key.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/key.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+util::StatusOr<SlhDsaPrivateKey> SlhDsaPrivateKey::Create(
+    const SlhDsaPublicKey& public_key, const RestrictedData& private_key_bytes,
+    PartialKeyAccessToken token) {
+  // Only 64-byte private keys are currently supported.
+  if (private_key_bytes.size() != SPX_SECRET_KEY_BYTES) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SLH-DSA private key length must be 64 bytes.");
+  }
+
+  if (public_key.GetParameters().GetPrivateKeySizeInBytes() !=
+      private_key_bytes.size()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Private key size does not match parameters");
+  }
+  // Confirm that the private key and public key are a valid SLH-DSA key pair.
+  std::string public_key_bytes_regen;
+  public_key_bytes_regen.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes_regen;
+  private_key_bytes_regen.resize(SPX_SECRET_KEY_BYTES);
+
+  absl::string_view expected_private_key_bytes =
+      private_key_bytes.GetSecret(InsecureSecretKeyAccess::Get());
+  SPX_generate_key_from_seed(
+      reinterpret_cast<uint8_t*>(public_key_bytes_regen.data()),
+      reinterpret_cast<uint8_t*>(private_key_bytes_regen.data()),
+      // Uses the first 48 bytes of the private key as seed.
+      reinterpret_cast<const uint8_t*>(expected_private_key_bytes.data()));
+
+  absl::string_view expected_public_key_bytes =
+      public_key.GetPublicKeyBytes(token);
+
+  if (CRYPTO_memcmp(expected_public_key_bytes.data(),
+                    public_key_bytes_regen.data(), SPX_PUBLIC_KEY_BYTES) != 0 ||
+      CRYPTO_memcmp(expected_private_key_bytes.data(),
+                    private_key_bytes_regen.data(),
+                    SPX_SECRET_KEY_BYTES) != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Invalid SLH-DSA key pair");
+  }
+
+  return SlhDsaPrivateKey(public_key, private_key_bytes);
+}
+
+bool SlhDsaPrivateKey::operator==(const Key& other) const {
+  const SlhDsaPrivateKey* that = dynamic_cast<const SlhDsaPrivateKey*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  return public_key_ == that->public_key_ &&
+         private_key_bytes_ == that->private_key_bytes_;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_private_key.h b/cc/experimental/pqcrypto/signature/slh_dsa_private_key.h
new file mode 100644
index 0000000..4e5b37c
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_private_key.h
@@ -0,0 +1,65 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PRIVATE_KEY_H_
+#define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PRIVATE_KEY_H_
+
+#include "tink/key.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/experimental/pqcrypto/signature/slh_dsa_public_key.h"
+#include "tink/signature/signature_private_key.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+class SlhDsaPrivateKey : public SignaturePrivateKey {
+ public:
+  // Copyable and movable.
+  SlhDsaPrivateKey(const SlhDsaPrivateKey& other) = default;
+  SlhDsaPrivateKey& operator=(const SlhDsaPrivateKey& other) = default;
+  SlhDsaPrivateKey(SlhDsaPrivateKey&& other) = default;
+  SlhDsaPrivateKey& operator=(SlhDsaPrivateKey&& other) = default;
+
+  // Creates a new SLH-DSA private key from `private_key_bytes`. Returns an
+  // error if `public_key` does not belong to the same key pair as
+  // `private_key_bytes`.
+  static util::StatusOr<SlhDsaPrivateKey> Create(
+      const SlhDsaPublicKey& public_key,
+      const RestrictedData& private_key_bytes, PartialKeyAccessToken token);
+
+  const RestrictedData& GetPrivateKeyBytes(PartialKeyAccessToken token) const {
+    return private_key_bytes_;
+  }
+
+  const SlhDsaPublicKey& GetPublicKey() const override { return public_key_; }
+
+  bool operator==(const Key& other) const override;
+
+ private:
+  explicit SlhDsaPrivateKey(const SlhDsaPublicKey& public_key,
+                             const RestrictedData& private_key_bytes)
+      : public_key_(public_key), private_key_bytes_(private_key_bytes) {}
+
+  SlhDsaPublicKey public_key_;
+  RestrictedData private_key_bytes_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PRIVATE_KEY_H_
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_private_key_test.cc b/cc/experimental/pqcrypto/signature/slh_dsa_private_key_test.cc
new file mode 100644
index 0000000..cea2641
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_private_key_test.cc
@@ -0,0 +1,279 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/experimental/pqcrypto/signature/slh_dsa_private_key.h"
+
+#include <cstdint>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#define OPENSSL_UNSTABLE_EXPERIMENTAL_SPX
+#include "openssl/experimental/spx.h"
+#undef OPENSSL_UNSTABLE_EXPERIMENTAL_SPX
+#include "tink/experimental/pqcrypto/signature/slh_dsa_parameters.h"
+#include "tink/experimental/pqcrypto/signature/slh_dsa_public_key.h"
+#include "tink/insecure_secret_key_access.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/random.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::Eq;
+using ::testing::HasSubstr;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  SlhDsaParameters::Variant variant;
+  absl::optional<int> id_requirement;
+  std::string output_prefix;
+};
+
+using SlhDsaPrivateKeyTest = TestWithParam<TestCase>;
+
+INSTANTIATE_TEST_SUITE_P(
+    SlhDsaPrivateKeyTestSuite, SlhDsaPrivateKeyTest,
+    Values(TestCase{SlhDsaParameters::Variant::kTink, 0x02030400,
+                    std::string("\x01\x02\x03\x04\x00", 5)},
+           TestCase{SlhDsaParameters::Variant::kTink, 0x03050709,
+                    std::string("\x01\x03\x05\x07\x09", 5)},
+           TestCase{SlhDsaParameters::Variant::kNoPrefix, absl::nullopt, ""}));
+
+TEST_P(SlhDsaPrivateKeyTest, CreateSucceeds) {
+  TestCase test_case = GetParam();
+
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2,
+      /*private_key_size_in_bytes=*/SPX_SECRET_KEY_BYTES,
+      SlhDsaParameters::SignatureType::kSmallSignature, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes;
+  private_key_bytes.resize(SPX_SECRET_KEY_BYTES);
+
+  SPX_generate_key(reinterpret_cast<uint8_t *>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t *>(private_key_bytes.data()));
+
+  util::StatusOr<SlhDsaPublicKey> public_key =
+      SlhDsaPublicKey::Create(*parameters, public_key_bytes,
+                              test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  RestrictedData restricted_private_key_bytes =
+      RestrictedData(private_key_bytes, InsecureSecretKeyAccess::Get());
+  util::StatusOr<SlhDsaPrivateKey> private_key = SlhDsaPrivateKey::Create(
+      *public_key, restricted_private_key_bytes, GetPartialKeyAccess());
+  ASSERT_THAT(private_key, IsOk());
+
+  EXPECT_THAT(private_key->GetParameters(), Eq(*parameters));
+  EXPECT_THAT(private_key->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT(private_key->GetPublicKey(), Eq(*public_key));
+  EXPECT_THAT(private_key->GetOutputPrefix(), Eq(test_case.output_prefix));
+  EXPECT_THAT(private_key->GetPrivateKeyBytes(GetPartialKeyAccess()),
+              Eq(restricted_private_key_bytes));
+}
+
+TEST(SlhDsaPrivateKeyTest, CreateWithInvalidPrivateKeyLengthFails) {
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2,
+      /*private_key_size_in_bytes=*/SPX_SECRET_KEY_BYTES,
+      SlhDsaParameters::SignatureType::kSmallSignature,
+      SlhDsaParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<SlhDsaPublicKey> public_key = SlhDsaPublicKey::Create(
+      *parameters, subtle::Random::GetRandomBytes(SPX_PUBLIC_KEY_BYTES),
+      /*id_requirement=*/123, GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  RestrictedData restricted_private_key_bytes = RestrictedData(
+      subtle::Random::GetRandomBytes(63), InsecureSecretKeyAccess::Get());
+  EXPECT_THAT(
+      SlhDsaPrivateKey::Create(*public_key, restricted_private_key_bytes,
+                               GetPartialKeyAccess())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("SLH-DSA private key length must be "
+                         "64 bytes")));
+}
+
+TEST(SlhDsaPrivateKeyTest, CreateWithMismatchedPairFails) {
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2,
+      /*private_key_size_in_bytes=*/SPX_SECRET_KEY_BYTES,
+      SlhDsaParameters::SignatureType::kSmallSignature,
+      SlhDsaParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes;
+  private_key_bytes.resize(SPX_SECRET_KEY_BYTES);
+
+  SPX_generate_key(reinterpret_cast<uint8_t *>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t *>(private_key_bytes.data()));
+
+  util::StatusOr<SlhDsaPublicKey> public_key =
+      SlhDsaPublicKey::Create(*parameters, public_key_bytes,
+                              /*id_requirement=*/123, GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  // Generate a new key pair.
+  SPX_generate_key(reinterpret_cast<uint8_t *>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t *>(private_key_bytes.data()));
+  RestrictedData restricted_private_key_bytes =
+      RestrictedData(private_key_bytes, InsecureSecretKeyAccess::Get());
+
+  // Creating the private key using the different private_key_bytes should fail.
+  EXPECT_THAT(
+      SlhDsaPrivateKey::Create(*public_key, restricted_private_key_bytes,
+                               GetPartialKeyAccess())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("Invalid SLH-DSA key pair")));
+}
+
+TEST(SlhDsaPrivateKeyTest, CreateWithModifiedPrivateKeyFails) {
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2,
+      /*private_key_size_in_bytes=*/SPX_SECRET_KEY_BYTES,
+      SlhDsaParameters::SignatureType::kSmallSignature,
+      SlhDsaParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes;
+  private_key_bytes.resize(SPX_SECRET_KEY_BYTES);
+
+  SPX_generate_key(reinterpret_cast<uint8_t *>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t *>(private_key_bytes.data()));
+
+  util::StatusOr<SlhDsaPublicKey> public_key =
+      SlhDsaPublicKey::Create(*parameters, public_key_bytes,
+                              /*id_requirement=*/123, GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  // Replace last 16 bytes of the private key bytes with random bytes.
+  private_key_bytes.replace(/*seed_size=*/48, /*pk_root_size=*/16,
+                            subtle::Random::GetRandomBytes(16));
+  RestrictedData restricted_private_key_bytes =
+      RestrictedData(private_key_bytes, InsecureSecretKeyAccess::Get());
+
+  EXPECT_THAT(
+      SlhDsaPrivateKey::Create(*public_key, restricted_private_key_bytes,
+                               GetPartialKeyAccess())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("Invalid SLH-DSA key pair")));
+}
+
+TEST_P(SlhDsaPrivateKeyTest, KeyEquals) {
+  TestCase test_case = GetParam();
+
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2,
+      /*private_key_size_in_bytes=*/SPX_SECRET_KEY_BYTES,
+      SlhDsaParameters::SignatureType::kSmallSignature, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes;
+  private_key_bytes.resize(SPX_SECRET_KEY_BYTES);
+
+  SPX_generate_key(reinterpret_cast<uint8_t *>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t *>(private_key_bytes.data()));
+
+  util::StatusOr<SlhDsaPublicKey> public_key =
+      SlhDsaPublicKey::Create(*parameters, public_key_bytes,
+                              test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  RestrictedData restricted_private_key_bytes =
+      RestrictedData(private_key_bytes, InsecureSecretKeyAccess::Get());
+  util::StatusOr<SlhDsaPrivateKey> private_key = SlhDsaPrivateKey::Create(
+      *public_key, restricted_private_key_bytes, GetPartialKeyAccess());
+  ASSERT_THAT(private_key, IsOk());
+
+  util::StatusOr<SlhDsaPrivateKey> other_private_key = SlhDsaPrivateKey::Create(
+      *public_key, restricted_private_key_bytes, GetPartialKeyAccess());
+  ASSERT_THAT(other_private_key, IsOk());
+
+  EXPECT_TRUE(*private_key == *other_private_key);
+  EXPECT_TRUE(*other_private_key == *private_key);
+  EXPECT_FALSE(*private_key != *other_private_key);
+  EXPECT_FALSE(*other_private_key != *private_key);
+}
+
+TEST(SlhDsaPrivateKeyTest, DifferentPublicKeyNotEqual) {
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2,
+      /*private_key_size_in_bytes=*/SPX_SECRET_KEY_BYTES,
+      SlhDsaParameters::SignatureType::kSmallSignature,
+      SlhDsaParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes;
+  private_key_bytes.resize(parameters->GetPrivateKeySizeInBytes());
+
+  SPX_generate_key(reinterpret_cast<uint8_t *>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t *>(private_key_bytes.data()));
+
+  util::StatusOr<SlhDsaPublicKey> public_key123 =
+      SlhDsaPublicKey::Create(*parameters, public_key_bytes,
+                              /*id_requirement=*/123, GetPartialKeyAccess());
+  ASSERT_THAT(public_key123, IsOk());
+
+  util::StatusOr<SlhDsaPublicKey> public_key456 =
+      SlhDsaPublicKey::Create(*parameters, public_key_bytes,
+                              /*id_requirement=*/456, GetPartialKeyAccess());
+  ASSERT_THAT(public_key456, IsOk());
+
+  RestrictedData restricted_private_key_bytes =
+      RestrictedData(private_key_bytes, InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<SlhDsaPrivateKey> private_key = SlhDsaPrivateKey::Create(
+      *public_key123, restricted_private_key_bytes, GetPartialKeyAccess());
+  ASSERT_THAT(private_key, IsOk());
+
+  util::StatusOr<SlhDsaPrivateKey> other_private_key = SlhDsaPrivateKey::Create(
+      *public_key456, restricted_private_key_bytes, GetPartialKeyAccess());
+  ASSERT_THAT(other_private_key, IsOk());
+
+  EXPECT_TRUE(*private_key != *other_private_key);
+  EXPECT_TRUE(*other_private_key != *private_key);
+  EXPECT_FALSE(*private_key == *other_private_key);
+  EXPECT_FALSE(*other_private_key == *private_key);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_proto_serialization.cc b/cc/experimental/pqcrypto/signature/slh_dsa_proto_serialization.cc
new file mode 100644
index 0000000..57931f8
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_proto_serialization.cc
@@ -0,0 +1,462 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/experimental/pqcrypto/signature/slh_dsa_proto_serialization.h"
+
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/experimental/pqcrypto/signature/slh_dsa_parameters.h"
+#include "tink/experimental/pqcrypto/signature/slh_dsa_private_key.h"
+#include "tink/experimental/pqcrypto/signature/slh_dsa_public_key.h"
+#include "tink/insecure_secret_key_access.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/experimental/pqcrypto/slh_dsa.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::google::crypto::tink::SlhDsaHashType;
+using ::google::crypto::tink::SlhDsaKeyFormat;
+using ::google::crypto::tink::SlhDsaParams;
+using ::google::crypto::tink::SlhDsaSignatureType;
+
+using SlhDsaProtoParametersParserImpl =
+    internal::ParametersParserImpl<internal::ProtoParametersSerialization,
+                                   SlhDsaParameters>;
+using SlhDsaProtoParametersSerializerImpl =
+    internal::ParametersSerializerImpl<SlhDsaParameters,
+                                       internal::ProtoParametersSerialization>;
+using SlhDsaProtoPublicKeyParserImpl =
+    internal::KeyParserImpl<internal::ProtoKeySerialization, SlhDsaPublicKey>;
+using SlhDsaProtoPublicKeySerializerImpl =
+    internal::KeySerializerImpl<SlhDsaPublicKey,
+                                internal::ProtoKeySerialization>;
+using SlhDsaProtoPrivateKeyParserImpl =
+    internal::KeyParserImpl<internal::ProtoKeySerialization, SlhDsaPrivateKey>;
+using SlhDsaProtoPrivateKeySerializerImpl =
+    internal::KeySerializerImpl<SlhDsaPrivateKey,
+                                internal::ProtoKeySerialization>;
+
+const absl::string_view kPrivateTypeUrl =
+    "type.googleapis.com/google.crypto.tink.SlhDsaPrivateKey";
+const absl::string_view kPublicTypeUrl =
+    "type.googleapis.com/google.crypto.tink.SlhDsaPublicKey";
+
+util::StatusOr<SlhDsaParameters::Variant> ToVariant(
+    OutputPrefixType output_prefix_type) {
+  switch (output_prefix_type) {
+    case OutputPrefixType::RAW:
+      return SlhDsaParameters::Variant::kNoPrefix;
+    case OutputPrefixType::TINK:
+      return SlhDsaParameters::Variant::kTink;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine SlhDsaParameters::Variant");
+  }
+}
+
+util::StatusOr<OutputPrefixType> ToOutputPrefixType(
+    SlhDsaParameters::Variant variant) {
+  switch (variant) {
+    case SlhDsaParameters::Variant::kNoPrefix:
+      return OutputPrefixType::RAW;
+    case SlhDsaParameters::Variant::kTink:
+      return OutputPrefixType::TINK;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine output prefix type");
+  }
+}
+
+util::StatusOr<SlhDsaParameters::HashType> ToHashType(
+    SlhDsaHashType proto_hash_type) {
+  switch (proto_hash_type) {
+    case SlhDsaHashType::SHA2:
+      return SlhDsaParameters::HashType::kSha2;
+    case SlhDsaHashType::SHAKE:
+      return SlhDsaParameters::HashType::kShake;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine SlhDsaParameters::HashType");
+  }
+}
+
+util::StatusOr<SlhDsaHashType> ToProtoHashType(
+    SlhDsaParameters::HashType hash_type) {
+  switch (hash_type) {
+    case SlhDsaParameters::HashType::kSha2:
+      return SlhDsaHashType::SHA2;
+    case SlhDsaParameters::HashType::kShake:
+      return SlhDsaHashType::SHAKE;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine SlhDsaHashType");
+  }
+}
+
+util::StatusOr<SlhDsaParameters::SignatureType> ToSignatureType(
+    SlhDsaSignatureType proto_signature_type) {
+  switch (proto_signature_type) {
+    case SlhDsaSignatureType::FAST_SIGNING:
+      return SlhDsaParameters::SignatureType::kFastSigning;
+    case SlhDsaSignatureType::SMALL_SIGNATURE:
+      return SlhDsaParameters::SignatureType::kSmallSignature;
+    default:
+      return util::Status(
+          absl::StatusCode::kInvalidArgument,
+          "Could not determine SlhDsaParameters::SignatureType");
+  }
+}
+
+util::StatusOr<SlhDsaSignatureType> ToProtoSignatureType(
+    SlhDsaParameters::SignatureType signature_type) {
+  switch (signature_type) {
+    case SlhDsaParameters::SignatureType::kFastSigning:
+      return SlhDsaSignatureType::FAST_SIGNING;
+    case SlhDsaParameters::SignatureType::kSmallSignature:
+      return SlhDsaSignatureType::SMALL_SIGNATURE;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine SlhDsaSignatureType");
+  }
+}
+
+util::StatusOr<SlhDsaParameters> ToParameters(
+    OutputPrefixType output_prefix_type, const SlhDsaParams& params) {
+  util::StatusOr<SlhDsaParameters::Variant> variant =
+      ToVariant(output_prefix_type);
+  if (!variant.ok()) {
+    return variant.status();
+  }
+
+  util::StatusOr<SlhDsaParameters::HashType> hash_type =
+      ToHashType(params.hash_type());
+  if (!hash_type.ok()) {
+    return hash_type.status();
+  }
+
+  util::StatusOr<SlhDsaParameters::SignatureType> signature_type =
+      ToSignatureType(params.sig_type());
+  if (!signature_type.ok()) {
+    return signature_type.status();
+  }
+
+  return SlhDsaParameters::Create(*hash_type, params.key_size(),
+                                  *signature_type, *variant);
+}
+
+util::StatusOr<SlhDsaParams> FromParameters(
+    const SlhDsaParameters& parameters) {
+  /* Only SLH-DSA-SHA2-128s  is currently supported*/
+  util::StatusOr<SlhDsaHashType> hash_type =
+      ToProtoHashType(parameters.GetHashType());
+  if (!hash_type.ok()) {
+    return hash_type.status();
+  }
+
+  util::StatusOr<SlhDsaSignatureType> signature_type =
+      ToProtoSignatureType(parameters.GetSignatureType());
+  if (!signature_type.ok()) {
+    return signature_type.status();
+  }
+
+  SlhDsaParams params;
+  params.set_key_size(parameters.GetPrivateKeySizeInBytes());
+  params.set_hash_type(*hash_type);
+  params.set_sig_type(*signature_type);
+
+  return params;
+}
+
+util::StatusOr<SlhDsaParameters> ParseParameters(
+    const internal::ProtoParametersSerialization& serialization) {
+  if (serialization.GetKeyTemplate().type_url() != kPrivateTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing SlhDsaParameters.");
+  }
+
+  SlhDsaKeyFormat proto_key_format;
+  if (!proto_key_format.ParseFromString(
+          serialization.GetKeyTemplate().value())) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse SlhDsaKeyFormat proto");
+  }
+  if (proto_key_format.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  if (!proto_key_format.has_params()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SlhDsaKeyFormat proto is missing params field.");
+  }
+
+  return ToParameters(serialization.GetKeyTemplate().output_prefix_type(),
+                      proto_key_format.params());
+}
+
+util::StatusOr<SlhDsaPublicKey> ParsePublicKey(
+    const internal::ProtoKeySerialization& serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  if (serialization.TypeUrl() != kPublicTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing SlhDsaPublicKey.");
+  }
+
+  google::crypto::tink::SlhDsaPublicKey proto_key;
+  const RestrictedData& restricted_data = serialization.SerializedKeyProto();
+  if (!proto_key.ParseFromString(
+          restricted_data.GetSecret(InsecureSecretKeyAccess::Get()))) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse SlhDsaPublicKey proto");
+  }
+  if (proto_key.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  util::StatusOr<SlhDsaParameters> parameters =
+      ToParameters(serialization.GetOutputPrefixType(), proto_key.params());
+  if (!parameters.ok()) {
+    return parameters.status();
+  }
+
+  return SlhDsaPublicKey::Create(*parameters, proto_key.key_value(),
+                                 serialization.IdRequirement(),
+                                 GetPartialKeyAccess());
+}
+
+util::StatusOr<SlhDsaPrivateKey> ParsePrivateKey(
+    const internal::ProtoKeySerialization& serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  if (serialization.TypeUrl() != kPrivateTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing SlhDsaPrivateKey.");
+  }
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kPermissionDenied,
+                        "SecretKeyAccess is required");
+  }
+  google::crypto::tink::SlhDsaPrivateKey proto_key;
+  const RestrictedData& restricted_data = serialization.SerializedKeyProto();
+  if (!proto_key.ParseFromString(restricted_data.GetSecret(*token))) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse SlhDsaPrivateKey proto");
+  }
+  if (proto_key.version() != 0) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Only version 0 keys are accepted.");
+  }
+
+  util::StatusOr<SlhDsaParameters> parameters = ToParameters(
+      serialization.GetOutputPrefixType(), proto_key.public_key().params());
+  if (!parameters.ok()) {
+    return parameters.status();
+  }
+
+  util::StatusOr<SlhDsaPublicKey> public_key = SlhDsaPublicKey::Create(
+      *parameters, proto_key.public_key().key_value(),
+      serialization.IdRequirement(), GetPartialKeyAccess());
+  if (!public_key.ok()) {
+    return public_key.status();
+  }
+
+  return SlhDsaPrivateKey::Create(*public_key,
+                                  RestrictedData(proto_key.key_value(), *token),
+                                  GetPartialKeyAccess());
+}
+
+util::StatusOr<internal::ProtoParametersSerialization> SerializeParameters(
+    const SlhDsaParameters& parameters) {
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(parameters.GetVariant());
+  if (!output_prefix_type.ok()) {
+    return output_prefix_type.status();
+  }
+
+  util::StatusOr<SlhDsaParams> params = FromParameters(parameters);
+  if (!params.ok()) {
+    return params.status();
+  }
+  SlhDsaKeyFormat proto_key_format;
+  *proto_key_format.mutable_params() = *params;
+  proto_key_format.set_version(0);
+
+  return internal::ProtoParametersSerialization::Create(
+      kPrivateTypeUrl, *output_prefix_type,
+      proto_key_format.SerializeAsString());
+}
+
+util::StatusOr<internal::ProtoKeySerialization> SerializePublicKey(
+    const SlhDsaPublicKey& key, absl::optional<SecretKeyAccessToken> token) {
+  util::StatusOr<SlhDsaParams> params = FromParameters(key.GetParameters());
+  if (!params.ok()) {
+    return params.status();
+  }
+
+  google::crypto::tink::SlhDsaPublicKey proto_key;
+  proto_key.set_version(0);
+  *proto_key.mutable_params() = *params;
+  proto_key.set_key_value(key.GetPublicKeyBytes(GetPartialKeyAccess()));
+
+  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(), InsecureSecretKeyAccess::Get());
+  return internal::ProtoKeySerialization::Create(
+      kPublicTypeUrl, restricted_output, KeyData::ASYMMETRIC_PUBLIC,
+      *output_prefix_type, key.GetIdRequirement());
+}
+
+util::StatusOr<internal::ProtoKeySerialization> SerializePrivateKey(
+    const SlhDsaPrivateKey& key, absl::optional<SecretKeyAccessToken> token) {
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kPermissionDenied,
+                        "SecretKeyAccess is required");
+  }
+  util::StatusOr<RestrictedData> restricted_input =
+      key.GetPrivateKeyBytes(GetPartialKeyAccess());
+  if (!restricted_input.ok()) {
+    return restricted_input.status();
+  }
+
+  util::StatusOr<SlhDsaParams> params =
+      FromParameters(key.GetPublicKey().GetParameters());
+  if (!params.ok()) {
+    return params.status();
+  }
+
+  google::crypto::tink::SlhDsaPublicKey proto_public_key;
+  proto_public_key.set_version(0);
+  *proto_public_key.mutable_params() = *params;
+  proto_public_key.set_key_value(
+      key.GetPublicKey().GetPublicKeyBytes(GetPartialKeyAccess()));
+
+  google::crypto::tink::SlhDsaPrivateKey proto_private_key;
+  proto_private_key.set_version(0);
+  *proto_private_key.mutable_public_key() = proto_public_key;
+  proto_private_key.set_key_value(restricted_input->GetSecret(*token));
+
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(key.GetPublicKey().GetParameters().GetVariant());
+  if (!output_prefix_type.ok()) {
+    return output_prefix_type.status();
+  }
+
+  RestrictedData restricted_output =
+      RestrictedData(proto_private_key.SerializeAsString(), *token);
+  return internal::ProtoKeySerialization::Create(
+      kPrivateTypeUrl, restricted_output, KeyData::ASYMMETRIC_PRIVATE,
+      *output_prefix_type, key.GetIdRequirement());
+}
+
+SlhDsaProtoParametersParserImpl& SlhDsaProtoParametersParser() {
+  static auto parser =
+      new SlhDsaProtoParametersParserImpl(kPrivateTypeUrl, ParseParameters);
+  return *parser;
+}
+
+SlhDsaProtoParametersSerializerImpl& SlhDsaProtoParametersSerializer() {
+  static auto serializer = new SlhDsaProtoParametersSerializerImpl(
+      kPrivateTypeUrl, SerializeParameters);
+  return *serializer;
+}
+
+SlhDsaProtoPublicKeyParserImpl& SlhDsaProtoPublicKeyParser() {
+  static auto* parser =
+      new SlhDsaProtoPublicKeyParserImpl(kPublicTypeUrl, ParsePublicKey);
+  return *parser;
+}
+
+SlhDsaProtoPublicKeySerializerImpl& SlhDsaProtoPublicKeySerializer() {
+  static auto* serializer =
+      new SlhDsaProtoPublicKeySerializerImpl(SerializePublicKey);
+  return *serializer;
+}
+
+SlhDsaProtoPrivateKeyParserImpl& SlhDsaProtoPrivateKeyParser() {
+  static auto* parser =
+      new SlhDsaProtoPrivateKeyParserImpl(kPrivateTypeUrl, ParsePrivateKey);
+  return *parser;
+}
+
+SlhDsaProtoPrivateKeySerializerImpl& SlhDsaProtoPrivateKeySerializer() {
+  static auto* serializer =
+      new SlhDsaProtoPrivateKeySerializerImpl(SerializePrivateKey);
+  return *serializer;
+}
+
+}  // namespace
+
+util::Status RegisterSlhDsaProtoSerialization() {
+  util::Status status =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .RegisterParametersParser(&SlhDsaProtoParametersParser());
+  if (!status.ok()) {
+    return status;
+  }
+
+  status =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .RegisterParametersSerializer(&SlhDsaProtoParametersSerializer());
+  if (!status.ok()) {
+    return status;
+  }
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterKeyParser(&SlhDsaProtoPublicKeyParser());
+  if (!status.ok()) {
+    return status;
+  }
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterKeySerializer(&SlhDsaProtoPublicKeySerializer());
+  if (!status.ok()) {
+    return status;
+  }
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterKeyParser(&SlhDsaProtoPrivateKeyParser());
+  if (!status.ok()) {
+    return status;
+  }
+
+  return internal::MutableSerializationRegistry::GlobalInstance()
+      .RegisterKeySerializer(&SlhDsaProtoPrivateKeySerializer());
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_proto_serialization.h b/cc/experimental/pqcrypto/signature/slh_dsa_proto_serialization.h
new file mode 100644
index 0000000..59f5c62
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_proto_serialization.h
@@ -0,0 +1,31 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PROTO_SERIALIZATION_H_
+#define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PROTO_SERIALIZATION_H_
+
+#include "tink/util/status.h"
+
+namespace crypto {
+namespace tink {
+
+// Registers proto parsers and serializers for SLH-DSA parameters and keys.
+crypto::tink::util::Status RegisterSlhDsaProtoSerialization();
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PROTO_SERIALIZATION_H_
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_proto_serialization_test.cc b/cc/experimental/pqcrypto/signature/slh_dsa_proto_serialization_test.cc
new file mode 100644
index 0000000..b516ac6
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_proto_serialization_test.cc
@@ -0,0 +1,731 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/experimental/pqcrypto/signature/slh_dsa_proto_serialization.h"
+
+#include <cstdint>
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#define OPENSSL_UNSTABLE_EXPERIMENTAL_SPX
+#include "openssl/experimental/spx.h"
+#undef OPENSSL_UNSTABLE_EXPERIMENTAL_SPX
+#include "tink/experimental/pqcrypto/signature/slh_dsa_parameters.h"
+#include "tink/experimental/pqcrypto/signature/slh_dsa_private_key.h"
+#include "tink/experimental/pqcrypto/signature/slh_dsa_public_key.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/internal/serialization.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/random.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "proto/experimental/pqcrypto/slh_dsa.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::StatusIs;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::google::crypto::tink::SlhDsaHashType;
+using ::google::crypto::tink::SlhDsaKeyFormat;
+using ::google::crypto::tink::SlhDsaParams;
+using ::google::crypto::tink::SlhDsaSignatureType;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using ::testing::IsTrue;
+using ::testing::NotNull;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+const absl::string_view kPrivateTypeUrl =
+    "type.googleapis.com/google.crypto.tink.SlhDsaPrivateKey";
+const absl::string_view kPublicTypeUrl =
+    "type.googleapis.com/google.crypto.tink.SlhDsaPublicKey";
+
+struct TestCase {
+  SlhDsaParameters::Variant variant;
+  OutputPrefixType output_prefix_type;
+  absl::optional<int> id_requirement;
+  std::string output_prefix;
+};
+
+class SlhDsaProtoSerializationTest : public TestWithParam<TestCase> {
+ protected:
+  SlhDsaProtoSerializationTest() {
+    internal::MutableSerializationRegistry::GlobalInstance().Reset();
+  }
+};
+
+TEST_F(SlhDsaProtoSerializationTest, RegisterTwiceSucceeds) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    SlhDsaProtoSerializationTestSuite, SlhDsaProtoSerializationTest,
+    Values(TestCase{SlhDsaParameters::Variant::kTink, OutputPrefixType::TINK,
+                    0x02030400, std::string("\x01\x02\x03\x04\x00", 5)},
+           TestCase{SlhDsaParameters::Variant::kTink, OutputPrefixType::TINK,
+                    0x03050709, std::string("\x01\x03\x05\x07\x09", 5)},
+           TestCase{SlhDsaParameters::Variant::kNoPrefix, OutputPrefixType::RAW,
+                    absl::nullopt, ""}));
+
+TEST_P(SlhDsaProtoSerializationTest,
+       ParseSlhDsa128Sha2SmallSignatureParametersWorks) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  SlhDsaKeyFormat key_format_proto;
+  SlhDsaParams& params = *key_format_proto.mutable_params();
+  params.set_sig_type(SlhDsaSignatureType::SMALL_SIGNATURE);
+  params.set_hash_type(SlhDsaHashType::SHA2);
+  params.set_key_size(64);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kPrivateTypeUrl, test_case.output_prefix_type,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parameters =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(parameters, IsOk());
+  EXPECT_EQ((*parameters)->HasIdRequirement(),
+            test_case.id_requirement.has_value());
+
+  const SlhDsaParameters* slh_dsa_parameters =
+      dynamic_cast<const SlhDsaParameters*>(parameters->get());
+  ASSERT_THAT(slh_dsa_parameters, NotNull());
+  EXPECT_THAT(slh_dsa_parameters->GetVariant(), Eq(test_case.variant));
+  EXPECT_THAT(slh_dsa_parameters->GetPrivateKeySizeInBytes(), Eq(64));
+  EXPECT_THAT(slh_dsa_parameters->GetSignatureType(),
+              Eq(SlhDsaParameters::SignatureType::kSmallSignature));
+  EXPECT_THAT(slh_dsa_parameters->GetHashType(),
+              Eq(SlhDsaParameters::HashType::kSha2));
+}
+
+TEST_F(SlhDsaProtoSerializationTest,
+       ParseParametersWithInvalidSerializationFails) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kPrivateTypeUrl, OutputPrefixType::RAW, "invalid_serialization");
+  ASSERT_THAT(serialization, IsOk());
+
+  EXPECT_THAT(internal::MutableSerializationRegistry::GlobalInstance()
+                  .ParseParameters(*serialization)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("Failed to parse SlhDsaKeyFormat proto")));
+}
+
+TEST_F(SlhDsaProtoSerializationTest, ParseParametersWithInvalidVersionFails) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  SlhDsaKeyFormat key_format_proto;
+  key_format_proto.set_version(1);
+  SlhDsaParams& params = *key_format_proto.mutable_params();
+  params.set_sig_type(SlhDsaSignatureType::SMALL_SIGNATURE);
+  params.set_hash_type(SlhDsaHashType::SHA2);
+  params.set_key_size(64);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kPrivateTypeUrl, OutputPrefixType::RAW,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parameters =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(parameters.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("Only version 0 keys are accepted")));
+}
+
+TEST_F(SlhDsaProtoSerializationTest,
+       ParseParametersKeyFormatWithoutParamsFails) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  SlhDsaKeyFormat key_format_proto;
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kPrivateTypeUrl, OutputPrefixType::RAW,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parameters =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+
+  ASSERT_THAT(parameters.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("SlhDsaKeyFormat proto is missing params")));
+}
+
+TEST_F(SlhDsaProtoSerializationTest,
+       ParseParametersWithUnkownOutputPrefixFails) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  SlhDsaKeyFormat key_format_proto;
+  SlhDsaParams& params = *key_format_proto.mutable_params();
+  params.set_sig_type(SlhDsaSignatureType::SMALL_SIGNATURE);
+  params.set_hash_type(SlhDsaHashType::SHA2);
+  params.set_key_size(64);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kPrivateTypeUrl, OutputPrefixType::UNKNOWN_PREFIX,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parameters =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(
+      parameters.status(),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("Could not determine SlhDsaParameters::Variant")));
+}
+
+TEST_F(SlhDsaProtoSerializationTest, ParseParametersWithUnkownSigTypeFails) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  SlhDsaKeyFormat key_format_proto;
+  SlhDsaParams& params = *key_format_proto.mutable_params();
+  params.set_sig_type(SlhDsaSignatureType::SLH_DSA_SIGNATURE_TYPE_UNSPECIFIED);
+  params.set_hash_type(SlhDsaHashType::SHA2);
+  params.set_key_size(64);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kPrivateTypeUrl, OutputPrefixType::RAW,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parameters =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(
+      parameters.status(),
+      StatusIs(
+          absl::StatusCode::kInvalidArgument,
+          HasSubstr("Could not determine SlhDsaParameters::SignatureType")));
+}
+
+TEST_F(SlhDsaProtoSerializationTest, ParseParametersWithUnkownHashTypeFails) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  SlhDsaKeyFormat key_format_proto;
+  SlhDsaParams& params = *key_format_proto.mutable_params();
+  params.set_sig_type(SlhDsaSignatureType::SMALL_SIGNATURE);
+  params.set_hash_type(SlhDsaHashType::SLH_DSA_HASH_TYPE_UNSPECIFIED);
+  params.set_key_size(64);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kPrivateTypeUrl, OutputPrefixType::RAW,
+          key_format_proto.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parameters =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  EXPECT_THAT(
+      parameters.status(),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("Could not determine SlhDsaParameters::HashType")));
+}
+
+TEST_P(SlhDsaProtoSerializationTest,
+       SerializeSlhDsa128Sha2SmallSignatureParametersWorks) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature, 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(kPrivateTypeUrl));
+
+  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(kPrivateTypeUrl));
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().output_prefix_type(),
+              Eq(test_case.output_prefix_type));
+
+  SlhDsaKeyFormat key_format;
+  ASSERT_THAT(
+      key_format.ParseFromString(proto_serialization->GetKeyTemplate().value()),
+      IsTrue());
+  ASSERT_TRUE(key_format.has_params());
+  EXPECT_THAT(key_format.params().hash_type(), Eq(SlhDsaHashType::SHA2));
+  EXPECT_THAT(key_format.params().sig_type(),
+              Eq(SlhDsaSignatureType::SMALL_SIGNATURE));
+  EXPECT_THAT(key_format.params().key_size(), Eq(64));
+}
+
+TEST_P(SlhDsaProtoSerializationTest, ParsePublicKeyWorks) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  SlhDsaParams params;
+  params.set_sig_type(SlhDsaSignatureType::SMALL_SIGNATURE);
+  params.set_hash_type(SlhDsaHashType::SHA2);
+  params.set_key_size(64);
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::SlhDsaPublicKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_key_value(raw_key_bytes);
+  *key_proto.mutable_params() = params;
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          kPublicTypeUrl, serialized_key, KeyData::ASYMMETRIC_PUBLIC,
+          test_case.output_prefix_type, test_case.id_requirement);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, /*token=*/absl::nullopt);
+  ASSERT_THAT(key, IsOk());
+  EXPECT_THAT((*key)->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT((*key)->GetParameters().HasIdRequirement(),
+              test_case.id_requirement.has_value());
+
+  util::StatusOr<SlhDsaParameters> expected_parameters =
+      SlhDsaParameters::Create(
+          SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+          SlhDsaParameters::SignatureType::kSmallSignature, test_case.variant);
+  ASSERT_THAT(expected_parameters, IsOk());
+
+  util::StatusOr<SlhDsaPublicKey> expected_key =
+      SlhDsaPublicKey::Create(*expected_parameters, raw_key_bytes,
+                              test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(expected_key, IsOk());
+
+  EXPECT_THAT(**key, Eq(*expected_key));
+}
+
+TEST_F(SlhDsaProtoSerializationTest,
+       ParsePublicKeyWithInvalidSerializationFails) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  RestrictedData serialized_key =
+      RestrictedData("invalid_serialization", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(kPublicTypeUrl, serialized_key,
+                                              KeyData::ASYMMETRIC_PUBLIC,
+                                              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,
+                       HasSubstr("Failed to parse SlhDsaPublicKey proto")));
+}
+
+TEST_F(SlhDsaProtoSerializationTest, ParsePublicKeyWithInvalidVersionFails) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  SlhDsaParams params;
+  params.set_sig_type(SlhDsaSignatureType::SMALL_SIGNATURE);
+  params.set_hash_type(SlhDsaHashType::SHA2);
+  params.set_key_size(64);
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::SlhDsaPublicKey key_proto;
+  key_proto.set_version(1);
+  key_proto.set_key_value(raw_key_bytes);
+  *key_proto.mutable_params() = params;
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(kPublicTypeUrl, serialized_key,
+                                              KeyData::ASYMMETRIC_PUBLIC,
+                                              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,
+                       HasSubstr("Only version 0 keys are accepted")));
+}
+
+TEST_P(SlhDsaProtoSerializationTest, SerializePublicKeyWorks) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  util::StatusOr<SlhDsaPublicKey> key =
+      SlhDsaPublicKey::Create(*parameters, raw_key_bytes,
+                              test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, /*token=*/absl::nullopt);
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(), Eq(kPublicTypeUrl));
+
+  const internal::ProtoKeySerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoKeySerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->TypeUrl(), Eq(kPublicTypeUrl));
+  EXPECT_THAT(proto_serialization->KeyMaterialType(),
+              Eq(KeyData::ASYMMETRIC_PUBLIC));
+  EXPECT_THAT(proto_serialization->GetOutputPrefixType(),
+              Eq(test_case.output_prefix_type));
+  EXPECT_THAT(proto_serialization->IdRequirement(),
+              Eq(test_case.id_requirement));
+
+  google::crypto::tink::SlhDsaPublicKey proto_key;
+  ASSERT_THAT(proto_key.ParseFromString(
+                  proto_serialization->SerializedKeyProto().GetSecret(
+                      InsecureSecretKeyAccess::Get())),
+              IsTrue());
+  EXPECT_THAT(proto_key.version(), Eq(0));
+  EXPECT_THAT(proto_key.key_value(), Eq(raw_key_bytes));
+  EXPECT_THAT(proto_key.has_params(), IsTrue());
+  EXPECT_THAT(proto_key.params().key_size(), Eq(64));
+  EXPECT_THAT(proto_key.params().hash_type(), Eq(SlhDsaHashType::SHA2));
+  EXPECT_THAT(proto_key.params().sig_type(),
+              Eq(SlhDsaSignatureType::SMALL_SIGNATURE));
+}
+
+TEST_P(SlhDsaProtoSerializationTest, ParsePrivateKeyWorks) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes;
+  private_key_bytes.resize(SPX_SECRET_KEY_BYTES);
+
+  SPX_generate_key(reinterpret_cast<uint8_t*>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t*>(private_key_bytes.data()));
+
+  SlhDsaParams params;
+  params.set_sig_type(SlhDsaSignatureType::SMALL_SIGNATURE);
+  params.set_hash_type(SlhDsaHashType::SHA2);
+  params.set_key_size(64);
+
+  google::crypto::tink::SlhDsaPublicKey public_key_proto;
+  public_key_proto.set_version(0);
+  public_key_proto.set_key_value(public_key_bytes);
+  *public_key_proto.mutable_params() = params;
+
+  google::crypto::tink::SlhDsaPrivateKey private_key_proto;
+  private_key_proto.set_version(0);
+  *private_key_proto.mutable_public_key() = public_key_proto;
+  private_key_proto.set_key_value(private_key_bytes);
+
+  RestrictedData serialized_key = RestrictedData(
+      private_key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          kPrivateTypeUrl, serialized_key, KeyData::ASYMMETRIC_PRIVATE,
+          test_case.output_prefix_type, test_case.id_requirement);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> private_key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(private_key, IsOk());
+
+  EXPECT_THAT((*private_key)->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT((*private_key)->GetParameters().HasIdRequirement(),
+              test_case.id_requirement.has_value());
+
+  util::StatusOr<SlhDsaParameters> expected_parameters =
+      SlhDsaParameters::Create(
+          SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+          SlhDsaParameters::SignatureType::kSmallSignature, test_case.variant);
+  ASSERT_THAT(expected_parameters, IsOk());
+
+  util::StatusOr<SlhDsaPublicKey> expected_public_key =
+      SlhDsaPublicKey::Create(*expected_parameters, public_key_bytes,
+                              test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(expected_public_key, IsOk());
+
+  util::StatusOr<SlhDsaPrivateKey> expected_private_key =
+      SlhDsaPrivateKey::Create(
+          *expected_public_key,
+          RestrictedData(private_key_bytes, InsecureSecretKeyAccess::Get()),
+          GetPartialKeyAccess());
+  ASSERT_THAT(expected_private_key, IsOk());
+
+  EXPECT_THAT(**private_key, Eq(*expected_private_key));
+}
+
+TEST_F(SlhDsaProtoSerializationTest, ParsePrivateKeyWithInvalidSerialization) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  RestrictedData serialized_key =
+      RestrictedData("invalid_serialization", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(kPrivateTypeUrl, serialized_key,
+                                              KeyData::ASYMMETRIC_PRIVATE,
+                                              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,
+                       HasSubstr("Failed to parse SlhDsaPrivateKey proto")));
+}
+
+TEST_F(SlhDsaProtoSerializationTest, ParsePrivateKeyWithInvalidVersion) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes;
+  private_key_bytes.resize(SPX_SECRET_KEY_BYTES);
+
+  SPX_generate_key(reinterpret_cast<uint8_t*>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t*>(private_key_bytes.data()));
+
+  SlhDsaParams params;
+  params.set_sig_type(SlhDsaSignatureType::SMALL_SIGNATURE);
+  params.set_hash_type(SlhDsaHashType::SHA2);
+  params.set_key_size(64);
+
+  google::crypto::tink::SlhDsaPublicKey public_key_proto;
+  public_key_proto.set_version(0);
+  public_key_proto.set_key_value(public_key_bytes);
+  *public_key_proto.mutable_params() = params;
+
+  google::crypto::tink::SlhDsaPrivateKey private_key_proto;
+  private_key_proto.set_version(1);
+  *private_key_proto.mutable_public_key() = public_key_proto;
+  private_key_proto.set_key_value(private_key_bytes);
+
+  RestrictedData serialized_key = RestrictedData(
+      private_key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(kPrivateTypeUrl, serialized_key,
+                                              KeyData::ASYMMETRIC_PRIVATE,
+                                              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,
+                       HasSubstr("Only version 0 keys are accepted")));
+}
+
+TEST_F(SlhDsaProtoSerializationTest, ParsePrivateKeyNoSecretKeyAccess) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes;
+  private_key_bytes.resize(SPX_SECRET_KEY_BYTES);
+
+  SPX_generate_key(reinterpret_cast<uint8_t*>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t*>(private_key_bytes.data()));
+
+  SlhDsaParams params;
+  params.set_sig_type(SlhDsaSignatureType::SMALL_SIGNATURE);
+  params.set_hash_type(SlhDsaHashType::SHA2);
+  params.set_key_size(64);
+
+  google::crypto::tink::SlhDsaPublicKey public_key_proto;
+  public_key_proto.set_version(0);
+  public_key_proto.set_key_value(public_key_bytes);
+  *public_key_proto.mutable_params() = params;
+
+  google::crypto::tink::SlhDsaPrivateKey private_key_proto;
+  private_key_proto.set_version(0);
+  *private_key_proto.mutable_public_key() = public_key_proto;
+  private_key_proto.set_key_value(private_key_bytes);
+
+  RestrictedData serialized_key = RestrictedData(
+      private_key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(kPrivateTypeUrl, serialized_key,
+                                              KeyData::ASYMMETRIC_PRIVATE,
+                                              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::kPermissionDenied));
+}
+
+TEST_P(SlhDsaProtoSerializationTest, SerializePrivateKey) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes;
+  private_key_bytes.resize(SPX_SECRET_KEY_BYTES);
+
+  SPX_generate_key(reinterpret_cast<uint8_t*>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t*>(private_key_bytes.data()));
+
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<SlhDsaPublicKey> public_key =
+      SlhDsaPublicKey::Create(*parameters, public_key_bytes,
+                              test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  util::StatusOr<SlhDsaPrivateKey> private_key = SlhDsaPrivateKey::Create(
+      *public_key,
+      RestrictedData(private_key_bytes, InsecureSecretKeyAccess::Get()),
+      GetPartialKeyAccess());
+  ASSERT_THAT(private_key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *private_key, InsecureSecretKeyAccess::Get());
+  ASSERT_THAT(serialization, IsOk());
+  EXPECT_THAT((*serialization)->ObjectIdentifier(), Eq(kPrivateTypeUrl));
+
+  const internal::ProtoKeySerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoKeySerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->TypeUrl(), Eq(kPrivateTypeUrl));
+  EXPECT_THAT(proto_serialization->KeyMaterialType(),
+              Eq(KeyData::ASYMMETRIC_PRIVATE));
+  EXPECT_THAT(proto_serialization->GetOutputPrefixType(),
+              Eq(test_case.output_prefix_type));
+  EXPECT_THAT(proto_serialization->IdRequirement(),
+              Eq(test_case.id_requirement));
+
+  google::crypto::tink::SlhDsaPrivateKey proto_key;
+  ASSERT_THAT(proto_key.ParseFromString(
+                  proto_serialization->SerializedKeyProto().GetSecret(
+                      InsecureSecretKeyAccess::Get())),
+              IsTrue());
+  EXPECT_THAT(proto_key.version(), Eq(0));
+  EXPECT_THAT(proto_key.key_value(), Eq(private_key_bytes));
+  EXPECT_THAT(proto_key.has_public_key(), IsTrue());
+  EXPECT_THAT(proto_key.public_key().version(), Eq(0));
+  EXPECT_THAT(proto_key.public_key().key_value(), Eq(public_key_bytes));
+  EXPECT_THAT(proto_key.public_key().has_params(), IsTrue());
+  EXPECT_THAT(proto_key.public_key().params().key_size(), Eq(64));
+  EXPECT_THAT(proto_key.public_key().params().hash_type(),
+              Eq(SlhDsaHashType::SHA2));
+  EXPECT_THAT(proto_key.public_key().params().sig_type(),
+              Eq(SlhDsaSignatureType::SMALL_SIGNATURE));
+}
+
+TEST_F(SlhDsaProtoSerializationTest, SerializePrivateKeyNoSecretKeyAccess) {
+  ASSERT_THAT(RegisterSlhDsaProtoSerialization(), IsOk());
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(SPX_PUBLIC_KEY_BYTES);
+  std::string private_key_bytes;
+  private_key_bytes.resize(SPX_SECRET_KEY_BYTES);
+
+  SPX_generate_key(reinterpret_cast<uint8_t*>(public_key_bytes.data()),
+                   reinterpret_cast<uint8_t*>(private_key_bytes.data()));
+
+  util::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature,
+      SlhDsaParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<SlhDsaPublicKey> public_key =
+      SlhDsaPublicKey::Create(*parameters, public_key_bytes,
+                              /*id_requirement=*/123, GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  util::StatusOr<SlhDsaPrivateKey> private_key = SlhDsaPrivateKey::Create(
+      *public_key,
+      RestrictedData(private_key_bytes, InsecureSecretKeyAccess::Get()),
+      GetPartialKeyAccess());
+  ASSERT_THAT(private_key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *private_key, /*token=*/absl::nullopt);
+  ASSERT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kPermissionDenied,
+                       HasSubstr("SecretKeyAccess is required")));
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_public_key.cc b/cc/experimental/pqcrypto/signature/slh_dsa_public_key.cc
new file mode 100644
index 0000000..9f600e5
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_public_key.cc
@@ -0,0 +1,99 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/experimental/pqcrypto/signature/slh_dsa_public_key.h"
+
+#include <string>
+
+#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/optional.h"
+#include "tink/experimental/pqcrypto/signature/slh_dsa_parameters.h"
+#include "tink/key.h"
+#include "tink/partial_key_access_token.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 SlhDsaParameters& parameters, absl::optional<int> id_requirement) {
+  switch (parameters.GetVariant()) {
+    case SlhDsaParameters::Variant::kNoPrefix:
+      return std::string("");  // Empty prefix.
+    case SlhDsaParameters::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<SlhDsaPublicKey> SlhDsaPublicKey::Create(
+    const SlhDsaParameters& parameters, absl::string_view public_key_bytes,
+    absl::optional<int> id_requirement, PartialKeyAccessToken token) {
+  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");
+  }
+  // Only 32-byte public keys are supported at the moment.
+  if (public_key_bytes.size() != 32) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Invalid public key size. Only 32-byte keys are "
+                        "currently supported.");
+  }
+  util::StatusOr<std::string> output_prefix =
+      ComputeOutputPrefix(parameters, id_requirement);
+  if (!output_prefix.ok()) {
+    return output_prefix.status();
+  }
+  return SlhDsaPublicKey(parameters, public_key_bytes, id_requirement,
+                         *output_prefix);
+}
+
+bool SlhDsaPublicKey::operator==(const Key& other) const {
+  const SlhDsaPublicKey* that = dynamic_cast<const SlhDsaPublicKey*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  return GetParameters() == that->GetParameters() &&
+         public_key_bytes_ == that->public_key_bytes_ &&
+         id_requirement_ == that->id_requirement_;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_public_key.h b/cc/experimental/pqcrypto/signature/slh_dsa_public_key.h
new file mode 100644
index 0000000..24fe2fe
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_public_key.h
@@ -0,0 +1,85 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PUBLIC_KEY_H_
+#define TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PUBLIC_KEY_H_
+
+#include <string>
+
+#include "absl/base/attributes.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/experimental/pqcrypto/signature/slh_dsa_parameters.h"
+#include "tink/key.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/signature/signature_public_key.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Representation of the verification function for the SLH-DSA digital signature
+// primitive.
+class SlhDsaPublicKey : public SignaturePublicKey {
+ public:
+  // Copyable and movable.
+  SlhDsaPublicKey(const SlhDsaPublicKey& other) = default;
+  SlhDsaPublicKey& operator=(const SlhDsaPublicKey& other) = default;
+  SlhDsaPublicKey(SlhDsaPublicKey&& other) = default;
+  SlhDsaPublicKey& operator=(SlhDsaPublicKey&& other) = default;
+
+  // Creates a new SLH-DSA public key from `public_key_bytes`. If the
+  // `parameters` specify a variant that uses a prefix, then `id_requirement` is
+  // used to compute this prefix.
+  static util::StatusOr<SlhDsaPublicKey> Create(
+      const SlhDsaParameters& parameters, absl::string_view public_key_bytes,
+      absl::optional<int> id_requirement, PartialKeyAccessToken token);
+
+  absl::string_view GetPublicKeyBytes(PartialKeyAccessToken token) const
+      ABSL_ATTRIBUTE_LIFETIME_BOUND {
+    return public_key_bytes_;
+  }
+
+  absl::string_view GetOutputPrefix() const override { return output_prefix_; }
+
+  const SlhDsaParameters& GetParameters() const override { return parameters_; }
+
+  absl::optional<int> GetIdRequirement() const override {
+    return id_requirement_;
+  }
+
+  bool operator==(const Key& other) const override;
+
+ private:
+  explicit SlhDsaPublicKey(const SlhDsaParameters& parameters,
+                           absl::string_view public_key_bytes,
+                           absl::optional<int> id_requirement,
+                           absl::string_view output_prefix)
+      : parameters_(parameters),
+        public_key_bytes_(public_key_bytes),
+        id_requirement_(id_requirement),
+        output_prefix_(output_prefix) {}
+
+  SlhDsaParameters parameters_;
+  std::string public_key_bytes_;
+  absl::optional<int> id_requirement_;
+  std::string output_prefix_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_EXPERIMENTAL_PQCRYPTO_SIGNATURE_SLH_DSA_PUBLIC_KEY_H_
diff --git a/cc/experimental/pqcrypto/signature/slh_dsa_public_key_test.cc b/cc/experimental/pqcrypto/signature/slh_dsa_public_key_test.cc
new file mode 100644
index 0000000..efbaa95
--- /dev/null
+++ b/cc/experimental/pqcrypto/signature/slh_dsa_public_key_test.cc
@@ -0,0 +1,212 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/experimental/pqcrypto/signature/slh_dsa_public_key.h"
+
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/experimental/pqcrypto/signature/slh_dsa_parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/subtle/random.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::Eq;
+using ::testing::HasSubstr;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  SlhDsaParameters::Variant variant;
+  absl::optional<int> id_requirement;
+  std::string output_prefix;
+};
+
+using SlhDsaPublicKeyTest = TestWithParam<TestCase>;
+
+INSTANTIATE_TEST_SUITE_P(
+    SlhDsaPublicKeyTestSuite, SlhDsaPublicKeyTest,
+    Values(TestCase{SlhDsaParameters::Variant::kTink, 0x02030400,
+                    std::string("\x01\x02\x03\x04\x00", 5)},
+           TestCase{SlhDsaParameters::Variant::kTink, 0x03050709,
+                    std::string("\x01\x03\x05\x07\x09", 5)},
+           TestCase{SlhDsaParameters::Variant::kNoPrefix, absl::nullopt, ""}));
+
+TEST_P(SlhDsaPublicKeyTest, CreatePublicKeyWorks) {
+  TestCase test_case = GetParam();
+
+  util::StatusOr<SlhDsaParameters> params = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  std::string public_key_bytes = subtle::Random::GetRandomBytes(32);
+  util::StatusOr<SlhDsaPublicKey> public_key =
+      SlhDsaPublicKey::Create(*params, public_key_bytes,
+                              test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  EXPECT_THAT(public_key->GetParameters(), Eq(*params));
+  EXPECT_THAT(public_key->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT(public_key->GetOutputPrefix(), Eq(test_case.output_prefix));
+  EXPECT_THAT(public_key->GetPublicKeyBytes(GetPartialKeyAccess()),
+              Eq(public_key_bytes));
+}
+
+TEST(SlhDsaPublicKeyTest, CreateWithInvalidPublicKeyLengthFails) {
+  util::StatusOr<SlhDsaParameters> params = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature,
+      SlhDsaParameters::Variant::kTink);
+  ASSERT_THAT(params, IsOk());
+
+  std::string public_key_bytes = subtle::Random::GetRandomBytes(31);
+
+  EXPECT_THAT(
+      SlhDsaPublicKey::Create(*params, public_key_bytes,
+                              /*id_requirement=*/123, GetPartialKeyAccess())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("Invalid public key size")));
+}
+
+TEST(SlhDsaPublicKeyTest, CreateKeyWithNoIdRequirementWithTinkParamsFails) {
+  util::StatusOr<SlhDsaParameters> tink_params = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature,
+      SlhDsaParameters::Variant::kTink);
+  ASSERT_THAT(tink_params, IsOk());
+
+  std::string public_key_bytes = subtle::Random::GetRandomBytes(32);
+
+  EXPECT_THAT(SlhDsaPublicKey::Create(*tink_params, public_key_bytes,
+                                      /*id_requirement=*/absl::nullopt,
+                                      GetPartialKeyAccess())
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("key without ID requirement with parameters "
+                                 "with ID requirement")));
+}
+
+TEST(SlhDsaPublicKeyTest, CreateKeyWithIdRequirementWithNoPrefixParamsFails) {
+  util::StatusOr<SlhDsaParameters> no_prefix_params =
+      SlhDsaParameters::Create(SlhDsaParameters::HashType::kSha2,
+                               /*private_key_size_in_bytes=*/64,
+                               SlhDsaParameters::SignatureType::kSmallSignature,
+                               SlhDsaParameters::Variant::kNoPrefix);
+  ASSERT_THAT(no_prefix_params, IsOk());
+
+  std::string public_key_bytes = subtle::Random::GetRandomBytes(32);
+
+  EXPECT_THAT(
+      SlhDsaPublicKey::Create(*no_prefix_params, public_key_bytes,
+                              /*id_requirement=*/123, GetPartialKeyAccess())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("key with ID requirement with parameters without ID "
+                         "requirement")));
+}
+
+TEST_P(SlhDsaPublicKeyTest, PublicKeyEquals) {
+  TestCase test_case = GetParam();
+
+  util::StatusOr<SlhDsaParameters> params = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2, /*private_key_size_in_bytes=*/64,
+      SlhDsaParameters::SignatureType::kSmallSignature, test_case.variant);
+  ASSERT_THAT(params, IsOk());
+
+  std::string public_key_bytes = subtle::Random::GetRandomBytes(32);
+
+  util::StatusOr<SlhDsaPublicKey> public_key =
+      SlhDsaPublicKey::Create(*params, public_key_bytes,
+                              test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  util::StatusOr<SlhDsaPublicKey> other_public_key =
+      SlhDsaPublicKey::Create(*params, public_key_bytes,
+                              test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(other_public_key, IsOk());
+
+  EXPECT_TRUE(*public_key == *other_public_key);
+  EXPECT_TRUE(*other_public_key == *public_key);
+  EXPECT_FALSE(*public_key != *other_public_key);
+  EXPECT_FALSE(*other_public_key != *public_key);
+}
+
+TEST(SlhDsaPublicKeyTest, DifferentPublicKeyBytesNotEqual) {
+  util::StatusOr<SlhDsaParameters> params =
+      SlhDsaParameters::Create(SlhDsaParameters::HashType::kSha2,
+                               /*private_key_size_in_bytes=*/64,
+                               SlhDsaParameters::SignatureType::kSmallSignature,
+                               SlhDsaParameters::Variant::kTink);
+
+  std::string public_key_bytes1 = subtle::Random::GetRandomBytes(32);
+  std::string public_key_bytes2 = subtle::Random::GetRandomBytes(32);
+
+  util::StatusOr<SlhDsaPublicKey> public_key = SlhDsaPublicKey::Create(
+      *params, public_key_bytes1, /*id_requirement=*/0x01020304,
+      GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  util::StatusOr<SlhDsaPublicKey> other_public_key = SlhDsaPublicKey::Create(
+      *params, public_key_bytes2, /*id_requirement=*/0x01020304,
+      GetPartialKeyAccess());
+  ASSERT_THAT(other_public_key, IsOk());
+
+  EXPECT_TRUE(*public_key != *other_public_key);
+  EXPECT_TRUE(*other_public_key != *public_key);
+  EXPECT_FALSE(*public_key == *other_public_key);
+  EXPECT_FALSE(*other_public_key == *public_key);
+}
+
+TEST(SlhDsaPublicKeyTest, DifferentIdRequirementNotEqual) {
+  util::StatusOr<SlhDsaParameters> params =
+      SlhDsaParameters::Create(SlhDsaParameters::HashType::kSha2,
+                               /*private_key_size_in_bytes=*/64,
+                               SlhDsaParameters::SignatureType::kSmallSignature,
+                               SlhDsaParameters::Variant::kTink);
+
+  std::string public_key_bytes = subtle::Random::GetRandomBytes(32);
+
+  util::StatusOr<SlhDsaPublicKey> public_key = SlhDsaPublicKey::Create(
+      *params, public_key_bytes, /*id_requirement=*/0x01020304,
+      GetPartialKeyAccess());
+  ASSERT_THAT(public_key, IsOk());
+
+  util::StatusOr<SlhDsaPublicKey> other_public_key = SlhDsaPublicKey::Create(
+      *params, public_key_bytes, /*id_requirement=*/0x02030405,
+      GetPartialKeyAccess());
+  ASSERT_THAT(other_public_key, IsOk());
+
+  EXPECT_TRUE(*public_key != *other_public_key);
+  EXPECT_TRUE(*other_public_key != *public_key);
+  EXPECT_FALSE(*public_key == *other_public_key);
+  EXPECT_FALSE(*other_public_key == *public_key);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/internal/BUILD.bazel b/cc/internal/BUILD.bazel
index edd7a44..c3c7bb3 100644
--- a/cc/internal/BUILD.bazel
+++ b/cc/internal/BUILD.bazel
@@ -1297,6 +1297,25 @@
 )
 
 cc_library(
+    name = "safe_stringops",
+    hdrs = ["safe_stringops.h"],
+    include_prefix = "tink/internal",
+    deps = [
+        ":call_with_core_dump_protection",
+        "@boringssl//:crypto",
+    ],
+)
+
+cc_test(
+    name = "safe_stringops_test",
+    srcs = ["safe_stringops_test.cc"],
+    deps = [
+        ":safe_stringops",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_library(
     name = "bn_encoding_util",
     srcs = ["bn_encoding_util.cc"],
     hdrs = ["bn_encoding_util.h"],
diff --git a/cc/internal/CMakeLists.txt b/cc/internal/CMakeLists.txt
index 6a7b393..4b807f9 100644
--- a/cc/internal/CMakeLists.txt
+++ b/cc/internal/CMakeLists.txt
@@ -1244,6 +1244,24 @@
 )
 
 tink_cc_library(
+  NAME safe_stringops
+  SRCS
+    safe_stringops.h
+  DEPS
+    crypto
+    tink::internal::call_with_core_dump_protection
+)
+
+tink_cc_test(
+  NAME safe_stringops_test
+  SRCS
+    safe_stringops_test.cc
+  DEPS
+    tink::internal::safe_stringops
+    gmock
+)
+
+tink_cc_library(
   NAME bn_encoding_util
   SRCS
     bn_encoding_util.cc
diff --git a/cc/internal/safe_stringops.h b/cc/internal/safe_stringops.h
new file mode 100644
index 0000000..6a341d5
--- /dev/null
+++ b/cc/internal/safe_stringops.h
@@ -0,0 +1,55 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_SAFE_STRINGOPS_H_
+#define TINK_INTERNAL_SAFE_STRINGOPS_H_
+
+#include <cstring>
+
+#include "tink/internal/call_with_core_dump_protection.h"
+#include "openssl/crypto.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+
+// Equivalents of regular memcpy/memmove, which do not leak contents of the
+// arguments in the core dump.
+
+inline void* SafeMemCopy(void* dst, const void* src, size_t n) {
+  return CallWithCoreDumpProtection(
+      [dst, src, n]() { return memcpy(dst, src, n); });
+}
+
+inline void* SafeMemMove(void* dst, const void* src, size_t n) {
+  return CallWithCoreDumpProtection(
+      [dst, src, n]() { return memmove(dst, src, n); });
+}
+
+// Test equality of two memory areas.
+// Not only protects from leaking any info about the contents in the core dump,
+// but also is safe for crypto material (const time).
+
+inline int SafeCryptoMemEquals(const void* s1, const void* s2, size_t n) {
+  return CallWithCoreDumpProtection(
+      [s1, s2, n]() { return CRYPTO_memcmp(s1, s2, n) == 0; });
+}
+
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_INTERNAL_SAFE_STRINGOPS_H_
diff --git a/cc/internal/safe_stringops_test.cc b/cc/internal/safe_stringops_test.cc
new file mode 100644
index 0000000..f23dc66
--- /dev/null
+++ b/cc/internal/safe_stringops_test.cc
@@ -0,0 +1,89 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/safe_stringops.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+namespace crypto {
+namespace tink {
+namespace internal {
+namespace {
+
+using ::testing::StrEq;
+
+TEST(MemCopyTest, Regular) {
+  char src[] = "hello";
+  char dst[] = "world";
+  EXPECT_THAT(dst, StrEq("world"));
+  static_assert(sizeof(src) == sizeof(dst), "size mismatch");
+  EXPECT_EQ(SafeMemCopy(dst, src, sizeof(src)), dst);
+  EXPECT_THAT(dst, StrEq("hello"));
+}
+
+TEST(MemMoveTest, Regular) {
+  char src[] = "hello";
+  char dst[] = "world";
+  EXPECT_THAT(dst, StrEq("world"));
+  static_assert(sizeof(src) == sizeof(dst), "size mismatch");
+  EXPECT_EQ(SafeMemMove(dst, src, sizeof(src)), dst);
+  EXPECT_THAT(dst, StrEq("hello"));
+}
+
+TEST(MemMoveTest, NoMove) {
+  char mem[] = "hello";
+  EXPECT_THAT(mem, StrEq("hello"));
+  EXPECT_EQ(SafeMemMove(mem, mem, sizeof(mem)), mem);
+  EXPECT_THAT(mem, StrEq("hello"));
+}
+
+TEST(MemmoveTest, OverlapSuffix) {
+  char mem[] = "hello";
+  EXPECT_THAT(mem, StrEq("hello"));
+  EXPECT_EQ(SafeMemMove(&mem[1], mem, sizeof(mem) - 2), &mem[1]);
+  EXPECT_THAT(mem, StrEq("hhell"));
+}
+
+TEST(MemMoveTest, OverlapPrefix) {
+  char mem[] = "hello";
+  EXPECT_THAT(mem, StrEq("hello"));
+  EXPECT_EQ(SafeMemMove(mem, &mem[1], sizeof(mem) - 2), mem);
+  EXPECT_THAT(mem, StrEq("elloo"));
+}
+
+TEST(MemEqualsTest, Equal) {
+  char a[] = "hello";
+  char b[] = "hello";
+  EXPECT_NE(a, b);
+  static_assert(sizeof(a) == sizeof(b), "size mismatch");
+  EXPECT_TRUE(SafeCryptoMemEquals(a, b, sizeof(a)));
+  EXPECT_TRUE(SafeCryptoMemEquals(b, a, sizeof(a)));
+}
+
+TEST(MemEqualsTest, Unequal) {
+  char a[] = "hello";
+  char b[] = "hellu";
+  EXPECT_NE(a, b);
+  static_assert(sizeof(a) == sizeof(b), "size mismatch");
+  EXPECT_FALSE(SafeCryptoMemEquals(a, b, sizeof(a)));
+  EXPECT_FALSE(SafeCryptoMemEquals(b, a, sizeof(a)));
+}
+
+}  // namespace
+}  // namespace internal
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/jwt/BUILD.bazel b/cc/jwt/BUILD.bazel
index ed25b79..8136429 100644
--- a/cc/jwt/BUILD.bazel
+++ b/cc/jwt/BUILD.bazel
@@ -210,6 +210,102 @@
     ],
 )
 
+cc_library(
+    name = "jwt_hmac_parameters",
+    srcs = ["jwt_hmac_parameters.cc"],
+    hdrs = ["jwt_hmac_parameters.h"],
+    include_prefix = "tink/jwt",
+    deps = [
+        ":jwt_mac_parameters",
+        "//:parameters",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+    ],
+)
+
+cc_library(
+    name = "jwt_hmac_key",
+    srcs = ["jwt_hmac_key.cc"],
+    hdrs = ["jwt_hmac_key.h"],
+    include_prefix = "tink/jwt",
+    deps = [
+        ":jwt_hmac_parameters",
+        ":jwt_mac_key",
+        "//:key",
+        "//:partial_key_access_token",
+        "//:restricted_data",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/base:endian",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/strings:string_view",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "jwt_hmac_proto_serialization",
+    srcs = ["jwt_hmac_proto_serialization.cc"],
+    hdrs = ["jwt_hmac_proto_serialization.h"],
+    include_prefix = "tink/jwt",
+    deps = [
+        ":jwt_hmac_key",
+        ":jwt_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:jwt_hmac_cc_proto",
+        "//proto:tink_cc_proto",
+        "//util:status",
+        "//util:statusor",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings:string_view",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "jwt_signature_parameters",
+    hdrs = ["jwt_signature_parameters.h"],
+    include_prefix = "tink/jwt",
+    deps = ["//:parameters"],
+)
+
+cc_library(
+    name = "jwt_signature_public_key",
+    hdrs = ["jwt_signature_public_key.h"],
+    include_prefix = "tink/jwt",
+    deps = [
+        ":jwt_signature_parameters",
+        "//:key",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
+cc_library(
+    name = "jwt_signature_private_key",
+    hdrs = ["jwt_signature_private_key.h"],
+    include_prefix = "tink/jwt",
+    deps = [
+        ":jwt_signature_parameters",
+        ":jwt_signature_public_key",
+        "//:key",
+        "//:private_key",
+        "@com_google_absl//absl/types:optional",
+    ],
+)
+
 # tests
 
 cc_test(
@@ -363,3 +459,60 @@
         "@com_google_googletest//:gtest_main",
     ],
 )
+
+cc_test(
+    name = "jwt_hmac_parameters_test",
+    srcs = ["jwt_hmac_parameters_test.cc"],
+    deps = [
+        ":jwt_hmac_parameters",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "jwt_hmac_key_test",
+    srcs = ["jwt_hmac_key_test.cc"],
+    deps = [
+        ":jwt_hmac_key",
+        ":jwt_hmac_parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "jwt_hmac_proto_serialization_test",
+    srcs = ["jwt_hmac_proto_serialization_test.cc"],
+    deps = [
+        ":jwt_hmac_key",
+        ":jwt_hmac_parameters",
+        ":jwt_hmac_proto_serialization",
+        "//:insecure_secret_key_access",
+        "//:key",
+        "//:parameters",
+        "//:partial_key_access",
+        "//:restricted_data",
+        "//internal:mutable_serialization_registry",
+        "//internal:proto_key_serialization",
+        "//internal:proto_parameters_serialization",
+        "//internal:serialization",
+        "//proto:common_cc_proto",
+        "//proto:jwt_hmac_cc_proto",
+        "//proto:tink_cc_proto",
+        "//subtle:random",
+        "//util:statusor",
+        "//util:test_matchers",
+        "@com_google_absl//absl/status",
+        "@com_google_absl//absl/strings:string_view",
+        "@com_google_absl//absl/types:optional",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/cc/jwt/CMakeLists.txt b/cc/jwt/CMakeLists.txt
index 8024aa6..637a66b 100644
--- a/cc/jwt/CMakeLists.txt
+++ b/cc/jwt/CMakeLists.txt
@@ -190,6 +190,98 @@
     tink::core::key
 )
 
+tink_cc_library(
+  NAME jwt_hmac_parameters
+  SRCS
+    jwt_hmac_parameters.cc
+    jwt_hmac_parameters.h
+  DEPS
+    tink::jwt::jwt_mac_parameters
+    absl::status
+    absl::strings
+    tink::core::parameters
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME jwt_hmac_key
+  SRCS
+    jwt_hmac_key.cc
+    jwt_hmac_key.h
+  DEPS
+    tink::jwt::jwt_hmac_parameters
+    tink::jwt::jwt_mac_key
+    absl::endian
+    absl::status
+    absl::strings
+    absl::string_view
+    absl::optional
+    tink::core::key
+    tink::core::partial_key_access_token
+    tink::core::restricted_data
+    tink::util::status
+    tink::util::statusor
+)
+
+tink_cc_library(
+  NAME jwt_hmac_proto_serialization
+  SRCS
+    jwt_hmac_proto_serialization.cc
+    jwt_hmac_proto_serialization.h
+  DEPS
+    tink::jwt::jwt_hmac_key
+    tink::jwt::jwt_hmac_parameters
+    absl::status
+    absl::string_view
+    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::jwt_hmac_cc_proto
+    tink::proto::tink_cc_proto
+)
+
+tink_cc_library(
+  NAME jwt_signature_parameters
+  SRCS
+    jwt_signature_parameters.h
+  DEPS
+    tink::core::parameters
+)
+
+tink_cc_library(
+  NAME jwt_signature_public_key
+  SRCS
+    jwt_signature_public_key.h
+  DEPS
+    tink::jwt::jwt_signature_parameters
+    absl::optional
+    tink::core::key
+)
+
+tink_cc_library(
+  NAME jwt_signature_private_key
+  SRCS
+    jwt_signature_private_key.h
+  DEPS
+    tink::jwt::jwt_signature_parameters
+    tink::jwt::jwt_signature_public_key
+    absl::optional
+    tink::core::key
+    tink::core::private_key
+)
+
 # tests
 
 tink_cc_test(
@@ -338,3 +430,60 @@
     tink::internal::fips_utils
     tink::util::test_matchers
 )
+
+tink_cc_test(
+  NAME jwt_hmac_parameters_test
+  SRCS
+    jwt_hmac_parameters_test.cc
+  DEPS
+    tink::jwt::jwt_hmac_parameters
+    gmock
+    absl::status
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME jwt_hmac_key_test
+  SRCS
+    jwt_hmac_key_test.cc
+  DEPS
+    tink::jwt::jwt_hmac_key
+    tink::jwt::jwt_hmac_parameters
+    gmock
+    absl::status
+    absl::optional
+    tink::core::partial_key_access
+    tink::core::restricted_data
+    tink::util::statusor
+    tink::util::test_matchers
+)
+
+tink_cc_test(
+  NAME jwt_hmac_proto_serialization_test
+  SRCS
+    jwt_hmac_proto_serialization_test.cc
+  DEPS
+    tink::jwt::jwt_hmac_key
+    tink::jwt::jwt_hmac_parameters
+    tink::jwt::jwt_hmac_proto_serialization
+    gmock
+    absl::status
+    absl::string_view
+    absl::optional
+    tink::core::insecure_secret_key_access
+    tink::core::key
+    tink::core::parameters
+    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::internal::serialization
+    tink::subtle::random
+    tink::util::statusor
+    tink::util::test_matchers
+    tink::proto::common_cc_proto
+    tink::proto::jwt_hmac_cc_proto
+    tink::proto::tink_cc_proto
+)
diff --git a/cc/jwt/jwt_hmac_key.cc b/cc/jwt/jwt_hmac_key.cc
new file mode 100644
index 0000000..cd191a5
--- /dev/null
+++ b/cc/jwt/jwt_hmac_key.cc
@@ -0,0 +1,151 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/jwt/jwt_hmac_key.h"
+
+#include <string>
+
+#include "absl/base/internal/endian.h"
+#include "absl/status/status.h"
+#include "absl/strings/escaping.h"
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/jwt/jwt_hmac_parameters.h"
+#include "tink/key.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+JwtHmacKey::Builder& JwtHmacKey::Builder::SetParameters(
+    const JwtHmacParameters& parameters) {
+  parameters_ = parameters;
+  return *this;
+}
+
+JwtHmacKey::Builder& JwtHmacKey::Builder::SetKeyBytes(
+    const RestrictedData& key_bytes) {
+  key_bytes_ = key_bytes;
+  return *this;
+}
+
+JwtHmacKey::Builder& JwtHmacKey::Builder::SetIdRequirement(int id_requirement) {
+  id_requirement_ = id_requirement;
+  return *this;
+}
+
+JwtHmacKey::Builder& JwtHmacKey::Builder::SetCustomKid(
+    absl::string_view custom_kid) {
+  custom_kid_ = custom_kid.data();
+  return *this;
+}
+
+util::StatusOr<absl::optional<std::string>> JwtHmacKey::Builder::ComputeKid() {
+  switch (parameters_->GetKidStrategy()) {
+    case JwtHmacParameters::KidStrategy::kBase64EncodedKeyId: {
+      if (custom_kid_.has_value()) {
+        return util::Status(
+            absl::StatusCode::kInvalidArgument,
+            "Custom kid must not be set for KidStrategy::kBase64EncodedKeyId.");
+      }
+      std::string base64_kid;
+      char buffer[4];
+      absl::big_endian::Store32(buffer, *id_requirement_);
+      absl::WebSafeBase64Escape(absl::string_view(buffer, 4), &base64_kid);
+      return base64_kid;
+    }
+    case JwtHmacParameters::KidStrategy::kCustom: {
+      if (!custom_kid_.has_value()) {
+        return util::Status(absl::StatusCode::kInvalidArgument,
+                            "Custom kid must be set for KidStrategy::kCustom.");
+      }
+      return custom_kid_;
+    }
+    case JwtHmacParameters::KidStrategy::kIgnored: {
+      if (custom_kid_.has_value()) {
+        return util::Status(
+            absl::StatusCode::kInvalidArgument,
+            "Custom kid must not be set for KidStrategy::kIgnored.");
+      }
+      return absl::nullopt;
+    }
+    default:
+      // Should be unreachable if all valid kid strategies have been handled.
+      return util::Status(absl::StatusCode::kFailedPrecondition,
+                          "Unknown kid strategy.");
+  }
+}
+
+util::StatusOr<JwtHmacKey> JwtHmacKey::Builder::Build(
+    PartialKeyAccessToken token) {
+  if (!parameters_.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "JWT HMAC parameters must be specified.");
+  }
+  if (!key_bytes_.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "JWT HMAC key bytes must be specified.");
+  }
+  if (parameters_->KeySizeInBytes() != key_bytes_->size()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Actual JWT HMAC key size does not match size specified in "
+        "the 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<absl::optional<std::string>> kid = ComputeKid();
+  if (!kid.ok()) {
+    return kid.status();
+  }
+  return JwtHmacKey(*parameters_, *key_bytes_, id_requirement_, *kid);
+}
+
+bool JwtHmacKey::operator==(const Key& other) const {
+  const JwtHmacKey* that = dynamic_cast<const JwtHmacKey*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (parameters_ != that->parameters_) {
+    return false;
+  }
+  if (key_bytes_ != that->key_bytes_) {
+    return false;
+  }
+  if (id_requirement_ != that->id_requirement_) {
+    return false;
+  }
+  if (kid_ != that->kid_) {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/jwt/jwt_hmac_key.h b/cc/jwt/jwt_hmac_key.h
new file mode 100644
index 0000000..7ab3360
--- /dev/null
+++ b/cc/jwt/jwt_hmac_key.h
@@ -0,0 +1,107 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_JWT_JWT_HMAC_KEY_H_
+#define TINK_JWT_JWT_HMAC_KEY_H_
+
+#include <string>
+
+#include "absl/strings/string_view.h"
+#include "absl/types/optional.h"
+#include "tink/jwt/jwt_hmac_parameters.h"
+#include "tink/jwt/jwt_mac_key.h"
+#include "tink/key.h"
+#include "tink/partial_key_access_token.h"
+#include "tink/restricted_data.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Represents functions to authenticate and verify JWTs using HMAC.
+class JwtHmacKey : public JwtMacKey {
+ public:
+  // Creates JWT HMAC key 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& SetParameters(const JwtHmacParameters& parameters);
+    Builder& SetKeyBytes(const RestrictedData& key_bytes);
+    Builder& SetIdRequirement(int id_requirement);
+    Builder& SetCustomKid(absl::string_view custom_kid);
+
+    // Creates JWT HMAC key object from this builder.
+    util::StatusOr<JwtHmacKey> Build(PartialKeyAccessToken token);
+
+   private:
+    util::StatusOr<absl::optional<std::string>> ComputeKid();
+
+    absl::optional<JwtHmacParameters> parameters_;
+    absl::optional<RestrictedData> key_bytes_;
+    absl::optional<int> id_requirement_;
+    absl::optional<std::string> custom_kid_;
+  };
+
+  // Copyable and movable.
+  JwtHmacKey(const JwtHmacKey& other) = default;
+  JwtHmacKey& operator=(const JwtHmacKey& other) = default;
+  JwtHmacKey(JwtHmacKey&& other) = default;
+  JwtHmacKey& operator=(JwtHmacKey&& other) = default;
+
+  const RestrictedData& GetKeyBytes(PartialKeyAccessToken token) const {
+    return key_bytes_;
+  }
+
+  const JwtHmacParameters& GetParameters() const override {
+    return parameters_;
+  }
+
+  absl::optional<int> GetIdRequirement() const override {
+    return id_requirement_;
+  }
+
+  absl::optional<std::string> GetKid() const override { return kid_; }
+
+  bool operator==(const Key& other) const override;
+
+ private:
+  JwtHmacKey(const JwtHmacParameters& parameters,
+             const RestrictedData& key_bytes,
+             absl::optional<int> id_requirement,
+             absl::optional<std::string> kid)
+      : parameters_(parameters),
+        key_bytes_(key_bytes),
+        id_requirement_(id_requirement),
+        kid_(kid) {}
+
+  JwtHmacParameters parameters_;
+  RestrictedData key_bytes_;
+  absl::optional<int> id_requirement_;
+  absl::optional<std::string> kid_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_JWT_JWT_HMAC_KEY_H_
diff --git a/cc/jwt/jwt_hmac_key_test.cc b/cc/jwt/jwt_hmac_key_test.cc
new file mode 100644
index 0000000..e64035c
--- /dev/null
+++ b/cc/jwt/jwt_hmac_key_test.cc
@@ -0,0 +1,418 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/jwt/jwt_hmac_key.h"
+
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.h"
+#include "absl/types/optional.h"
+#include "tink/jwt/jwt_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::StatusIs;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct TestCase {
+  int key_size_in_bytes;
+  JwtHmacParameters::KidStrategy kid_strategy;
+  JwtHmacParameters::Algorithm algorithm;
+  absl::optional<std::string> custom_kid;
+  absl::optional<int> id_requirement;
+  absl::optional<std::string> expected_kid;
+};
+
+using JwtHmacKeyTest = TestWithParam<TestCase>;
+
+INSTANTIATE_TEST_SUITE_P(
+    JwtHmacKeyTestSuite, JwtHmacKeyTest,
+    Values(TestCase{/*key_size_in_bytes=*/16,
+                    JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+                    JwtHmacParameters::Algorithm::kHs256,
+                    /*custom_kid=*/absl::nullopt, /*id_requirement=*/123,
+                    /*expected_kid=*/"AAAAew"},
+           TestCase{/*key_size_in_bytes=*/32,
+                    JwtHmacParameters::KidStrategy::kCustom,
+                    JwtHmacParameters::Algorithm::kHs384,
+                    /*custom_kid=*/"custom_kid",
+                    /*id_requirement=*/absl::nullopt,
+                    /*expected_kid=*/"custom_kid"},
+           TestCase{/*key_size_in_bytes=*/32,
+                    JwtHmacParameters::KidStrategy::kIgnored,
+                    JwtHmacParameters::Algorithm::kHs512,
+                    /*custom_kid=*/absl::nullopt,
+                    /*id_requirement=*/absl::nullopt,
+                    /*expected_kid=*/absl::nullopt}));
+
+TEST_P(JwtHmacKeyTest, CreateSucceeds) {
+  TestCase test_case = GetParam();
+
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      test_case.key_size_in_bytes, test_case.kid_strategy, test_case.algorithm);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(test_case.key_size_in_bytes);
+  JwtHmacKey::Builder builder =
+      JwtHmacKey::Builder().SetParameters(*params).SetKeyBytes(secret);
+  if (test_case.id_requirement.has_value()) {
+    builder.SetIdRequirement(*test_case.id_requirement);
+  }
+  if (test_case.custom_kid.has_value()) {
+    builder.SetCustomKid(*test_case.custom_kid);
+  }
+  util::StatusOr<JwtHmacKey> key = builder.Build(GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  EXPECT_THAT(key->GetParameters(), Eq(*params));
+  EXPECT_THAT(key->GetKeyBytes(GetPartialKeyAccess()), Eq(secret));
+  EXPECT_THAT(key->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT(key->GetKid(), Eq(test_case.expected_kid));
+}
+
+TEST(JwtHmacKeyTest, CreateKeyWithMismatchedKeySizeFails) {
+  // Key size parameter is 32 bytes.
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  // Key material is 16 bytes (another valid key length).
+  RestrictedData mismatched_secret = RestrictedData(/*num_random_bytes=*/16);
+  JwtHmacKey::Builder builder = JwtHmacKey::Builder()
+                                    .SetParameters(*params)
+                                    .SetKeyBytes(mismatched_secret)
+                                    .SetIdRequirement(123);
+
+  EXPECT_THAT(builder.Build(GetPartialKeyAccess()).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("Actual JWT HMAC key size does not match")));
+}
+
+TEST(JwtHmacKeyTest, CreateKeyWithoutKeyBytesFails) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  JwtHmacKey::Builder builder =
+      JwtHmacKey::Builder().SetParameters(*params).SetIdRequirement(123);
+
+  EXPECT_THAT(builder.Build(GetPartialKeyAccess()).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("JWT HMAC key bytes must be specified")));
+}
+
+TEST(JwtHmacKeyTest, CreateKeyWithoutParametersFails) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+  JwtHmacKey::Builder builder =
+      JwtHmacKey::Builder().SetKeyBytes(secret).SetIdRequirement(123);
+
+  EXPECT_THAT(builder.Build(GetPartialKeyAccess()).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("JWT HMAC parameters must be specified")));
+}
+
+TEST(JwtHmacKeyTest, CreateBase64EncodedKidWithoutIdRequirementFails) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+  JwtHmacKey::Builder builder =
+      JwtHmacKey::Builder().SetParameters(*params).SetKeyBytes(secret);
+
+  EXPECT_THAT(builder.Build(GetPartialKeyAccess()).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("Cannot create key without ID requirement "
+                                 "with parameters with ID requirement")));
+}
+
+TEST(JwtHmacKeyTest, CreateBase64EncodedKidWithCustomKidFails) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+  JwtHmacKey::Builder builder = JwtHmacKey::Builder()
+                                    .SetParameters(*params)
+                                    .SetKeyBytes(secret)
+                                    .SetIdRequirement(123)
+                                    .SetCustomKid("custom_kid");
+
+  EXPECT_THAT(builder.Build(GetPartialKeyAccess()).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("Custom kid must not be set for "
+                                 "KidStrategy::kBase64EncodedKeyId")));
+}
+
+TEST(JwtHmacKeyTest, CreateCustomKidWithIdRequirementFails) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32, JwtHmacParameters::KidStrategy::kCustom,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+  JwtHmacKey::Builder builder = JwtHmacKey::Builder()
+                                    .SetParameters(*params)
+                                    .SetKeyBytes(secret)
+                                    .SetCustomKid("custom_kid")
+                                    .SetIdRequirement(123);
+
+  EXPECT_THAT(builder.Build(GetPartialKeyAccess()).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("Cannot create key with ID requirement with "
+                                 "parameters without ID requirement")));
+}
+
+TEST(JwtHmacKeyTest, CreateCustomKidWithoutCustomKidFails) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32, JwtHmacParameters::KidStrategy::kCustom,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+  JwtHmacKey::Builder builder =
+      JwtHmacKey::Builder().SetParameters(*params).SetKeyBytes(secret);
+
+  EXPECT_THAT(builder.Build(GetPartialKeyAccess()).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("Custom kid must be set")));
+}
+
+TEST(JwtHmacKeyTest, CreateIgnoredKidWithIdRequirementFails) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32, JwtHmacParameters::KidStrategy::kIgnored,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+  JwtHmacKey::Builder builder = JwtHmacKey::Builder()
+                                    .SetParameters(*params)
+                                    .SetKeyBytes(secret)
+                                    .SetIdRequirement(123);
+
+  EXPECT_THAT(builder.Build(GetPartialKeyAccess()).status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("Cannot create key with ID requirement with "
+                                 "parameters without ID requirement")));
+}
+
+TEST(JwtHmacKeyTest, CreateIgnoredKidWithCustomKidFails) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32, JwtHmacParameters::KidStrategy::kIgnored,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+  JwtHmacKey::Builder builder = JwtHmacKey::Builder()
+                                    .SetParameters(*params)
+                                    .SetKeyBytes(secret)
+                                    .SetCustomKid("custom_kid");
+
+  EXPECT_THAT(
+      builder.Build(GetPartialKeyAccess()).status(),
+      StatusIs(
+          absl::StatusCode::kInvalidArgument,
+          HasSubstr("Custom kid must not be set for KidStrategy::kIgnored")));
+}
+
+TEST_P(JwtHmacKeyTest, KeyEquals) {
+  TestCase test_case = GetParam();
+
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      test_case.key_size_in_bytes, test_case.kid_strategy, test_case.algorithm);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(test_case.key_size_in_bytes);
+  JwtHmacKey::Builder builder =
+      JwtHmacKey::Builder().SetParameters(*params).SetKeyBytes(secret);
+  if (test_case.id_requirement.has_value()) {
+    builder.SetIdRequirement(*test_case.id_requirement);
+  }
+  if (test_case.custom_kid.has_value()) {
+    builder.SetCustomKid(*test_case.custom_kid);
+  }
+  util::StatusOr<JwtHmacKey> key = builder.Build(GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  JwtHmacKey::Builder other_builder =
+      JwtHmacKey::Builder().SetParameters(*params).SetKeyBytes(secret);
+  if (test_case.id_requirement.has_value()) {
+    other_builder.SetIdRequirement(*test_case.id_requirement);
+  }
+  if (test_case.custom_kid.has_value()) {
+    other_builder.SetCustomKid(*test_case.custom_kid);
+  }
+  util::StatusOr<JwtHmacKey> other_key =
+      other_builder.Build(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(JwtHmacKeyTest, DifferentParametersNotEqual) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  util::StatusOr<JwtHmacParameters> other_params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs384);
+  ASSERT_THAT(other_params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<JwtHmacKey> key = JwtHmacKey::Builder()
+                                       .SetParameters(*params)
+                                       .SetKeyBytes(secret)
+                                       .SetIdRequirement(123)
+                                       .Build(GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<JwtHmacKey> other_key = JwtHmacKey::Builder()
+                                             .SetParameters(*other_params)
+                                             .SetKeyBytes(secret)
+                                             .SetIdRequirement(123)
+                                             .Build(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(JwtHmacKeyTest, DifferentSecretDataNotEqual) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+  RestrictedData other_secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<JwtHmacKey> key = JwtHmacKey::Builder()
+                                       .SetParameters(*params)
+                                       .SetKeyBytes(secret)
+                                       .SetIdRequirement(123)
+                                       .Build(GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<JwtHmacKey> other_key = JwtHmacKey::Builder()
+                                             .SetParameters(*params)
+                                             .SetKeyBytes(other_secret)
+                                             .SetIdRequirement(123)
+                                             .Build(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(JwtHmacKeyTest, DifferentIdRequirementNotEqual) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<JwtHmacKey> key = JwtHmacKey::Builder()
+                                       .SetParameters(*params)
+                                       .SetKeyBytes(secret)
+                                       .SetIdRequirement(123)
+                                       .Build(GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<JwtHmacKey> other_key = JwtHmacKey::Builder()
+                                             .SetParameters(*params)
+                                             .SetKeyBytes(secret)
+                                             .SetIdRequirement(456)
+                                             .Build(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(JwtHmacKeyTest, DifferentCustomKidNotEqual) {
+  util::StatusOr<JwtHmacParameters> params = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32, JwtHmacParameters::KidStrategy::kCustom,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(params, IsOk());
+
+  RestrictedData secret = RestrictedData(/*num_random_bytes=*/32);
+
+  util::StatusOr<JwtHmacKey> key = JwtHmacKey::Builder()
+                                       .SetParameters(*params)
+                                       .SetKeyBytes(secret)
+                                       .SetCustomKid("custom_kid")
+                                       .Build(GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<JwtHmacKey> other_key = JwtHmacKey::Builder()
+                                             .SetParameters(*params)
+                                             .SetKeyBytes(secret)
+                                             .SetCustomKid("other_custom_kid")
+                                             .Build(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/jwt/jwt_hmac_parameters.cc b/cc/jwt/jwt_hmac_parameters.cc
new file mode 100644
index 0000000..ae7a332
--- /dev/null
+++ b/cc/jwt/jwt_hmac_parameters.cc
@@ -0,0 +1,77 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/jwt/jwt_hmac_parameters.h"
+
+#include <set>
+
+#include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
+#include "tink/parameters.h"
+#include "tink/util/status.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+util::StatusOr<JwtHmacParameters> JwtHmacParameters::Create(
+    int key_size_in_bytes, KidStrategy kid_strategy, Algorithm algorithm) {
+  if (key_size_in_bytes < 16) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        absl::StrCat("Key size should be at least 16 bytes, got ",
+                     key_size_in_bytes, " bytes."));
+  }
+  static const std::set<KidStrategy>* kSupportedKidStrategies =
+      new std::set<KidStrategy>({KidStrategy::kBase64EncodedKeyId,
+                                 KidStrategy::kIgnored, KidStrategy::kCustom});
+  if (kSupportedKidStrategies->find(kid_strategy) ==
+      kSupportedKidStrategies->end()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create JWT HMAC parameters with unknown kid strategy.");
+  }
+  static const std::set<Algorithm>* kSupportedAlgorithms =
+      new std::set<Algorithm>(
+          {Algorithm::kHs256, Algorithm::kHs384, Algorithm::kHs512});
+  if (kSupportedAlgorithms->find(algorithm) == kSupportedAlgorithms->end()) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Cannot create JWT HMAC parameters with unknown algorithm.");
+  }
+  return JwtHmacParameters(key_size_in_bytes, kid_strategy, algorithm);
+}
+
+bool JwtHmacParameters::operator==(const Parameters& other) const {
+  const JwtHmacParameters* that =
+      dynamic_cast<const JwtHmacParameters*>(&other);
+  if (that == nullptr) {
+    return false;
+  }
+  if (key_size_in_bytes_ != that->key_size_in_bytes_) {
+    return false;
+  }
+  if (kid_strategy_ != that->kid_strategy_) {
+    return false;
+  }
+  if (algorithm_ != that->algorithm_) {
+    return false;
+  }
+  return true;
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/jwt/jwt_hmac_parameters.h b/cc/jwt/jwt_hmac_parameters.h
new file mode 100644
index 0000000..4755bd4
--- /dev/null
+++ b/cc/jwt/jwt_hmac_parameters.h
@@ -0,0 +1,115 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_JWT_JWT_HMAC_PARAMETERS_H_
+#define TINK_JWT_JWT_HMAC_PARAMETERS_H_
+
+#include "tink/jwt/jwt_mac_parameters.h"
+#include "tink/parameters.h"
+#include "tink/util/statusor.h"
+
+namespace crypto {
+namespace tink {
+
+// Describes the parameters of an `JwtHmacKey`.
+class JwtHmacParameters : public JwtMacParameters {
+ public:
+  // Strategy for handling the "kid" header.
+  enum class KidStrategy : int {
+    // The `kid` is the URL safe (RFC 4648 Section 5) base64-encoded big-endian
+    // `key_id` in the keyset.
+    //
+    // In `ComputeMacAndEncode()`, Tink always adds the `kid`.
+    //
+    // In `VerifyMacAndDecode()`, Tink checks that the `kid` is present and
+    // equal to this value.
+    //
+    // NOTE: This strategy is recommended by Tink.
+    kBase64EncodedKeyId = 1,
+    // The `kid` header is ignored.
+    //
+    // In `ComputeMacAndEncode()`, Tink does not write a `kid` header.
+    //
+    // In `VerifyMacAndDecode()`, Tink ignores the `kid` header.
+    kIgnored = 2,
+    // The `kid` is fixed. It can be obtained by calling `key.GetKid()`.
+    //
+    // In `ComputeMacAndEncode()`, Tink writes the `kid` header to the
+    // value given by `key.getCustomKid()`.
+    //
+    // In `VerifyMacAndDecode()`, if the `kid` is present, it must match
+    // `key.GetKid()`. If the `kid` is absent, it will be accepted.
+    //
+    // NOTE: Tink does not allow random generation of `JwtHmacKey` objects from
+    // parameters objects with `KidStrategy::kCustom`.
+    kCustom = 3,
+    kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+  };
+
+  // MAC computation algorithm.
+  enum class Algorithm : int {
+    kHs256 = 1,
+    kHs384 = 2,
+    kHs512 = 3,
+    kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements = 20,
+  };
+
+  // Copyable and movable.
+  JwtHmacParameters(const JwtHmacParameters& other) = default;
+  JwtHmacParameters& operator=(const JwtHmacParameters& other) = default;
+  JwtHmacParameters(JwtHmacParameters&& other) = default;
+  JwtHmacParameters& operator=(JwtHmacParameters&& other) = default;
+
+  // Creates JWT HMAC parameters object. Returns an error status if
+  // `key_size_in_bytes` is less than 16 bytes, if `kid_strategy` is invalid, or
+  // if `algorithm` is invalid.
+  static util::StatusOr<JwtHmacParameters> Create(int key_size_in_bytes,
+                                                  KidStrategy kid_strategy,
+                                                  Algorithm algorithm);
+
+  int KeySizeInBytes() const { return key_size_in_bytes_; }
+
+  KidStrategy GetKidStrategy() const { return kid_strategy_; }
+
+  Algorithm GetAlgorithm() const { return algorithm_; }
+
+  bool AllowKidAbsent() const override {
+    return kid_strategy_ == KidStrategy::kCustom ||
+           kid_strategy_ == KidStrategy::kIgnored;
+  }
+
+  bool HasIdRequirement() const override {
+    return kid_strategy_ == KidStrategy::kBase64EncodedKeyId;
+  }
+
+  bool operator==(const Parameters& other) const override;
+
+ private:
+  JwtHmacParameters(int key_size_in_bytes, KidStrategy kid_strategy,
+                    Algorithm algorithm)
+      : key_size_in_bytes_(key_size_in_bytes),
+        kid_strategy_(kid_strategy),
+        algorithm_(algorithm) {}
+
+  int key_size_in_bytes_;
+  KidStrategy kid_strategy_;
+  Algorithm algorithm_;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_JWT_JWT_HMAC_PARAMETERS_H_
diff --git a/cc/jwt/jwt_hmac_parameters_test.cc b/cc/jwt/jwt_hmac_parameters_test.cc
new file mode 100644
index 0000000..0b25889
--- /dev/null
+++ b/cc/jwt/jwt_hmac_parameters_test.cc
@@ -0,0 +1,222 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/jwt/jwt_hmac_parameters.h"
+
+#include <tuple>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "absl/status/status.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::HasSubstr;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+struct KidStrategyTuple {
+  JwtHmacParameters::KidStrategy kid_strategy;
+  bool allowed_kid_absent;
+  bool has_id_requirement;
+};
+
+using JwtHmacParametersTest = TestWithParam<
+    std::tuple<int, KidStrategyTuple, JwtHmacParameters::Algorithm>>;
+
+INSTANTIATE_TEST_SUITE_P(
+    JwtHmacParametersTestSuite, JwtHmacParametersTest,
+    Combine(Values(16, 32),
+            Values(
+                KidStrategyTuple{
+                    JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+                    /*allowed_kid_absent=*/false, /*has_id_requirement=*/true},
+                KidStrategyTuple{JwtHmacParameters::KidStrategy::kCustom,
+                                 /*allowed_kid_absent=*/true,
+                                 /*has_id_requirement=*/false},
+                KidStrategyTuple{JwtHmacParameters::KidStrategy::kIgnored,
+                                 /*allowed_kid_absent=*/true,
+                                 /*has_id_requirement=*/false}),
+            Values(JwtHmacParameters::Algorithm::kHs256,
+                   JwtHmacParameters::Algorithm::kHs384,
+                   JwtHmacParameters::Algorithm::kHs512)));
+
+TEST_P(JwtHmacParametersTest, Create) {
+  int key_size_in_bytes;
+  KidStrategyTuple tuple;
+  JwtHmacParameters::Algorithm algorithm;
+  std::tie(key_size_in_bytes, tuple, algorithm) = GetParam();
+
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      key_size_in_bytes, tuple.kid_strategy, algorithm);
+  ASSERT_THAT(parameters, IsOk());
+
+  EXPECT_THAT(parameters->KeySizeInBytes(), Eq(key_size_in_bytes));
+  EXPECT_THAT(parameters->GetKidStrategy(), Eq(tuple.kid_strategy));
+  EXPECT_THAT(parameters->AllowKidAbsent(), Eq(tuple.allowed_kid_absent));
+  EXPECT_THAT(parameters->HasIdRequirement(), Eq(tuple.has_id_requirement));
+  EXPECT_THAT(parameters->GetAlgorithm(), Eq(algorithm));
+}
+
+TEST(JwtHmacParametersTest, CreateWithInvalidKidStrategyFails) {
+  EXPECT_THAT(JwtHmacParameters::Create(
+                  /*key_size_in_bytes=*/16,
+                  JwtHmacParameters::KidStrategy::
+                      kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements,
+                  JwtHmacParameters::Algorithm::kHs512)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("unknown kid strategy")));
+}
+
+TEST(JwtHmacParametersTest, CreateWithInvalidAlgorithmFails) {
+  EXPECT_THAT(JwtHmacParameters::Create(
+                  /*key_size_in_bytes=*/16,
+                  JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+                  JwtHmacParameters::Algorithm::
+                      kDoNotUseInsteadUseDefaultWhenWritingSwitchStatements)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("unknown algorithm")));
+}
+
+TEST(JwtHmacParametersTest, CreateWithInvalidKeySizeFails) {
+  EXPECT_THAT(JwtHmacParameters::Create(
+                  /*key_size_in_bytes=*/15,
+                  JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+                  JwtHmacParameters::Algorithm::kHs512)
+                  .status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("Key size should be at least 16 bytes")));
+}
+
+TEST(JwtHmacParametersTest, CopyConstructor) {
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/16,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs512);
+  ASSERT_THAT(parameters, IsOk());
+
+  JwtHmacParameters copy(*parameters);
+
+  EXPECT_THAT(copy.KeySizeInBytes(), Eq(parameters->KeySizeInBytes()));
+  EXPECT_THAT(copy.GetKidStrategy(), Eq(parameters->GetKidStrategy()));
+  EXPECT_THAT(copy.GetAlgorithm(), Eq(parameters->GetAlgorithm()));
+  EXPECT_THAT(copy.AllowKidAbsent(), Eq(parameters->AllowKidAbsent()));
+  EXPECT_THAT(copy.HasIdRequirement(), Eq(parameters->HasIdRequirement()));
+}
+
+TEST(JwtHmacParametersTest, CopyAssignment) {
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/16,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs512);
+  ASSERT_THAT(parameters, IsOk());
+
+  JwtHmacParameters copy = *parameters;
+
+  EXPECT_THAT(copy.KeySizeInBytes(), Eq(parameters->KeySizeInBytes()));
+  EXPECT_THAT(copy.GetKidStrategy(), Eq(parameters->GetKidStrategy()));
+  EXPECT_THAT(copy.GetAlgorithm(), Eq(parameters->GetAlgorithm()));
+  EXPECT_THAT(copy.AllowKidAbsent(), Eq(parameters->AllowKidAbsent()));
+  EXPECT_THAT(copy.HasIdRequirement(), Eq(parameters->HasIdRequirement()));
+}
+
+TEST_P(JwtHmacParametersTest, ParametersEquals) {
+  int key_size_in_bytes;
+  KidStrategyTuple tuple;
+  JwtHmacParameters::Algorithm algorithm;
+  std::tie(key_size_in_bytes, tuple, algorithm) = GetParam();
+
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      key_size_in_bytes, tuple.kid_strategy, algorithm);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<JwtHmacParameters> other_parameters =
+      JwtHmacParameters::Create(key_size_in_bytes, tuple.kid_strategy,
+                                algorithm);
+  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(JwtHmacParametersTest, KeySizeNotEqual) {
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/16,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<JwtHmacParameters> other_parameters =
+      JwtHmacParameters::Create(
+          /*key_size_in_bytes=*/32,
+          JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+          JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(JwtHmacParametersTest, KidStrategyNotEqual) {
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/16,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<JwtHmacParameters> other_parameters =
+      JwtHmacParameters::Create(
+          /*key_size_in_bytes=*/16, JwtHmacParameters::KidStrategy::kCustom,
+          JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+TEST(JwtHmacParametersTest, AlgorithmNotEqual) {
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/16,
+      JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(parameters, IsOk());
+
+  util::StatusOr<JwtHmacParameters> other_parameters =
+      JwtHmacParameters::Create(
+          /*key_size_in_bytes=*/16,
+          JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+          JwtHmacParameters::Algorithm::kHs384);
+  ASSERT_THAT(other_parameters, IsOk());
+
+  EXPECT_TRUE(*parameters != *other_parameters);
+  EXPECT_FALSE(*parameters == *other_parameters);
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/jwt/jwt_hmac_proto_serialization.cc b/cc/jwt/jwt_hmac_proto_serialization.cc
new file mode 100644
index 0000000..53fd660
--- /dev/null
+++ b/cc/jwt/jwt_hmac_proto_serialization.cc
@@ -0,0 +1,326 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/jwt/jwt_hmac_proto_serialization.h"
+
+#include <string>
+
+#include "absl/status/status.h"
+#include "absl/strings/string_view.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/jwt/jwt_hmac_key.h"
+#include "tink/jwt/jwt_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/jwt_hmac.pb.h"
+#include "proto/tink.pb.h"
+
+namespace crypto {
+namespace tink {
+namespace {
+
+using ::google::crypto::tink::JwtHmacAlgorithm;
+using ::google::crypto::tink::JwtHmacKeyFormat;
+using ::google::crypto::tink::OutputPrefixType;
+
+using JwtHmacProtoParametersParserImpl =
+    internal::ParametersParserImpl<internal::ProtoParametersSerialization,
+                                   JwtHmacParameters>;
+using JwtHmacProtoParametersSerializerImpl =
+    internal::ParametersSerializerImpl<JwtHmacParameters,
+                                       internal::ProtoParametersSerialization>;
+using JwtHmacProtoKeyParserImpl =
+    internal::KeyParserImpl<internal::ProtoKeySerialization, JwtHmacKey>;
+using JwtHmacProtoKeySerializerImpl =
+    internal::KeySerializerImpl<JwtHmacKey, internal::ProtoKeySerialization>;
+
+const absl::string_view kTypeUrl =
+    "type.googleapis.com/google.crypto.tink.JwtHmacKey";
+
+util::StatusOr<JwtHmacParameters::KidStrategy> ToKidStrategy(
+    OutputPrefixType output_prefix_type, bool has_custom_kid) {
+  switch (output_prefix_type) {
+    case OutputPrefixType::RAW:
+      if (has_custom_kid) {
+        return JwtHmacParameters::KidStrategy::kCustom;
+      }
+      return JwtHmacParameters::KidStrategy::kIgnored;
+    case OutputPrefixType::TINK:
+      return JwtHmacParameters::KidStrategy::kBase64EncodedKeyId;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Invalid OutputPrefixType for JwtHmacKeyFormat.");
+  }
+}
+
+util::StatusOr<OutputPrefixType> ToOutputPrefixType(
+    JwtHmacParameters::KidStrategy kid_strategy) {
+  switch (kid_strategy) {
+    case JwtHmacParameters::KidStrategy::kCustom:
+      return OutputPrefixType::RAW;
+    case JwtHmacParameters::KidStrategy::kIgnored:
+      return OutputPrefixType::RAW;
+    case JwtHmacParameters::KidStrategy::kBase64EncodedKeyId:
+      return OutputPrefixType::TINK;
+    default:
+      return util::Status(
+          absl::StatusCode::kInvalidArgument,
+          "Could not determine JwtHmacParameters::KidStrategy.");
+  }
+}
+
+util::StatusOr<JwtHmacParameters::Algorithm> FromProtoAlgorithm(
+    JwtHmacAlgorithm algorithm) {
+  switch (algorithm) {
+    case JwtHmacAlgorithm::HS256:
+      return JwtHmacParameters::Algorithm::kHs256;
+    case JwtHmacAlgorithm::HS384:
+      return JwtHmacParameters::Algorithm::kHs384;
+    case JwtHmacAlgorithm::HS512:
+      return JwtHmacParameters::Algorithm::kHs512;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine JwtHmacAlgorithm.");
+  }
+}
+
+util::StatusOr<JwtHmacAlgorithm> ToProtoAlgorithm(
+    JwtHmacParameters::Algorithm algorithm) {
+  switch (algorithm) {
+    case JwtHmacParameters::Algorithm::kHs256:
+      return JwtHmacAlgorithm::HS256;
+    case JwtHmacParameters::Algorithm::kHs384:
+      return JwtHmacAlgorithm::HS384;
+    case JwtHmacParameters::Algorithm::kHs512:
+      return JwtHmacAlgorithm::HS512;
+    default:
+      return util::Status(absl::StatusCode::kInvalidArgument,
+                          "Could not determine JwtHmacParameters::Algorithm");
+  }
+}
+
+util::StatusOr<JwtHmacParameters> ToParameters(
+    int key_size_in_bytes, OutputPrefixType output_prefix_type,
+    JwtHmacAlgorithm proto_algorithm, bool has_custom_kid) {
+  util::StatusOr<JwtHmacParameters::KidStrategy> kid_strategy =
+      ToKidStrategy(output_prefix_type, has_custom_kid);
+  if (!kid_strategy.ok()) {
+    return kid_strategy.status();
+  }
+  util::StatusOr<JwtHmacParameters::Algorithm> algorithm =
+      FromProtoAlgorithm(proto_algorithm);
+  if (!algorithm.ok()) {
+    return algorithm.status();
+  }
+  return JwtHmacParameters::Create(key_size_in_bytes, *kid_strategy,
+                                   *algorithm);
+}
+
+util::StatusOr<JwtHmacParameters> ParseParameters(
+    const internal::ProtoParametersSerialization& serialization) {
+  if (serialization.GetKeyTemplate().type_url() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing JwtHmacParameters.");
+  }
+  JwtHmacKeyFormat proto_key_format;
+  if (!proto_key_format.ParseFromString(
+          serialization.GetKeyTemplate().value())) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse JwtHmacKeyFormat proto.");
+  }
+  if (proto_key_format.version() != 0) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Parsing JwtHmacParameters failed: only version 0 is accepted.");
+  }
+
+  return ToParameters(proto_key_format.key_size(),
+                      serialization.GetKeyTemplate().output_prefix_type(),
+                      proto_key_format.algorithm(), /*has_custom_kid=*/false);
+}
+
+util::StatusOr<internal::ProtoParametersSerialization> SerializeParameters(
+    const JwtHmacParameters& parameters) {
+  if (parameters.GetKidStrategy() == JwtHmacParameters::KidStrategy::kCustom) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Unable to serialize JwtHmacParameters::KidStrategy::kCustom.");
+  }
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(parameters.GetKidStrategy());
+  if (!output_prefix_type.ok()) {
+    return output_prefix_type.status();
+  }
+  util::StatusOr<JwtHmacAlgorithm> proto_algorithm =
+      ToProtoAlgorithm(parameters.GetAlgorithm());
+  if (!proto_algorithm.ok()) {
+    return proto_algorithm.status();
+  }
+
+  JwtHmacKeyFormat format;
+  format.set_version(0);
+  format.set_key_size(parameters.KeySizeInBytes());
+  format.set_algorithm(*proto_algorithm);
+
+  return internal::ProtoParametersSerialization::Create(
+      kTypeUrl, *output_prefix_type, format.SerializeAsString());
+}
+
+util::StatusOr<JwtHmacKey> ParseKey(
+    const internal::ProtoKeySerialization& serialization,
+    absl::optional<SecretKeyAccessToken> token) {
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required.");
+  }
+  if (serialization.TypeUrl() != kTypeUrl) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Wrong type URL when parsing JwtHmacKey.");
+  }
+
+  google::crypto::tink::JwtHmacKey proto_key;
+  const RestrictedData& restricted_data = serialization.SerializedKeyProto();
+  if (!proto_key.ParseFromString(restricted_data.GetSecret(*token))) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "Failed to parse JwtHmacKey proto.");
+  }
+  if (proto_key.version() != 0) {
+    return util::Status(
+        absl::StatusCode::kInvalidArgument,
+        "Parsing JwtHmacKey failed: only version 0 is accepted.");
+  }
+
+  util::StatusOr<JwtHmacParameters> parameters = ToParameters(
+      proto_key.key_value().length(), serialization.GetOutputPrefixType(),
+      proto_key.algorithm(), proto_key.has_custom_kid());
+  if (!parameters.ok()) {
+    return parameters.status();
+  }
+
+  JwtHmacKey::Builder builder =
+      JwtHmacKey::Builder()
+          .SetParameters(*parameters)
+          .SetKeyBytes(RestrictedData(proto_key.key_value(), *token));
+  if (serialization.IdRequirement().has_value()) {
+    builder.SetIdRequirement(*serialization.IdRequirement());
+  }
+  if (proto_key.has_custom_kid()) {
+    builder.SetCustomKid(proto_key.custom_kid().value());
+  }
+  return builder.Build(GetPartialKeyAccess());
+}
+
+util::StatusOr<internal::ProtoKeySerialization> SerializeKey(
+    const JwtHmacKey& key, absl::optional<SecretKeyAccessToken> token) {
+  if (!token.has_value()) {
+    return util::Status(absl::StatusCode::kInvalidArgument,
+                        "SecretKeyAccess is required.");
+  }
+  util::StatusOr<RestrictedData> restricted_input =
+      key.GetKeyBytes(GetPartialKeyAccess());
+  if (!restricted_input.ok()) {
+    return restricted_input.status();
+  }
+  util::StatusOr<JwtHmacAlgorithm> proto_algorithm =
+      ToProtoAlgorithm(key.GetParameters().GetAlgorithm());
+  if (!proto_algorithm.ok()) {
+    return proto_algorithm.status();
+  }
+
+  google::crypto::tink::JwtHmacKey proto_key;
+  proto_key.set_version(0);
+  proto_key.set_key_value(restricted_input->GetSecret(*token));
+  proto_key.set_algorithm(*proto_algorithm);
+  if (key.GetParameters().GetKidStrategy() ==
+      JwtHmacParameters::KidStrategy::kCustom) {
+    proto_key.mutable_custom_kid()->set_value(*key.GetKid());
+  }
+
+  util::StatusOr<OutputPrefixType> output_prefix_type =
+      ToOutputPrefixType(key.GetParameters().GetKidStrategy());
+  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());
+}
+
+JwtHmacProtoParametersParserImpl* JwtHmacProtoParametersParser() {
+  static auto* parser =
+      new JwtHmacProtoParametersParserImpl(kTypeUrl, ParseParameters);
+  return parser;
+}
+
+JwtHmacProtoParametersSerializerImpl* JwtHmacProtoParametersSerializer() {
+  static auto* serializer =
+      new JwtHmacProtoParametersSerializerImpl(kTypeUrl, SerializeParameters);
+  return serializer;
+}
+
+JwtHmacProtoKeyParserImpl* JwtHmacProtoKeyParser() {
+  static auto* parser = new JwtHmacProtoKeyParserImpl(kTypeUrl, ParseKey);
+  return parser;
+}
+
+JwtHmacProtoKeySerializerImpl* JwtHmacProtoKeySerializer() {
+  static auto* serializer = new JwtHmacProtoKeySerializerImpl(SerializeKey);
+  return serializer;
+}
+
+}  // namespace
+
+util::Status RegisterJwtHmacProtoSerialization() {
+  util::Status status =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .RegisterParametersParser(JwtHmacProtoParametersParser());
+  if (!status.ok()) {
+    return status;
+  }
+
+  status =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .RegisterParametersSerializer(JwtHmacProtoParametersSerializer());
+  if (!status.ok()) {
+    return status;
+  }
+
+  status = internal::MutableSerializationRegistry::GlobalInstance()
+               .RegisterKeyParser(JwtHmacProtoKeyParser());
+  if (!status.ok()) {
+    return status;
+  }
+
+  return internal::MutableSerializationRegistry::GlobalInstance()
+      .RegisterKeySerializer(JwtHmacProtoKeySerializer());
+}
+
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/jwt/jwt_hmac_proto_serialization.h b/cc/jwt/jwt_hmac_proto_serialization.h
new file mode 100644
index 0000000..aebba79
--- /dev/null
+++ b/cc/jwt/jwt_hmac_proto_serialization.h
@@ -0,0 +1,31 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_JWT_JWT_HMAC_PROTO_SERIALIZATION_H_
+#define TINK_JWT_JWT_HMAC_PROTO_SERIALIZATION_H_
+
+#include "tink/util/status.h"
+
+namespace crypto {
+namespace tink {
+
+// Registers proto parsers and serializers for JWT HMAC parameters and keys.
+crypto::tink::util::Status RegisterJwtHmacProtoSerialization();
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_JWT_JWT_HMAC_PROTO_SERIALIZATION_H_
diff --git a/cc/jwt/jwt_hmac_proto_serialization_test.cc b/cc/jwt/jwt_hmac_proto_serialization_test.cc
new file mode 100644
index 0000000..c427772
--- /dev/null
+++ b/cc/jwt/jwt_hmac_proto_serialization_test.cc
@@ -0,0 +1,643 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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/jwt/jwt_hmac_proto_serialization.h"
+
+#include <memory>
+#include <string>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#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/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/jwt/jwt_hmac_key.h"
+#include "tink/jwt/jwt_hmac_parameters.h"
+#include "tink/key.h"
+#include "tink/parameters.h"
+#include "tink/partial_key_access.h"
+#include "tink/restricted_data.h"
+#include "tink/subtle/random.h"
+#include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
+#include "proto/common.pb.h"
+#include "proto/jwt_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::StatusIs;
+using ::google::crypto::tink::JwtHmacAlgorithm;
+using ::google::crypto::tink::JwtHmacKeyFormat;
+using ::google::crypto::tink::KeyData;
+using ::google::crypto::tink::OutputPrefixType;
+using ::testing::Eq;
+using ::testing::HasSubstr;
+using ::testing::IsFalse;
+using ::testing::IsTrue;
+using ::testing::NotNull;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+const absl::string_view kTypeUrl =
+    "type.googleapis.com/google.crypto.tink.JwtHmacKey";
+
+struct TestCase {
+  JwtHmacParameters::KidStrategy strategy;
+  OutputPrefixType output_prefix_type;
+  JwtHmacParameters::Algorithm algorithm;
+  JwtHmacAlgorithm proto_algorithm;
+  int key_size;
+  absl::optional<std::string> expected_kid;
+  absl::optional<int> id;
+  std::string output_prefix;
+};
+
+class JwtHmacProtoSerializationTest : public TestWithParam<TestCase> {
+ protected:
+  void SetUp() override {
+    internal::MutableSerializationRegistry::GlobalInstance().Reset();
+  }
+};
+
+TEST_F(JwtHmacProtoSerializationTest, RegisterTwiceSucceeds) {
+  EXPECT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+  EXPECT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    JwtHmacProtoSerializationTestSuite, JwtHmacProtoSerializationTest,
+    Values(TestCase{JwtHmacParameters::KidStrategy::kBase64EncodedKeyId,
+                    OutputPrefixType::TINK,
+                    JwtHmacParameters::Algorithm::kHs256,
+                    JwtHmacAlgorithm::HS256,
+                    /*key_size=*/16, /*expected_kid=*/"AgMEAA",
+                    /*id=*/0x02030400,
+                    /*output_prefix=*/std::string("\x01\x02\x03\x04\x00", 5)},
+           TestCase{JwtHmacParameters::KidStrategy::kIgnored,
+                    OutputPrefixType::RAW, JwtHmacParameters::Algorithm::kHs384,
+                    JwtHmacAlgorithm::HS384,
+                    /*key_size=*/32, /*expected_kid=*/absl::nullopt,
+                    /*id=*/absl::nullopt, /*output_prefix=*/""},
+           TestCase{JwtHmacParameters::KidStrategy::kIgnored,
+                    OutputPrefixType::RAW, JwtHmacParameters::Algorithm::kHs512,
+                    JwtHmacAlgorithm::HS512,
+                    /*key_size=*/32, /*expected_kid=*/absl::nullopt,
+                    /*id=*/absl::nullopt, /*output_prefix=*/""}));
+
+TEST_P(JwtHmacProtoSerializationTest, ParseParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  JwtHmacKeyFormat format;
+  format.set_version(0);
+  format.set_key_size(test_case.key_size);
+  format.set_algorithm(test_case.proto_algorithm);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kTypeUrl, test_case.output_prefix_type, format.SerializeAsString());
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Parameters>> parsed =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseParameters(
+          *serialization);
+  ASSERT_THAT(parsed, IsOk());
+  EXPECT_THAT((*parsed)->HasIdRequirement(), test_case.id.has_value());
+
+  util::StatusOr<JwtHmacParameters> expected = JwtHmacParameters::Create(
+      test_case.key_size, test_case.strategy, test_case.algorithm);
+  ASSERT_THAT(expected, IsOk());
+  EXPECT_THAT(**parsed, Eq(*expected));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, ParseParametersWithInvalidSerialization) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kTypeUrl, 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,
+                       HasSubstr("Failed to parse JwtHmacKeyFormat proto")));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, ParseParametersWithInvalidVersion) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  JwtHmacKeyFormat format;
+  format.set_version(1);  // Invalid version number.
+  format.set_key_size(32);
+  format.set_algorithm(JwtHmacAlgorithm::HS256);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kTypeUrl, OutputPrefixType::RAW, format.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,
+                       HasSubstr("only version 0 is accepted")));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, ParseParametersWithUnknownAlgorithm) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  JwtHmacKeyFormat format;
+  format.set_version(0);
+  format.set_key_size(32);
+  format.set_algorithm(JwtHmacAlgorithm::HS_UNKNOWN);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kTypeUrl, OutputPrefixType::RAW, format.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,
+                       HasSubstr("Could not determine JwtHmacAlgorithm")));
+}
+
+using JwtHmacParsePrefixTest = TestWithParam<OutputPrefixType>;
+
+INSTANTIATE_TEST_SUITE_P(JwtHmacParsePrefixTestSuite, JwtHmacParsePrefixTest,
+                         Values(OutputPrefixType::CRUNCHY,
+                                OutputPrefixType::LEGACY,
+                                OutputPrefixType::UNKNOWN_PREFIX));
+
+TEST_P(JwtHmacParsePrefixTest, ParseParametersWithInvalidPrefix) {
+  OutputPrefixType invalid_output_prefix_type = GetParam();
+  internal::MutableSerializationRegistry::GlobalInstance().Reset();
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  JwtHmacKeyFormat format;
+  format.set_version(0);
+  format.set_key_size(32);
+  format.set_algorithm(JwtHmacAlgorithm::HS256);
+
+  util::StatusOr<internal::ProtoParametersSerialization> serialization =
+      internal::ProtoParametersSerialization::Create(
+          kTypeUrl, invalid_output_prefix_type, format.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,
+               HasSubstr("Invalid OutputPrefixType for JwtHmacKeyFormat")));
+}
+
+TEST_P(JwtHmacProtoSerializationTest, SerializeParameters) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      test_case.key_size, test_case.strategy, test_case.algorithm);
+  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(kTypeUrl));
+
+  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(kTypeUrl));
+  EXPECT_THAT(proto_serialization->GetKeyTemplate().output_prefix_type(),
+              Eq(test_case.output_prefix_type));
+
+  JwtHmacKeyFormat format;
+  ASSERT_THAT(
+      format.ParseFromString(proto_serialization->GetKeyTemplate().value()),
+      IsTrue());
+  EXPECT_THAT(format.version(), Eq(0));
+  EXPECT_THAT(format.key_size(), Eq(test_case.key_size));
+  EXPECT_THAT(format.algorithm(), Eq(test_case.proto_algorithm));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, SerializeParametersWithCustomKidFails) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32, JwtHmacParameters::KidStrategy::kCustom,
+      JwtHmacParameters::Algorithm::kHs256);
+  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,
+          HasSubstr(
+              "Unable to serialize JwtHmacParameters::KidStrategy::kCustom")));
+}
+
+TEST_P(JwtHmacProtoSerializationTest, ParseKeyWithoutCustomKid) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  google::crypto::tink::JwtHmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_algorithm(test_case.proto_algorithm);
+  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(
+          kTypeUrl, 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<JwtHmacParameters> expected_parameters =
+      JwtHmacParameters::Create(test_case.key_size, test_case.strategy,
+                                test_case.algorithm);
+  ASSERT_THAT(expected_parameters, IsOk());
+
+  JwtHmacKey::Builder builder =
+      JwtHmacKey::Builder()
+          .SetParameters(*expected_parameters)
+          .SetKeyBytes(
+              RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()));
+  if (test_case.id.has_value()) {
+    builder.SetIdRequirement(*test_case.id);
+  }
+  util::StatusOr<JwtHmacKey> expected_key =
+      builder.Build(GetPartialKeyAccess());
+  ASSERT_THAT(expected_key, IsOk());
+  EXPECT_THAT(**parsed_key, Eq(*expected_key));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, ParseKeyWithCustomKid) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::JwtHmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_algorithm(JwtHmacAlgorithm::HS256);
+  key_proto.set_key_value(raw_key_bytes);
+  key_proto.mutable_custom_kid()->set_value("custom_kid");
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          kTypeUrl, serialized_key, KeyData::SYMMETRIC, OutputPrefixType::RAW,
+          /*id_requirement=*/absl::nullopt);
+  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(), IsFalse());
+  EXPECT_THAT((*parsed_key)->GetIdRequirement(), Eq(absl::nullopt));
+
+  util::StatusOr<JwtHmacParameters> expected_parameters =
+      JwtHmacParameters::Create(/*key_size_in_bytes=*/32,
+                                JwtHmacParameters::KidStrategy::kCustom,
+                                JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(expected_parameters, IsOk());
+
+  util::StatusOr<JwtHmacKey> expected_key =
+      JwtHmacKey::Builder()
+          .SetParameters(*expected_parameters)
+          .SetKeyBytes(
+              RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()))
+          .SetCustomKid(key_proto.custom_kid().value())
+          .Build(GetPartialKeyAccess());
+  ASSERT_THAT(expected_key, IsOk());
+  EXPECT_THAT(**parsed_key, Eq(*expected_key));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, ParseTinkKeyWithCustomKidFails) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::JwtHmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_algorithm(JwtHmacAlgorithm::HS256);
+  key_proto.set_key_value(raw_key_bytes);
+  key_proto.mutable_custom_kid()->set_value("custom_kid");
+  RestrictedData serialized_key = RestrictedData(
+      key_proto.SerializeAsString(), InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          kTypeUrl, serialized_key, KeyData::SYMMETRIC, OutputPrefixType::TINK,
+          /*id_requirement=*/123);
+  ASSERT_THAT(serialization, IsOk());
+
+  util::StatusOr<std::unique_ptr<Key>> key =
+      internal::MutableSerializationRegistry::GlobalInstance().ParseKey(
+          *serialization, InsecureSecretKeyAccess::Get());
+  // Omitting expectation on specific error message since the error occurs
+  // downstream while building JwtHmacKey object.
+  EXPECT_THAT(key.status(), StatusIs(absl::StatusCode::kInvalidArgument));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, ParseKeyWithInvalidSerialization) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::JwtHmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_algorithm(JwtHmacAlgorithm::HS256);
+  key_proto.set_key_value(raw_key_bytes);
+  RestrictedData serialized_key =
+      RestrictedData("invalid_serialization", InsecureSecretKeyAccess::Get());
+
+  util::StatusOr<internal::ProtoKeySerialization> serialization =
+      internal::ProtoKeySerialization::Create(
+          kTypeUrl, serialized_key, KeyData::SYMMETRIC, OutputPrefixType::RAW,
+          /*id_requirement=*/absl::nullopt);
+  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,
+                       HasSubstr("Failed to parse JwtHmacKey proto")));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, ParseKeyWithInvalidVersion) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::JwtHmacKey key_proto;
+  key_proto.set_version(1);  // Invalid version number.
+  key_proto.set_algorithm(JwtHmacAlgorithm::HS256);
+  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(
+          kTypeUrl, serialized_key, KeyData::SYMMETRIC, OutputPrefixType::RAW,
+          /*id_requirement=*/absl::nullopt);
+  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,
+          HasSubstr("Parsing JwtHmacKey failed: only version 0 is accepted")));
+}
+
+TEST_P(JwtHmacParsePrefixTest, ParseKeyWithInvalidPrefix) {
+  OutputPrefixType invalid_output_prefix_type = GetParam();
+  internal::MutableSerializationRegistry::GlobalInstance().Reset();
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::JwtHmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_algorithm(JwtHmacAlgorithm::HS256);
+  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(kTypeUrl, serialized_key,
+                                              KeyData::SYMMETRIC,
+                                              invalid_output_prefix_type,
+                                              /*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,
+               HasSubstr("Invalid OutputPrefixType for JwtHmacKeyFormat")));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, ParseKeyWithUnknownAlgorithm) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::JwtHmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_algorithm(JwtHmacAlgorithm::HS_UNKNOWN);
+  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(
+          kTypeUrl, serialized_key, KeyData::SYMMETRIC, OutputPrefixType::RAW,
+          /*id_requirement=*/absl::nullopt);
+  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,
+                       HasSubstr("Could not determine JwtHmacAlgorithm")));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, ParseKeyWithoutSecretKeyAccess) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  google::crypto::tink::JwtHmacKey key_proto;
+  key_proto.set_version(0);
+  key_proto.set_algorithm(JwtHmacAlgorithm::HS256);
+  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(
+          kTypeUrl, serialized_key, KeyData::SYMMETRIC, OutputPrefixType::RAW,
+          /*id_requirement=*/absl::nullopt);
+  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,
+                                     HasSubstr("SecretKeyAccess is required")));
+}
+
+TEST_P(JwtHmacProtoSerializationTest, SerializeKeyWithoutCustomKid) {
+  TestCase test_case = GetParam();
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      test_case.key_size, test_case.strategy, test_case.algorithm);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(test_case.key_size);
+  JwtHmacKey::Builder builder =
+      JwtHmacKey::Builder()
+          .SetParameters(*parameters)
+          .SetKeyBytes(
+              RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()));
+  if (test_case.id.has_value()) {
+    builder.SetIdRequirement(*test_case.id);
+  }
+  util::StatusOr<JwtHmacKey> key = builder.Build(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(kTypeUrl));
+
+  const internal::ProtoKeySerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoKeySerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->TypeUrl(), Eq(kTypeUrl));
+  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::JwtHmacKey proto_key;
+  ASSERT_THAT(proto_key.ParseFromString(
+                  proto_serialization->SerializedKeyProto().GetSecret(
+                      InsecureSecretKeyAccess::Get())),
+              IsTrue());
+  EXPECT_THAT(proto_key.version(), Eq(0));
+  EXPECT_THAT(proto_key.key_value(), Eq(raw_key_bytes));
+  EXPECT_THAT(proto_key.algorithm(), Eq(test_case.proto_algorithm));
+  EXPECT_THAT(proto_key.has_custom_kid(), IsFalse());
+}
+
+TEST_F(JwtHmacProtoSerializationTest, SerializeKeyWithCustomKid) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32, JwtHmacParameters::KidStrategy::kCustom,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  util::StatusOr<JwtHmacKey> key =
+      JwtHmacKey::Builder()
+          .SetParameters(*parameters)
+          .SetKeyBytes(
+              RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()))
+          .SetCustomKid("custom_kid")
+          .Build(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(kTypeUrl));
+
+  const internal::ProtoKeySerialization* proto_serialization =
+      dynamic_cast<const internal::ProtoKeySerialization*>(
+          serialization->get());
+  ASSERT_THAT(proto_serialization, NotNull());
+  EXPECT_THAT(proto_serialization->TypeUrl(), Eq(kTypeUrl));
+  EXPECT_THAT(proto_serialization->KeyMaterialType(), Eq(KeyData::SYMMETRIC));
+  EXPECT_THAT(proto_serialization->GetOutputPrefixType(),
+              Eq(OutputPrefixType::RAW));
+  EXPECT_THAT(proto_serialization->IdRequirement(), Eq(absl::nullopt));
+
+  google::crypto::tink::JwtHmacKey proto_key;
+  ASSERT_THAT(proto_key.ParseFromString(
+                  proto_serialization->SerializedKeyProto().GetSecret(
+                      InsecureSecretKeyAccess::Get())),
+              IsTrue());
+  EXPECT_THAT(proto_key.version(), Eq(0));
+  EXPECT_THAT(proto_key.key_value(), Eq(raw_key_bytes));
+  EXPECT_THAT(proto_key.algorithm(), Eq(JwtHmacAlgorithm::HS256));
+  ASSERT_THAT(proto_key.has_custom_kid(), IsTrue());
+  EXPECT_THAT(proto_key.custom_kid().value(), Eq(*key->GetKid()));
+}
+
+TEST_F(JwtHmacProtoSerializationTest, SerializeKeyWithoutSecretKeyAccess) {
+  ASSERT_THAT(RegisterJwtHmacProtoSerialization(), IsOk());
+
+  util::StatusOr<JwtHmacParameters> parameters = JwtHmacParameters::Create(
+      /*key_size_in_bytes=*/32, JwtHmacParameters::KidStrategy::kIgnored,
+      JwtHmacParameters::Algorithm::kHs256);
+  ASSERT_THAT(parameters, IsOk());
+
+  std::string raw_key_bytes = Random::GetRandomBytes(32);
+  util::StatusOr<JwtHmacKey> key =
+      JwtHmacKey::Builder()
+          .SetParameters(*parameters)
+          .SetKeyBytes(
+              RestrictedData(raw_key_bytes, InsecureSecretKeyAccess::Get()))
+          .Build(GetPartialKeyAccess());
+  ASSERT_THAT(key, IsOk());
+
+  util::StatusOr<std::unique_ptr<Serialization>> serialization =
+      internal::MutableSerializationRegistry::GlobalInstance()
+          .SerializeKey<internal::ProtoKeySerialization>(
+              *key, /*token=*/absl::nullopt);
+  ASSERT_THAT(serialization.status(),
+              StatusIs(absl::StatusCode::kInvalidArgument,
+                       HasSubstr("SecretKeyAccess is required")));
+}
+
+}  // namespace
+}  // namespace tink
+}  // namespace crypto
diff --git a/cc/jwt/jwt_signature_parameters.h b/cc/jwt/jwt_signature_parameters.h
new file mode 100644
index 0000000..ddc97ef
--- /dev/null
+++ b/cc/jwt/jwt_signature_parameters.h
@@ -0,0 +1,35 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_JWT_JWT_SIGNATURE_PARAMETERS_H_
+#define TINK_JWT_JWT_SIGNATURE_PARAMETERS_H_
+
+#include "tink/parameters.h"
+
+namespace crypto {
+namespace tink {
+
+// Describes a JWT signature key pair without the randomly chosen key material.
+class JwtSignatureParameters : public Parameters {
+  // Returns true if verification is allowed for tokens without a `kid` header.
+  virtual bool AllowKidAbsent() const = 0;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+
+#endif  // TINK_JWT_JWT_SIGNATURE_PARAMETERS_H_
diff --git a/cc/jwt/jwt_signature_private_key.h b/cc/jwt/jwt_signature_private_key.h
new file mode 100644
index 0000000..b63722b
--- /dev/null
+++ b/cc/jwt/jwt_signature_private_key.h
@@ -0,0 +1,54 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_JWT_JWT_SIGNATURE_PRIVATE_KEY_H_
+#define TINK_JWT_JWT_SIGNATURE_PRIVATE_KEY_H_
+
+#include <string>
+
+#include "absl/types/optional.h"
+#include "tink/jwt/jwt_signature_parameters.h"
+#include "tink/jwt/jwt_signature_public_key.h"
+#include "tink/key.h"
+#include "tink/private_key.h"
+
+namespace crypto {
+namespace tink {
+
+// Represents the signing function for a JWT Signature primitive.
+class JwtSignaturePrivateKey : public PrivateKey {
+ public:
+  const JwtSignaturePublicKey& GetPublicKey() const override = 0;
+
+  absl::optional<std::string> GetKid() const {
+    return GetPublicKey().GetKid();
+  }
+
+  absl::optional<int> GetIdRequirement() const override {
+    return GetPublicKey().GetIdRequirement();
+  }
+
+  const JwtSignatureParameters& GetParameters() const override {
+    return GetPublicKey().GetParameters();
+  }
+
+  bool operator==(const Key& other) const override = 0;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_JWT_JWT_SIGNATURE_PRIVATE_KEY_H_
diff --git a/cc/jwt/jwt_signature_public_key.h b/cc/jwt/jwt_signature_public_key.h
new file mode 100644
index 0000000..e2e670c
--- /dev/null
+++ b/cc/jwt/jwt_signature_public_key.h
@@ -0,0 +1,59 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT 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_JWT_JWT_SIGNATURE_PUBLIC_KEY_H_
+#define TINK_JWT_JWT_SIGNATURE_PUBLIC_KEY_H_
+
+#include <string>
+
+#include "absl/types/optional.h"
+#include "tink/jwt/jwt_signature_parameters.h"
+#include "tink/key.h"
+
+namespace crypto {
+namespace tink {
+
+// Represents the verification function for a JWT Signature primitive.
+class JwtSignaturePublicKey : public Key {
+ public:
+  // Returns the `kid` to be used for this key
+  // (https://www.rfc-editor.org/rfc/rfc7517#section-4.5).
+  //
+  // Note that the `kid` is not necessarily related to Tink's key ID in the
+  // keyset.
+  //
+  // If present, this `kid` will be written into the `kid` header during
+  // `ComputeMacAndEncode()`. If absent, no `kid` will be written.
+  //
+  // If present, and the `kid` header is present, the contents of the
+  // `kid` header need to match the return value of this function for
+  // validation to succeed in `VerifyMacAndDecode()`.
+  //
+  // Note that `GetParameters().AllowKidAbsent()` specifies whether or not
+  // omitting the `kid` header is allowed. Of course, if
+  // `GetParameters().AllowKidAbsent()` returns false, then `GetKid()` must
+  // return a non-empty value.
+  virtual absl::optional<std::string> GetKid() const = 0;
+
+  const JwtSignatureParameters& GetParameters() const override = 0;
+
+  bool operator==(const Key& other) const override = 0;
+};
+
+}  // namespace tink
+}  // namespace crypto
+
+#endif  // TINK_JWT_JWT_SIGNATURE_PUBLIC_KEY_H_
diff --git a/cc/prf/prf_set.h b/cc/prf/prf_set.h
index bdbbbb6..27b1ced 100644
--- a/cc/prf/prf_set.h
+++ b/cc/prf/prf_set.h
@@ -29,7 +29,7 @@
 namespace tink {
 
 // The PRF interface is an abstraction for an element of a pseudo random
-// function family, selected by a key. It has the following property:
+// function family, selected by a key. It has the following properties:
 //   * 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
diff --git a/cc/proto/aes_cmac_prf.proto b/cc/proto/aes_cmac_prf.proto
index cfc4cf1..dd8dcae 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/tink-crypto/tink-go/v2/aes_cmac_prf_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 23028c6..a873097 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/tink-crypto/tink-go/v2/aes_ctr_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 a95a80d..7059346 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/tink-crypto/tink-go/v2/aes_ctr_hmac_aead_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 2d5678b..599e267 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/tink-crypto/tink-go/v2/aes_ctr_hmac_streaming_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 82ff7ee..f086d49 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/tink-crypto/tink-go/v2/aes_eax_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 459ec3d..1908c4a 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/tink-crypto/tink-go/v2/aes_gcm_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 2c445ae..a436abb 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/tink-crypto/tink-go/v2/aes_gcm_hkdf_streaming_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 8cfea19..c663aee 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/tink-crypto/tink-go/v2/aes_gcm_siv_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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/subtle/common_enums_test.cc b/cc/subtle/common_enums_test.cc
index 517d588..4e27b73 100644
--- a/cc/subtle/common_enums_test.cc
+++ b/cc/subtle/common_enums_test.cc
@@ -29,7 +29,6 @@
   EXPECT_EQ("NIST_P384", EnumToString(EllipticCurveType::NIST_P384));
   EXPECT_EQ("NIST_P521", EnumToString(EllipticCurveType::NIST_P521));
   EXPECT_EQ("UNKNOWN_CURVE", EnumToString(EllipticCurveType::UNKNOWN_CURVE));
-  EXPECT_EQ("UNKNOWN_CURVE: 42", EnumToString((EllipticCurveType)42));
 }
 
 TEST_F(CommonEnumsTest, testHashTypeToString) {
@@ -39,7 +38,6 @@
   EXPECT_EQ("SHA384", EnumToString(HashType::SHA384));
   EXPECT_EQ("SHA512", EnumToString(HashType::SHA512));
   EXPECT_EQ("UNKNOWN_HASH", EnumToString(HashType::UNKNOWN_HASH));
-  EXPECT_EQ("UNKNOWN_HASH: 42", EnumToString((HashType)42));
 }
 
 TEST_F(CommonEnumsTest, testEcPointFormatToString) {
@@ -49,7 +47,6 @@
             EnumToString(EcPointFormat::DO_NOT_USE_CRUNCHY_UNCOMPRESSED));
 
   EXPECT_EQ("UNKNOWN_FORMAT", EnumToString(EcPointFormat::UNKNOWN_FORMAT));
-  EXPECT_EQ("UNKNOWN_FORMAT: 42", EnumToString((EcPointFormat)42));
 }
 
 }  // namespace
diff --git a/cc/subtle/stateful_hmac_boringssl_test.cc b/cc/subtle/stateful_hmac_boringssl_test.cc
index 3f761b3..fcd5d7b 100644
--- a/cc/subtle/stateful_hmac_boringssl_test.cc
+++ b/cc/subtle/stateful_hmac_boringssl_test.cc
@@ -53,7 +53,6 @@
 using ::testing::Eq;
 using ::testing::HasSubstr;
 using ::testing::SizeIs;
-using ::testing::StrEq;
 
 struct TestVector {
   TestVector(std::string test_name, std::string hex_key, HashType hash_type,
diff --git a/cc/util/enums_test.cc b/cc/util/enums_test.cc
index c60e146..b01f89b 100644
--- a/cc/util/enums_test.cc
+++ b/cc/util/enums_test.cc
@@ -46,8 +46,6 @@
             Enums::SubtleToProto(subtle::EllipticCurveType::CURVE25519));
   EXPECT_EQ(pb::EllipticCurveType::UNKNOWN_CURVE,
             Enums::SubtleToProto(subtle::EllipticCurveType::UNKNOWN_CURVE));
-  EXPECT_EQ(pb::EllipticCurveType::UNKNOWN_CURVE,
-            Enums::SubtleToProto((subtle::EllipticCurveType)42));
 
   EXPECT_EQ(subtle::EllipticCurveType::NIST_P256,
             Enums::ProtoToSubtle(pb::EllipticCurveType::NIST_P256));
@@ -59,8 +57,6 @@
             Enums::ProtoToSubtle(pb::EllipticCurveType::CURVE25519));
   EXPECT_EQ(subtle::EllipticCurveType::UNKNOWN_CURVE,
             Enums::ProtoToSubtle(pb::EllipticCurveType::UNKNOWN_CURVE));
-  EXPECT_EQ(subtle::EllipticCurveType::UNKNOWN_CURVE,
-            Enums::ProtoToSubtle((pb::EllipticCurveType)42));
 
   // Check that enum conversion covers the entire range of the proto-enum.
   int count = 0;
@@ -87,8 +83,6 @@
             Enums::SubtleToProto(subtle::HashType::SHA512));
   EXPECT_EQ(pb::HashType::UNKNOWN_HASH,
             Enums::SubtleToProto(subtle::HashType::UNKNOWN_HASH));
-  EXPECT_EQ(pb::HashType::UNKNOWN_HASH,
-            Enums::SubtleToProto((subtle::HashType)42));
 
   EXPECT_EQ(subtle::HashType::SHA1, Enums::ProtoToSubtle(pb::HashType::SHA1));
   EXPECT_EQ(subtle::HashType::SHA224,
@@ -101,8 +95,6 @@
             Enums::ProtoToSubtle(pb::HashType::SHA512));
   EXPECT_EQ(subtle::HashType::UNKNOWN_HASH,
             Enums::ProtoToSubtle(pb::HashType::UNKNOWN_HASH));
-  EXPECT_EQ(subtle::HashType::UNKNOWN_HASH,
-            Enums::ProtoToSubtle((pb::HashType)42));
 
   // Check that enum conversion covers the entire range of the proto-enum.
   int count = 0;
@@ -147,8 +139,6 @@
             Enums::ProtoToSubtle(pb::EcPointFormat::COMPRESSED));
   EXPECT_EQ(subtle::EcPointFormat::UNKNOWN_FORMAT,
             Enums::ProtoToSubtle(pb::EcPointFormat::UNKNOWN_FORMAT));
-  EXPECT_EQ(subtle::EcPointFormat::UNKNOWN_FORMAT,
-            Enums::ProtoToSubtle((pb::EcPointFormat)42));
 
   // Check that enum conversion covers the entire range of the proto-enum.
   int count = 0;
@@ -202,8 +192,6 @@
   EXPECT_EQ(
       "UNKNOWN_STATUS",
       std::string(Enums::KeyStatusName(pb::KeyStatusType::UNKNOWN_STATUS)));
-  EXPECT_EQ("UNKNOWN_STATUS",
-            std::string(Enums::KeyStatusName((pb::KeyStatusType)42)));
 
   EXPECT_EQ(pb::KeyStatusType::ENABLED, Enums::KeyStatus("ENABLED"));
   EXPECT_EQ(pb::KeyStatusType::DISABLED, Enums::KeyStatus("DISABLED"));
diff --git a/cc/util/secret_data.h b/cc/util/secret_data.h
index 66e3003..dd2fa5f 100644
--- a/cc/util/secret_data.h
+++ b/cc/util/secret_data.h
@@ -18,11 +18,11 @@
 #define TINK_UTIL_SECRET_DATA_H_
 
 #include <cstddef>
-#include <cstdint>
+#include <cstdint>  // IWYU pragma: keep
 #include <memory>
 #include <string>
 #include <type_traits>
-#include <vector>
+#include <vector>  // IWYU pragma: keep
 
 #include "absl/strings/string_view.h"
 #include "tink/util/secret_data_internal.h"
diff --git a/docs/PRIMITIVES.md b/docs/PRIMITIVES.md
index 53ba5ad..5a00eb7 100644
--- a/docs/PRIMITIVES.md
+++ b/docs/PRIMITIVES.md
@@ -38,38 +38,7 @@
 
 ## Pseudo Random Function Families
 
-The PRF set primitive allows to redact data in a deterministic fashion, for
-example personal identifiable information or internal IDs, or to come up with a
-user ID from user information without revealing said information in the ID. This
-allows someone with access to the output of the PRF without access to the key do
-some types of analysis, while limiting others.
-
-Note that while in theory PRFs can be used in other ways, for example for
-encryption or message authentication, the corresponding primitives should only
-be used for these use cases.
-
-WARNING: Since PRFs operate deterministically on their input, using a PRF to
-redact will not automatically provide anonymity, but only provide pseudonymity.
-It is an important tool to build privacy aware systems, but has to be used
-carefully.
-
-Minimal properties:
-
--   without knowledge of the key the PRF is indistinguishable from a random
-    function
--   at least 128-bit security, also in multi-user scenarios (when an attacker is
-    not targeting a specific key, but any key from a set of up to 2<sup>32</sup>
-    keys)
--   at least 16 byte of output available
-
-WARNING: While HMAC-SHA-2 and HKDF-SHA-2 behave like a cryptographically secure
-hash function if the key is revealed, and still provide some protection against
-revealing the input, AES-CMAC is only secure as long as the key is secure.
-
-Since Tink operates on key sets, this primitive exposes a corresponding set of
-PRFs instead of a single PRF. The PRFs are indexed by a 32 bit key id. This can
-be used to rotate the key used to redact a piece of information, without losing
-the previous association.
+See https://developers.google.com/tink/prf
 
 ## Hybrid Encryption
 
diff --git a/go/daead/BUILD.bazel b/go/daead/BUILD.bazel
index 08c12b4..f7ec324 100644
--- a/go/daead/BUILD.bazel
+++ b/go/daead/BUILD.bazel
@@ -36,6 +36,7 @@
     name = "daead_test",
     srcs = [
         "aes_siv_key_manager_test.go",
+        "daead_benchmark_test.go",
         "daead_factory_test.go",
         "daead_init_test.go",
         "daead_key_templates_test.go",
diff --git a/go/daead/daead_benchmark_test.go b/go/daead/daead_benchmark_test.go
new file mode 100644
index 0000000..abc60bb
--- /dev/null
+++ b/go/daead/daead_benchmark_test.go
@@ -0,0 +1,71 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/daead"
+	"github.com/google/tink/go/keyset"
+	"github.com/google/tink/go/subtle/random"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+// Benchmarks for Deterministic AEAD algorithms.
+
+func BenchmarkAESSIV(b *testing.B) {
+	const (
+		plaintextSize      = 16 * 1024
+		associatedDataSize = 256
+	)
+	testCases := []struct {
+		name     string
+		template *tinkpb.KeyTemplate
+	}{
+		{
+			name:     "AES256_SIV",
+			template: daead.AESSIVKeyTemplate(),
+		},
+	}
+	for _, tc := range testCases {
+		b.Run(tc.name, func(b *testing.B) {
+			b.ReportAllocs()
+
+			handle, err := keyset.NewHandle(tc.template)
+			if err != nil {
+				b.Fatal(err)
+			}
+			primitive, err := daead.New(handle)
+			if err != nil {
+				b.Fatal(err)
+			}
+			plaintext := random.GetRandomBytes(plaintextSize)
+			associatedData := random.GetRandomBytes(associatedDataSize)
+			b.ResetTimer()
+			for i := 0; i < b.N; i++ {
+				ciphertext, err := primitive.EncryptDeterministically(plaintext, associatedData)
+				if err != nil {
+					b.Fatal(err)
+				}
+				_, err = primitive.DecryptDeterministically(ciphertext, associatedData)
+				if err != nil {
+					b.Error(err)
+				}
+			}
+		})
+	}
+}
diff --git a/go/integration/gcpkms/BUILD.bazel b/go/integration/gcpkms/BUILD.bazel
index ed6f7c4..9859a09 100644
--- a/go/integration/gcpkms/BUILD.bazel
+++ b/go/integration/gcpkms/BUILD.bazel
@@ -23,6 +23,7 @@
 go_test(
     name = "gcpkms_test",
     srcs = [
+        "gcp_kms_aead_test.go",
         "gcp_kms_client_test.go",
         "gcp_kms_integration_test.go",
     ],
@@ -31,10 +32,11 @@
         "//testdata/gcp:credentials",
         "@google_root_pem//file",  #keep
     ],
+    embed = [":gcpkms"],
     tags = ["manual"],
     deps = [
-        ":gcpkms",
         "//aead",
+        "@org_golang_google_api//cloudkms/v1:cloudkms",
         "@org_golang_google_api//option",
     ],
 )
diff --git a/go/integration/gcpkms/gcp_kms_aead.go b/go/integration/gcpkms/gcp_kms_aead.go
index ca4a043..410293e 100644
--- a/go/integration/gcpkms/gcp_kms_aead.go
+++ b/go/integration/gcpkms/gcp_kms_aead.go
@@ -16,6 +16,8 @@
 
 import (
 	"encoding/base64"
+	"fmt"
+	"hash/crc32"
 
 	"google.golang.org/api/cloudkms/v1"
 
@@ -24,45 +26,90 @@
 
 // gcpAEAD represents a GCP KMS service to a particular URI.
 type gcpAEAD struct {
-	keyURI string
-	kms    cloudkms.Service
+	keyName string
+	kms     cloudkms.Service
 }
 
 var _ tink.AEAD = (*gcpAEAD)(nil)
 
 // newGCPAEAD returns a new GCP KMS service.
-func newGCPAEAD(keyURI string, kms *cloudkms.Service) tink.AEAD {
+func newGCPAEAD(keyName string, kms *cloudkms.Service) tink.AEAD {
 	return &gcpAEAD{
-		keyURI: keyURI,
-		kms:    *kms,
+		keyName: keyName,
+		kms:     *kms,
 	}
 }
 
-// Encrypt encrypts the plaintext with associatedData.
+// Encrypt calls GCP KMS to encrypt the plaintext with associatedData and returns the resulting ciphertext.
+// It returns an error if the call to KMS fails or if the response returned by KMS does not pass integrity verification
+// (http://cloud.google.com/kms/docs/data-integrity-guidelines#calculating_and_verifying_checksums).
 func (a *gcpAEAD) Encrypt(plaintext, associatedData []byte) ([]byte, error) {
 
 	req := &cloudkms.EncryptRequest{
-		Plaintext:                   base64.URLEncoding.EncodeToString(plaintext),
-		AdditionalAuthenticatedData: base64.URLEncoding.EncodeToString(associatedData),
+		Plaintext:                         base64.URLEncoding.EncodeToString(plaintext),
+		PlaintextCrc32c:                   computeChecksum(plaintext),
+		AdditionalAuthenticatedData:       base64.URLEncoding.EncodeToString(associatedData),
+		AdditionalAuthenticatedDataCrc32c: computeChecksum(associatedData),
+		// Send the integrity verification fields even if their value is 0.
+		ForceSendFields: []string{"PlaintextCrc32c", "AdditionalAuthenticatedDataCrc32c"},
 	}
-	resp, err := a.kms.Projects.Locations.KeyRings.CryptoKeys.Encrypt(a.keyURI, req).Do()
+
+	resp, err := a.kms.Projects.Locations.KeyRings.CryptoKeys.Encrypt(a.keyName, req).Do()
 	if err != nil {
 		return nil, err
 	}
 
-	return base64.StdEncoding.DecodeString(resp.Ciphertext)
+	if !resp.VerifiedPlaintextCrc32c {
+		return nil, fmt.Errorf("KMS request for %q is missing the checksum field plaintext_crc32c, and other information may be missing from the response. Please retry a limited number of times in case the error is transient", a.keyName)
+	}
+	if !resp.VerifiedAdditionalAuthenticatedDataCrc32c {
+		return nil, fmt.Errorf("KMS request for %q is missing the checksum field additional_authenticated_data_crc32c, and other information may be missing from the response. Please retry a limited number of times in case the error is transient", a.keyName)
+	}
+	ciphertext, err := base64.StdEncoding.DecodeString(resp.Ciphertext)
+	if err != nil {
+		return nil, err
+	}
+	if resp.CiphertextCrc32c != computeChecksum(ciphertext) {
+		return nil, fmt.Errorf("KMS response corrupted in transit for %q: the checksum in field ciphertext_crc32c did not match the data in field ciphertext. Please retry in case this is a transient error", a.keyName)
+	}
+
+	return ciphertext, nil
 }
 
-// Decrypt decrypts ciphertext with with associatedData.
+// Decrypt calls GCP KMS to decrypt the ciphertext with with associatedData and returns the resulting plaintext.
+// It returns an error if the call to KMS fails or if the response returned by KMS does not pass integrity verification
+// (http://cloud.google.com/kms/docs/data-integrity-guidelines#calculating_and_verifying_checksums).
 func (a *gcpAEAD) Decrypt(ciphertext, associatedData []byte) ([]byte, error) {
 
 	req := &cloudkms.DecryptRequest{
-		Ciphertext:                  base64.URLEncoding.EncodeToString(ciphertext),
-		AdditionalAuthenticatedData: base64.URLEncoding.EncodeToString(associatedData),
+		Ciphertext:                        base64.URLEncoding.EncodeToString(ciphertext),
+		CiphertextCrc32c:                  computeChecksum(ciphertext),
+		AdditionalAuthenticatedData:       base64.URLEncoding.EncodeToString(associatedData),
+		AdditionalAuthenticatedDataCrc32c: computeChecksum(associatedData),
+		// Send the integrity verification fields even if their value is 0.
+		ForceSendFields: []string{"CiphertextCrc32c", "AdditionalAuthenticatedDataCrc32c"},
 	}
-	resp, err := a.kms.Projects.Locations.KeyRings.CryptoKeys.Decrypt(a.keyURI, req).Do()
+
+	resp, err := a.kms.Projects.Locations.KeyRings.CryptoKeys.Decrypt(a.keyName, req).Do()
 	if err != nil {
 		return nil, err
 	}
-	return base64.StdEncoding.DecodeString(resp.Plaintext)
+
+	plaintext, err := base64.StdEncoding.DecodeString(resp.Plaintext)
+	if err != nil {
+		return nil, err
+	}
+	if resp.PlaintextCrc32c != computeChecksum(plaintext) {
+		return nil, fmt.Errorf("KMS response corrupted in transit for %q: the checksum in field plaintext_crc32c did not match the data in field plaintext. Please retry in case this is a transient error", a.keyName)
+	}
+	return plaintext, nil
+}
+
+// crc32cTable is used to compute checksums. It is defined as a package level variable to avoid
+// re-computation on every CRC calculation.
+var crc32cTable = crc32.MakeTable(crc32.Castagnoli)
+
+// computeChecksum returns the checksum that corresponds to the input value as an int64.
+func computeChecksum(value []byte) int64 {
+	return int64(crc32.Checksum(value, crc32cTable))
 }
diff --git a/go/integration/gcpkms/gcp_kms_aead_test.go b/go/integration/gcpkms/gcp_kms_aead_test.go
new file mode 100644
index 0000000..d2461e3
--- /dev/null
+++ b/go/integration/gcpkms/gcp_kms_aead_test.go
@@ -0,0 +1,270 @@
+// Copyright 2024 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 gcpkms
+
+import (
+	"bytes"
+	"context"
+	"encoding/base64"
+	"encoding/json"
+	"hash/crc32"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"google.golang.org/api/cloudkms/v1"
+	"google.golang.org/api/option"
+)
+
+func initializeServerWithResponse(ctx context.Context, t *testing.T, response any) (*httptest.Server, *cloudkms.Service) {
+	t.Helper()
+	var b []byte
+	switch r := response.(type) {
+	case *cloudkms.EncryptResponse, *cloudkms.DecryptResponse:
+		var err error
+		b, err = json.Marshal(r)
+		if err != nil {
+			t.Fatalf("unable to marshal response: %v", err)
+		}
+	default:
+		t.Fatalf("unsupported response type: %T", r)
+	}
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Write(b)
+	}))
+	svc, err := cloudkms.NewService(ctx, option.WithoutAuthentication(), option.WithEndpoint(ts.URL))
+	if err != nil {
+		t.Fatalf("unable to create client: %v", err)
+	}
+	return ts, svc
+}
+
+func TestEncrypt_FailsWhenPlaintextUnverifed(t *testing.T) {
+	additionalData := []byte("additional data")
+	ciphertext := []byte("ciphertext")
+	ciphertextCrc32c := int64(crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli)))
+
+	testcases := []struct {
+		name            string
+		encryptResponse *cloudkms.EncryptResponse
+	}{
+		{
+			name: "verified_plaintext_crc32c is false",
+			encryptResponse: &cloudkms.EncryptResponse{
+				Ciphertext:              base64.StdEncoding.EncodeToString(ciphertext),
+				CiphertextCrc32c:        ciphertextCrc32c,
+				VerifiedPlaintextCrc32c: false,
+				VerifiedAdditionalAuthenticatedDataCrc32c: true,
+			},
+		},
+		{
+			name: "verified_plaintext_crc32c missing",
+			encryptResponse: &cloudkms.EncryptResponse{
+				Ciphertext:       base64.StdEncoding.EncodeToString(ciphertext),
+				CiphertextCrc32c: ciphertextCrc32c,
+				VerifiedAdditionalAuthenticatedDataCrc32c: true,
+			},
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+			ts, svc := initializeServerWithResponse(ctx, t, tc.encryptResponse)
+			defer ts.Close()
+
+			aead := newGCPAEAD("key name", svc)
+			// Encryption should fail for all plaintexts (empty or non-empty)
+			_, err := aead.Encrypt([]byte("plaintext"), additionalData)
+			if err == nil {
+				t.Errorf("a.Encrypt err = nil, want error")
+			}
+			_, err = aead.Encrypt([]byte(""), additionalData)
+			if err == nil {
+				t.Errorf("a.Encrypt err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestEncrypt_FailsWhenAdditionalAuthenticatedDataUnverifed(t *testing.T) {
+	plaintext := []byte("plaintext")
+	ciphertext := []byte("ciphertext")
+	ciphertextCrc32c := int64(crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli)))
+
+	testcases := []struct {
+		name            string
+		encryptResponse *cloudkms.EncryptResponse
+	}{
+		{
+			name: "verified_additional_authenticated_data_crc32c is false",
+			encryptResponse: &cloudkms.EncryptResponse{
+				Ciphertext:              base64.StdEncoding.EncodeToString(ciphertext),
+				CiphertextCrc32c:        ciphertextCrc32c,
+				VerifiedPlaintextCrc32c: true,
+				VerifiedAdditionalAuthenticatedDataCrc32c: false,
+			},
+		},
+		{
+			name: "verified_additional_authenticated_data_crc32c missing",
+			encryptResponse: &cloudkms.EncryptResponse{
+				Ciphertext:              base64.StdEncoding.EncodeToString(ciphertext),
+				CiphertextCrc32c:        ciphertextCrc32c,
+				VerifiedPlaintextCrc32c: true,
+			},
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+			ts, svc := initializeServerWithResponse(ctx, t, tc.encryptResponse)
+			defer ts.Close()
+
+			aead := newGCPAEAD("key name", svc)
+			// Encryption should fail for all additional authenticated data (empty or non-empty)
+			_, err := aead.Encrypt(plaintext, []byte("additional data"))
+			if err == nil {
+				t.Errorf("a.Encrypt err = nil, want error")
+			}
+			_, err = aead.Encrypt(plaintext, []byte(""))
+			if err == nil {
+				t.Errorf("a.Encrypt err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestEncrypt_FailsWithInvalidCiphertextCrc32c(t *testing.T) {
+	testcases := []struct {
+		name            string
+		encryptResponse *cloudkms.EncryptResponse
+	}{
+		{
+			name: "ciphertext_crc32c does not match ciphertext",
+			encryptResponse: &cloudkms.EncryptResponse{
+				Ciphertext:              base64.StdEncoding.EncodeToString([]byte("ciphertext")),
+				CiphertextCrc32c:        int64(1),
+				VerifiedPlaintextCrc32c: true,
+				VerifiedAdditionalAuthenticatedDataCrc32c: true,
+			},
+		},
+		{
+			name: "ciphertext_crc32c missing",
+			encryptResponse: &cloudkms.EncryptResponse{
+				Ciphertext:              base64.StdEncoding.EncodeToString([]byte("ciphertext")),
+				VerifiedPlaintextCrc32c: true,
+				VerifiedAdditionalAuthenticatedDataCrc32c: true,
+			},
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+			ts, svc := initializeServerWithResponse(ctx, t,
+				tc.encryptResponse)
+			defer ts.Close()
+
+			aead := newGCPAEAD("key name", svc)
+			_, err := aead.Encrypt([]byte("plaintext"), []byte("additional data"))
+			if err == nil {
+				t.Errorf("a.Encrypt err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestEncrypt_Success(t *testing.T) {
+	ciphertext := []byte("ciphertext")
+	ciphertextCrc32c := int64(crc32.Checksum(ciphertext, crc32.MakeTable(crc32.Castagnoli)))
+
+	ctx := context.Background()
+	ts, svc := initializeServerWithResponse(ctx, t,
+		&cloudkms.EncryptResponse{
+			Ciphertext:              base64.StdEncoding.EncodeToString(ciphertext),
+			CiphertextCrc32c:        ciphertextCrc32c,
+			VerifiedPlaintextCrc32c: true,
+			VerifiedAdditionalAuthenticatedDataCrc32c: true,
+		})
+	defer ts.Close()
+
+	aead := newGCPAEAD("key name", svc)
+	gotCiphertext, err := aead.Encrypt([]byte("plaintext"), []byte("additional data"))
+	if err != nil {
+		t.Errorf("a.Encrypt err = %q, want nil", err)
+	}
+	if !bytes.Equal(gotCiphertext, ciphertext) {
+		t.Errorf("Returned ciphertext: %q, want: %q", gotCiphertext, ciphertext)
+	}
+}
+
+func TestDecrypt_FailsWithInvalidPlaintextCrc32c(t *testing.T) {
+	testcases := []struct {
+		name            string
+		decryptResponse *cloudkms.DecryptResponse
+	}{
+		{
+			name: "plaintext_crc32c does not match plaintext",
+			decryptResponse: &cloudkms.DecryptResponse{
+				Plaintext:       base64.StdEncoding.EncodeToString([]byte("plaintext")),
+				PlaintextCrc32c: int64(1),
+			},
+		},
+		{
+			name: "plaintext_crc32c missing",
+			decryptResponse: &cloudkms.DecryptResponse{
+				Plaintext: base64.StdEncoding.EncodeToString([]byte("plaintext")),
+			},
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			ctx := context.Background()
+			ts, svc := initializeServerWithResponse(ctx, t,
+				tc.decryptResponse)
+			defer ts.Close()
+
+			aead := newGCPAEAD("key name", svc)
+			_, err := aead.Decrypt([]byte("ciphertext"), []byte("additional data"))
+			if err == nil {
+				t.Errorf("a.Decrypt err = nil, want error")
+			}
+		})
+	}
+}
+
+func TestDecrypt_Success(t *testing.T) {
+	plaintext := []byte("plaintext")
+	plaintextCrc32c := int64(crc32.Checksum(plaintext, crc32.MakeTable(crc32.Castagnoli)))
+
+	ctx := context.Background()
+	ts, svc := initializeServerWithResponse(ctx, t,
+		&cloudkms.DecryptResponse{
+			Plaintext:       base64.StdEncoding.EncodeToString(plaintext),
+			PlaintextCrc32c: plaintextCrc32c,
+		})
+	defer ts.Close()
+
+	aead := newGCPAEAD("key name", svc)
+	gotPlaintext, err := aead.Decrypt([]byte("ciphertext"), []byte("additional data"))
+	if err != nil {
+		t.Errorf("a.Decrypt err = %q, want nil", err)
+	}
+	if !bytes.Equal(gotPlaintext, plaintext) {
+		t.Errorf("Returned plaitext: %q, want: %q", gotPlaintext, plaintext)
+	}
+}
diff --git a/go/integration/gcpkms/gcp_kms_client.go b/go/integration/gcpkms/gcp_kms_client.go
index 7e36bbe..cbab5a7 100644
--- a/go/integration/gcpkms/gcp_kms_client.go
+++ b/go/integration/gcpkms/gcp_kms_client.go
@@ -77,6 +77,6 @@
 		return nil, errors.New("unsupported keyURI")
 	}
 
-	uri := strings.TrimPrefix(keyURI, gcpPrefix)
-	return newGCPAEAD(uri, c.kms), nil
+	keyName := strings.TrimPrefix(keyURI, gcpPrefix)
+	return newGCPAEAD(keyName, c.kms), nil
 }
diff --git a/go/integration/gcpkms/gcp_kms_integration_test.go b/go/integration/gcpkms/gcp_kms_integration_test.go
index 292bb4b..30ad657 100644
--- a/go/integration/gcpkms/gcp_kms_integration_test.go
+++ b/go/integration/gcpkms/gcp_kms_integration_test.go
@@ -87,3 +87,57 @@
 		t.Error("a.Decrypt(ciphertext, []byte(\"invalid associatedData\")) err = nil, want error")
 	}
 }
+
+func TestAead(t *testing.T) {
+	srcDir, ok := os.LookupEnv("TEST_SRCDIR")
+	if !ok {
+		t.Skip("TEST_SRCDIR not set")
+	}
+	workspaceDir, ok := os.LookupEnv("TEST_WORKSPACE")
+	if !ok {
+		t.Skip("TEST_WORKSPACE not set")
+	}
+	ctx := context.Background()
+	gcpClient, err := gcpkms.NewClientWithOptions(
+		ctx, keyURI, option.WithCredentialsFile(filepath.Join(srcDir, workspaceDir, credFile)))
+	if err != nil {
+		t.Fatalf("gcpkms.NewClientWithOptions() err = %q, want nil", err)
+	}
+	aead, err := gcpClient.GetAEAD(keyURI)
+	if err != nil {
+		t.Fatalf("gcpClient.GetAEAD(keyURI) err = %q, want nil", err)
+	}
+
+	testcases := []struct {
+		name           string
+		plaintext      []byte
+		associatedData []byte
+	}{
+		{
+			name:           "empty_plaintext",
+			plaintext:      []byte(""),
+			associatedData: []byte("authenticated data"),
+		},
+		{
+			name:           "empty_associated_data",
+			plaintext:      []byte("plaintext"),
+			associatedData: []byte(""),
+		},
+	}
+
+	for _, tc := range testcases {
+		t.Run(tc.name, func(t *testing.T) {
+			ciphertext, err := aead.Encrypt(tc.plaintext, tc.associatedData)
+			if err != nil {
+				t.Fatalf("aead.Encrypt(plaintext, associatedData) err = %q, want nil", err)
+			}
+			gotPlaintext, err := aead.Decrypt(ciphertext, tc.associatedData)
+			if err != nil {
+				t.Fatalf("aead.Decrypt(ciphertext, associatedData) err = %q, want nil", err)
+			}
+			if !bytes.Equal(gotPlaintext, tc.plaintext) {
+				t.Errorf("aead.Decrypt() = %q, want %q", gotPlaintext, tc.plaintext)
+			}
+		})
+	}
+}
diff --git a/go/mac/BUILD.bazel b/go/mac/BUILD.bazel
index 0120395..ac76377 100644
--- a/go/mac/BUILD.bazel
+++ b/go/mac/BUILD.bazel
@@ -40,6 +40,7 @@
     srcs = [
         "aes_cmac_key_manager_test.go",
         "hmac_key_manager_test.go",
+        "mac_benchmark_test.go",
         "mac_factory_test.go",
         "mac_init_test.go",
         "mac_key_templates_test.go",
diff --git a/go/mac/mac_benchmark_test.go b/go/mac/mac_benchmark_test.go
new file mode 100644
index 0000000..514960a
--- /dev/null
+++ b/go/mac/mac_benchmark_test.go
@@ -0,0 +1,113 @@
+// Copyright 2024 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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/keyset"
+	"github.com/google/tink/go/mac"
+	"github.com/google/tink/go/subtle/random"
+	tinkpb "github.com/google/tink/go/proto/tink_go_proto"
+)
+
+// Benchmarks for MAC algorithms.
+
+var benchmarkTestCases = []struct {
+	name     string
+	template *tinkpb.KeyTemplate
+	dataSize uint32
+}{
+	{
+		name:     "HMAC_SHA256_16",
+		template: mac.HMACSHA256Tag128KeyTemplate(),
+		dataSize: 16,
+	}, {
+		name:     "HMAC_SHA512_16",
+		template: mac.HMACSHA512Tag256KeyTemplate(),
+		dataSize: 16,
+	}, {
+		name:     "AES_CMAC_16",
+		template: mac.AESCMACTag128KeyTemplate(),
+		dataSize: 16,
+	}, {
+		name:     "HMAC_SHA256_16k",
+		template: mac.HMACSHA256Tag128KeyTemplate(),
+		dataSize: 16 * 1024,
+	}, {
+		name:     "HMAC_SHA512_16k",
+		template: mac.HMACSHA512Tag256KeyTemplate(),
+		dataSize: 16 * 1024,
+	}, {
+		name:     "AES_CMAC_16k",
+		template: mac.AESCMACTag128KeyTemplate(),
+		dataSize: 16 * 1024,
+	},
+}
+
+func BenchmarkComputeMac(b *testing.B) {
+	for _, tc := range benchmarkTestCases {
+		b.Run(tc.name, func(b *testing.B) {
+			b.ReportAllocs()
+
+			handle, err := keyset.NewHandle(tc.template)
+			if err != nil {
+				b.Fatal(err)
+			}
+			primitive, err := mac.New(handle)
+			if err != nil {
+				b.Fatal(err)
+			}
+			data := random.GetRandomBytes(tc.dataSize)
+			b.ResetTimer()
+			for i := 0; i < b.N; i++ {
+				_, err := primitive.ComputeMAC(data)
+				if err != nil {
+					b.Error(err)
+				}
+			}
+		})
+	}
+}
+
+func BenchmarkVerifyMac(b *testing.B) {
+	for _, tc := range benchmarkTestCases {
+		b.Run(tc.name, func(b *testing.B) {
+			b.ReportAllocs()
+
+			handle, err := keyset.NewHandle(tc.template)
+			if err != nil {
+				b.Fatal(err)
+			}
+			primitive, err := mac.New(handle)
+			if err != nil {
+				b.Fatal(err)
+			}
+			data := random.GetRandomBytes(tc.dataSize)
+			tag, err := primitive.ComputeMAC(data)
+			if err != nil {
+				b.Fatal(err)
+			}
+			b.ResetTimer()
+			for i := 0; i < b.N; i++ {
+				if err = primitive.VerifyMAC(tag, data); err != nil {
+					b.Error(err)
+				}
+			}
+		})
+	}
+}
diff --git a/go/prf/prf_set.go b/go/prf/prf_set.go
index daf3084..c54ee41 100644
--- a/go/prf/prf_set.go
+++ b/go/prf/prf_set.go
@@ -23,36 +23,42 @@
 	"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
-//     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
+// The PRF interface is an abstraction for an element of a pseudo-random
+// function family, selected by a key.
+//
+// It has the following properties:
+//   - It is deterministic. ComputePRF(input, length) will always return the
+//     same output if the same key is used. ComputePRF(input, length1) will be a
+//     prefix of ComputePRF(input, length2) if length1 < length2 and the same
 //     key is used.
-//   - 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.
+//   - 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.
-// While PRFs can be used in order to prove authenticity of a message, using the
-// MAC interface is recommended for that use case, as it has support for
+//
+// While PRFs can be used in order to prove authenticity of a message, using
+// the MAC interface is recommended for that use case, as it has support for
 // verification, avoiding the security problems that often happen during
 // verification, and having automatic support for key rotation. It also allows
 // for non-deterministic MAC algorithms.
 type PRF interface {
 	// 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.
 	// If you have 2^n different inputs that your system has to handle
 	// set the output length (in bytes) to at least
 	// ceil(n/4 + 4)
-	// This corresponds to 2*n + 32 bits, meaning a collision will occur with
-	// a probability less than 1:2^32. When in doubt, request a security review.
-	// Returns a non ok status if the algorithm fails or if the output of
-	// algorithm is less than outputLength.
+	//
+	// This corresponds to 2*n + 32 bits, meaning a collision will occur
+	// with a probability less than 1:2^32. When in doubt, request a
+	// security review.
+	//
+	// Returns a non-nil error if the algorithm fails or if the output of
+	// the underlying algorithm is less than outputLength.
 	ComputePRF(input []byte, outputLength uint32) ([]byte, error)
 }
 
@@ -74,10 +80,11 @@
 	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
-// the Keyset.
+// 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 the Keyset.
 type Set struct {
 	// PrimaryID is the key ID marked as primary in the corresponding Keyset.
 	PrimaryID uint32
@@ -85,7 +92,7 @@
 	PRFs map[uint32]PRF
 }
 
-// ComputePrimaryPRF is equivalent to set.PRFs[set.PrimaryID].ComputePRF(input, outputLength).
+// ComputePrimaryPRF is equivalent to set.PRFs[set.PrimaryID].ComputePRF().
 func (s Set) ComputePrimaryPRF(input []byte, outputLength uint32) ([]byte, error) {
 	prf, ok := s.PRFs[s.PrimaryID]
 	if !ok {
diff --git a/java_src/proto/aes_cmac_prf.proto b/java_src/proto/aes_cmac_prf.proto
index cfc4cf1..dd8dcae 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/tink-crypto/tink-go/v2/aes_cmac_prf_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 23028c6..a873097 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/tink-crypto/tink-go/v2/aes_ctr_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 a95a80d..7059346 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/tink-crypto/tink-go/v2/aes_ctr_hmac_aead_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 2d5678b..599e267 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/tink-crypto/tink-go/v2/aes_ctr_hmac_streaming_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 82ff7ee..f086d49 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/tink-crypto/tink-go/v2/aes_eax_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 459ec3d..1908c4a 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/tink-crypto/tink-go/v2/aes_gcm_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 2c445ae..a436abb 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/tink-crypto/tink-go/v2/aes_gcm_hkdf_streaming_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 8cfea19..c663aee 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/tink-crypto/tink-go/v2/aes_gcm_siv_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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/src/main/java/com/google/crypto/tink/prf/Prf.java b/java_src/src/main/java/com/google/crypto/tink/prf/Prf.java
index 1bdf774..d365bd4 100644
--- a/java_src/src/main/java/com/google/crypto/tink/prf/Prf.java
+++ b/java_src/src/main/java/com/google/crypto/tink/prf/Prf.java
@@ -20,14 +20,19 @@
 import java.security.GeneralSecurityException;
 
 /**
- * The PRF interface is an abstraction for an element of a pseudo random function family, selected
- * by a key. It has the following properties:
+ * The Prf interface is an abstraction for an element of a pseudo random function family, selected
+ * by a key.
  *
- * <p>- 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:
- * 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.
+ * <p>It has the following properties:
+ *
+ * <ul>
+ *   <li>It is deterministic: {@link #compute(input, outputLength)} will always return the same
+ *       output if the same key is used. {@code compute(input, length1)} will be a prefix of {@code
+ *       compute(input, length2)} if {@code length1 < length2} and the same key is used.
+ *   <li>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.
+ * </ul>
  *
  * <p>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. While PRFs can be
diff --git a/java_src/tools/gen_java_test_rules.bzl b/java_src/tools/gen_java_test_rules.bzl
index ab532d4..a60e19f 100644
--- a/java_src/tools/gen_java_test_rules.bzl
+++ b/java_src/tools/gen_java_test_rules.bzl
@@ -21,7 +21,7 @@
 
 """
 
-load("//third_party/bazel_rules/rules_java/java:java_test.bzl", "java_test")
+load("@rules_java//java:defs.bzl", "java_test")
 
 def gen_java_test_rules(
         test_files,
diff --git a/java_src/tools/jar_jar.bzl b/java_src/tools/jar_jar.bzl
index 939de65..dd6c6ee 100644
--- a/java_src/tools/jar_jar.bzl
+++ b/java_src/tools/jar_jar.bzl
@@ -14,6 +14,8 @@
 """starlark rules for jarjar. See https://github.com/pantsbuild/jarjar
 """
 
+load("@rules_java//java:defs.bzl", "JavaInfo")
+
 def _jar_jar_impl(ctx):
     ctx.actions.run(
         inputs = [ctx.file.rules, ctx.file.input_jar],
diff --git a/java_src/tools/java_single_jar.bzl b/java_src/tools/java_single_jar.bzl
index 9523c1c..647ea65 100644
--- a/java_src/tools/java_single_jar.bzl
+++ b/java_src/tools/java_single_jar.bzl
@@ -14,6 +14,8 @@
 
 """ Definition of java_single_jar. """
 
+load("@rules_java//java:defs.bzl", "JavaInfo")
+
 def _check_non_empty(value, name):
     if not value:
         fail("%s must be non-empty" % name)
diff --git a/java_src/tools/javadoc.bzl b/java_src/tools/javadoc.bzl
index 5e68c1f..ed8f0f2 100644
--- a/java_src/tools/javadoc.bzl
+++ b/java_src/tools/javadoc.bzl
@@ -30,6 +30,8 @@
   external_javadoc_links: a list of URLs that are passed to Javadoc's `-linkoffline` flag
 """
 
+load("@rules_java//java:defs.bzl", "JavaInfo", "java_common")
+
 def _check_non_empty(value, name):
     if not value:
         fail("%s must be non-empty" % name)
diff --git a/proto/aes_cmac_prf.proto b/proto/aes_cmac_prf.proto
index cfc4cf1..dd8dcae 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/tink-crypto/tink-go/v2/aes_cmac_prf_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 23028c6..a873097 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/tink-crypto/tink-go/v2/aes_ctr_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 a95a80d..7059346 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/tink-crypto/tink-go/v2/aes_ctr_hmac_aead_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 2d5678b..599e267 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/tink-crypto/tink-go/v2/aes_ctr_hmac_streaming_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 82ff7ee..f086d49 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/tink-crypto/tink-go/v2/aes_eax_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 459ec3d..1908c4a 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/tink-crypto/tink-go/v2/aes_gcm_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 2c445ae..a436abb 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/tink-crypto/tink-go/v2/aes_gcm_hkdf_streaming_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 8cfea19..c663aee 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/tink-crypto/tink-go/v2/aes_gcm_siv_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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/jwt/_json_util_test.py b/python/tink/jwt/_json_util_test.py
index 527a779..15462e9 100644
--- a/python/tink/jwt/_json_util_test.py
+++ b/python/tink/jwt/_json_util_test.py
@@ -14,6 +14,7 @@
 """Tests for tink.python.tink.jwt._json_util."""
 
 from absl.testing import absltest
+
 from tink.jwt import _json_util
 from tink.jwt import _jwt_error
 
@@ -37,7 +38,10 @@
       _json_util.json_loads('{"a":"a1", "a":"a2"}')
 
   def test_json_loads_recursion(self):
-    num_recursions = 1000
+    # NOTE: Python 3.12 has raised the maximum C recursion limit to 1500 [1].
+    #
+    # [1] https://github.com/python/cpython/pull/107618
+    num_recursions = 2000
     recursive_json = ('{"a":' * num_recursions) + '""' + ('}' * num_recursions)
     with self.assertRaises(_jwt_error.JwtInvalidError):
       _json_util.json_loads(recursive_json)
diff --git a/python/tink/proto/aes_cmac_prf.proto b/python/tink/proto/aes_cmac_prf.proto
index cfc4cf1..dd8dcae 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/tink-crypto/tink-go/v2/aes_cmac_prf_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 23028c6..a873097 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/tink-crypto/tink-go/v2/aes_ctr_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 6c8927a..fce983c 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/tink-crypto/tink-go/v2/aes_ctr_hmac_aead_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 7dcb44f..a20a6a9 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/tink-crypto/tink-go/v2/aes_ctr_hmac_streaming_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 82ff7ee..f086d49 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/tink-crypto/tink-go/v2/aes_eax_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 459ec3d..1908c4a 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/tink-crypto/tink-go/v2/aes_gcm_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 6800e3b..43862c4 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/tink-crypto/tink-go/v2/aes_gcm_hkdf_streaming_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/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 8cfea19..c663aee 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/tink-crypto/tink-go/v2/aes_gcm_siv_go_proto";
+option go_package = "github.com/tink-crypto/tink-go/v2/proto/aes_gcm_siv_go_proto";
 
 // The only allowed IV size is 12 bytes and tag size is 16 bytes.
 // Thus, accept no params.