Merge branch '1.4' of github.com:google/tink into 1.4
diff --git a/README.md b/README.md
index fe3db8e..13368bc 100644
--- a/README.md
+++ b/README.md
@@ -107,8 +107,8 @@
 ## Current status
 
 *   The latest version is
-    [1.4.0-rc2](https://github.com/google/tink/releases/tag/v1.4.0-rc2),
-    released on 2020-05-14.
+    [1.4.0](https://github.com/google/tink/releases/tag/v1.4.0), released on
+    2020-07-13.
 *   [Java and Android](docs/JAVA-HOWTO.md), [C++](docs/CPP-HOWTO.md),
     [Obj-C](docs/OBJC-HOWTO.md), [Go](docs/GOLANG-HOWTO.md), and
     [Python](docs/PYTHON-HOWTO.md) are field tested and ready for production.
diff --git a/apps/paymentmethodtoken/README.md b/apps/paymentmethodtoken/README.md
index fab00d5..14368a7 100644
--- a/apps/paymentmethodtoken/README.md
+++ b/apps/paymentmethodtoken/README.md
@@ -3,9 +3,9 @@
 ## Latest release
 
 The most recent release is
-[1.4.0-rc2](https://github.com/google/tink/releases/tag/v1.4.0-rc2), released
-2020-05-14. API docs can be found
-[here](https://google.github.com/tink/javadoc/apps-paymentmethodtoken/1.4.0-rc2).
+[1.4.0](https://github.com/google/tink/releases/tag/v1.4.0), released
+2020-07-13. API docs can be found
+[here](https://google.github.com/tink/javadoc/apps-paymentmethodtoken/1.4.0).
 
 The Maven group ID is `com.google.crypto.tink`, and the artifact ID is
 `apps-paymentmethodtoken`.
@@ -16,7 +16,7 @@
 <dependency>
   <groupId>com.google.crypto.tink</groupId>
   <artifactId>apps-paymentmethodtoken</artifactId>
-  <version>1.4.0-rc2</version>
+  <version>1.4.0</version>
 </dependency>
 ```
 
diff --git a/apps/rewardedads/README.md b/apps/rewardedads/README.md
index d3b5a98..e41dd1e 100644
--- a/apps/rewardedads/README.md
+++ b/apps/rewardedads/README.md
@@ -6,9 +6,9 @@
 ## Latest Release
 
 The most recent release is
-[1.4.0-rc2](https://github.com/google/tink/releases/tag/v1.4.0-rc2), released
-2020-05-14. API docs can be found
-[here](https://google.github.com/tink/javadoc/apps-rewardedads/1.4.0-rc2).
+[1.4.0](https://github.com/google/tink/releases/tag/v1.4.0), released
+2020-07-13. API docs can be found
+[here](https://google.github.com/tink/javadoc/apps-rewardedads/1.4.0).
 
 The Maven group ID is `com.google.crypto.tink`, and the artifact ID is
 `apps-rewardedads`.
@@ -19,7 +19,7 @@
 <dependency>
   <groupId>com.google.crypto.tink</groupId>
   <artifactId>apps-rewardedads</artifactId>
-  <version>1.4.0-rc2</version>
+  <version>1.4.0</version>
 </dependency>
 ```
 
diff --git a/apps/webpush/README.md b/apps/webpush/README.md
index 39abdb3..186f967 100644
--- a/apps/webpush/README.md
+++ b/apps/webpush/README.md
@@ -11,7 +11,7 @@
 <dependency>
   <groupId>com.google.crypto.tink</groupId>
   <artifactId>apps-webpush</artifactId>
-  <version>1.4.0-rc2</version>
+  <version>1.4.0</version>
 </dependency>
 ```
 
@@ -19,7 +19,7 @@
 
 ```
 dependencies {
-  implementation 'com.google.crypto.tink:apps-webpush:1.4.0-rc2'
+  implementation 'com.google.crypto.tink:apps-webpush:1.4.0'
 }
 ```
 
diff --git a/cc/aead/aes_ctr_hmac_aead_key_manager.cc b/cc/aead/aes_ctr_hmac_aead_key_manager.cc
index 645b574..cde69a4 100644
--- a/cc/aead/aes_ctr_hmac_aead_key_manager.cc
+++ b/cc/aead/aes_ctr_hmac_aead_key_manager.cc
@@ -19,6 +19,7 @@
 #include <map>
 
 #include "absl/base/casts.h"
+#include "absl/strings/str_cat.h"
 #include "tink/aead.h"
 #include "tink/key_manager.h"
 #include "tink/mac.h"
@@ -144,22 +145,26 @@
   }
   auto params = hmac_key_format.params();
   if (params.tag_size() < kMinTagSizeInBytes) {
-    return ToStatusF(util::error::INVALID_ARGUMENT,
-                     "Invalid HmacParams: tag_size %d is too small.",
-                     params.tag_size());
+    return util::Status(util::error::INVALID_ARGUMENT,
+                        absl::StrCat("Invalid HmacParams: tag_size ",
+                                     params.tag_size(),
+                                     " is too small."));
   }
   std::map<HashType, uint32_t> max_tag_size = {
       {HashType::SHA1, 20}, {HashType::SHA256, 32}, {HashType::SHA512, 64}};
   if (max_tag_size.find(params.hash()) == max_tag_size.end()) {
-    return ToStatusF(util::error::INVALID_ARGUMENT,
-                     "Invalid HmacParams: HashType '%s' not supported.",
-                     Enums::HashName(params.hash()));
+    return util::Status(util::error::INVALID_ARGUMENT,
+                        absl::StrCat("Invalid HmacParams: HashType '",
+                                     Enums::HashName(params.hash()),
+                                     "' not supported."));
   } else {
     if (params.tag_size() > max_tag_size[params.hash()]) {
-      return ToStatusF(
-          util::error::INVALID_ARGUMENT,
-          "Invalid HmacParams: tag_size %d is too big for HashType '%s'.",
-          params.tag_size(), Enums::HashName(params.hash()));
+      return util::Status(util::error::INVALID_ARGUMENT,
+                          absl::StrCat("Invalid HmacParams: tag_size ",
+                                       params.tag_size(),
+                                       " is too big for HashType '",
+                                       Enums::HashName(params.hash()),
+                                       "'."));
     }
   }
 
diff --git a/cc/core/json_keyset_reader.cc b/cc/core/json_keyset_reader.cc
index 1b014be..8ca5e33 100644
--- a/cc/core/json_keyset_reader.cc
+++ b/cc/core/json_keyset_reader.cc
@@ -22,6 +22,7 @@
 
 #include "absl/memory/memory.h"
 #include "absl/strings/escaping.h"
+#include "absl/strings/str_cat.h"
 #include "include/rapidjson/document.h"
 #include "include/rapidjson/error/en.h"
 #include "tink/util/enums.h"
@@ -31,7 +32,6 @@
 #include "tink/util/statusor.h"
 #include "proto/tink.pb.h"
 
-namespace tinkutil = crypto::tink::util;
 
 namespace crypto {
 namespace tink {
@@ -46,30 +46,30 @@
 
 
 // Helpers for validating and parsing JSON strings with EncryptedKeyset-protos.
-tinkutil::Status ValidateEncryptedKeyset(const rapidjson::Document& json_doc) {
+util::Status ValidateEncryptedKeyset(const rapidjson::Document& json_doc) {
   if (!json_doc.HasMember("encryptedKeyset") ||
       !json_doc["encryptedKeyset"].IsString() ||
       (json_doc.HasMember("keysetInfo") &&
        !json_doc["keysetInfo"].IsObject())) {
-    return tinkutil::Status(tinkutil::error::INVALID_ARGUMENT,
+    return util::Status(util::error::INVALID_ARGUMENT,
                             "Invalid JSON EncryptedKeyset");
   }
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
-tinkutil::Status ValidateKeysetInfo(const rapidjson::Value& json_value) {
+util::Status ValidateKeysetInfo(const rapidjson::Value& json_value) {
   if (!json_value.HasMember("primaryKeyId") ||
       !json_value["primaryKeyId"].IsUint() ||
       !json_value.HasMember("keyInfo") ||
       !json_value["keyInfo"].IsArray() ||
       json_value["keyInfo"].Size() < 1) {
-    return tinkutil::Status(tinkutil::error::INVALID_ARGUMENT,
+    return util::Status(util::error::INVALID_ARGUMENT,
                             "Invalid JSON KeysetInfo");
   }
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
-tinkutil::Status ValidateKeyInfo(const rapidjson::Value& json_value) {
+util::Status ValidateKeyInfo(const rapidjson::Value& json_value) {
   if (!json_value.HasMember("typeUrl") ||
       !json_value["typeUrl"].IsString() ||
       !json_value.HasMember("status") ||
@@ -78,13 +78,13 @@
       !json_value["keyId"].IsUint() ||
       !json_value.HasMember("outputPrefixType") ||
       !json_value["outputPrefixType"].IsString()) {
-    return tinkutil::Status(tinkutil::error::INVALID_ARGUMENT,
+    return util::Status(util::error::INVALID_ARGUMENT,
                             "Invalid JSON KeyInfo");
   }
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
-tinkutil::StatusOr<std::unique_ptr<KeysetInfo::KeyInfo>>
+util::StatusOr<std::unique_ptr<KeysetInfo::KeyInfo>>
 KeyInfoFromJson(const rapidjson::Value& json_value) {
   auto status = ValidateKeyInfo(json_value);
   if (!status.ok()) return status;
@@ -98,7 +98,7 @@
   return std::move(key_info);
 }
 
-tinkutil::StatusOr<std::unique_ptr<KeysetInfo>>
+util::StatusOr<std::unique_ptr<KeysetInfo>>
 KeysetInfoFromJson(const rapidjson::Value& json_value) {
   auto status = ValidateKeysetInfo(json_value);
   if (!status.ok()) return status;
@@ -112,14 +112,14 @@
   return std::move(keyset_info);
 }
 
-tinkutil::StatusOr<std::unique_ptr<EncryptedKeyset>>
+util::StatusOr<std::unique_ptr<EncryptedKeyset>>
 EncryptedKeysetFromJson(const rapidjson::Document& json_doc) {
   auto status = ValidateEncryptedKeyset(json_doc);
   if (!status.ok()) return status;
   std::string enc_keyset;
   if (!absl::Base64Unescape(
           json_doc["encryptedKeyset"].GetString(), &enc_keyset)) {
-    return tinkutil::Status(tinkutil::error::INVALID_ARGUMENT,
+    return util::Status(util::error::INVALID_ARGUMENT,
                             "Invalid JSON EncryptedKeyset");
   }
   auto encrypted_keyset = absl::make_unique<EncryptedKeyset>();
@@ -137,19 +137,19 @@
 }
 
 // Helpers for validating and parsing JSON strings with Keyset-protos.
-tinkutil::Status ValidateKeyset(const rapidjson::Document& json_doc) {
+util::Status ValidateKeyset(const rapidjson::Document& json_doc) {
   if (!json_doc.HasMember("primaryKeyId") ||
       !json_doc["primaryKeyId"].IsUint() ||
       !json_doc.HasMember("key") ||
       !json_doc["key"].IsArray() ||
       json_doc["key"].Size() < 1) {
-    return tinkutil::Status(tinkutil::error::INVALID_ARGUMENT,
+    return util::Status(util::error::INVALID_ARGUMENT,
                             "Invalid JSON Keyset");
   }
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
-tinkutil::Status ValidateKey(const rapidjson::Value& json_value) {
+util::Status ValidateKey(const rapidjson::Value& json_value) {
   if (!json_value.HasMember("keyData") ||
       !json_value["keyData"].IsObject() ||
       !json_value.HasMember("status") ||
@@ -158,32 +158,32 @@
       !json_value["keyId"].IsUint() ||
       !json_value.HasMember("outputPrefixType") ||
       !json_value["outputPrefixType"].IsString()) {
-    return tinkutil::Status(tinkutil::error::INVALID_ARGUMENT,
+    return util::Status(util::error::INVALID_ARGUMENT,
                             "Invalid JSON Key");
   }
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
-tinkutil::Status ValidateKeyData(const rapidjson::Value& json_value) {
+util::Status ValidateKeyData(const rapidjson::Value& json_value) {
   if (!json_value.HasMember("typeUrl") ||
       !json_value["typeUrl"].IsString() ||
       !json_value.HasMember("value") ||
       !json_value["value"].IsString() ||
       !json_value.HasMember("keyMaterialType") ||
       !json_value["keyMaterialType"].IsString()) {
-    return tinkutil::Status(tinkutil::error::INVALID_ARGUMENT,
+    return util::Status(util::error::INVALID_ARGUMENT,
                             "Invalid JSON KeyData");
   }
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
-tinkutil::StatusOr<std::unique_ptr<KeyData>>
+util::StatusOr<std::unique_ptr<KeyData>>
 KeyDataFromJson(const rapidjson::Value& json_value) {
   auto status = ValidateKeyData(json_value);
   if (!status.ok()) return status;
   std::string value_field;
   if (!absl::Base64Unescape(json_value["value"].GetString(), &value_field)) {
-    return tinkutil::Status(tinkutil::error::INVALID_ARGUMENT,
+    return util::Status(util::error::INVALID_ARGUMENT,
                             "Invalid JSON KeyData");
   }
   auto key_data = absl::make_unique<KeyData>();
@@ -194,7 +194,7 @@
   return std::move(key_data);
 }
 
-tinkutil::StatusOr<std::unique_ptr<Keyset::Key>>
+util::StatusOr<std::unique_ptr<Keyset::Key>>
 KeyFromJson(const rapidjson::Value& json_value) {
   auto status = ValidateKey(json_value);
   if (!status.ok()) return status;
@@ -210,7 +210,7 @@
   return std::move(key);
 }
 
-tinkutil::StatusOr<std::unique_ptr<Keyset>>
+util::StatusOr<std::unique_ptr<Keyset>>
 KeysetFromJson(const rapidjson::Document& json_doc) {
   auto status = ValidateKeyset(json_doc);
   if (!status.ok()) return status;
@@ -228,10 +228,10 @@
 
 
 //  static
-tinkutil::StatusOr<std::unique_ptr<KeysetReader>> JsonKeysetReader::New(
+util::StatusOr<std::unique_ptr<KeysetReader>> JsonKeysetReader::New(
     std::unique_ptr<std::istream> keyset_stream) {
   if (keyset_stream == nullptr) {
-    return tinkutil::Status(tinkutil::error::INVALID_ARGUMENT,
+    return util::Status(util::error::INVALID_ARGUMENT,
                             "keyset_stream must be non-null.");
   }
   std::unique_ptr<KeysetReader> reader(
@@ -240,13 +240,13 @@
 }
 
 //  static
-tinkutil::StatusOr<std::unique_ptr<KeysetReader>> JsonKeysetReader::New(
+util::StatusOr<std::unique_ptr<KeysetReader>> JsonKeysetReader::New(
     absl::string_view serialized_keyset) {
   std::unique_ptr<KeysetReader> reader(new JsonKeysetReader(serialized_keyset));
   return std::move(reader);
 }
 
-tinkutil::StatusOr<std::unique_ptr<Keyset>> JsonKeysetReader::Read() {
+util::StatusOr<std::unique_ptr<Keyset>> JsonKeysetReader::Read() {
   std::string serialized_keyset_from_stream;
   std::string* serialized_keyset;
   if (keyset_stream_ == nullptr) {
@@ -258,15 +258,16 @@
   }
   rapidjson::Document json_doc(rapidjson::kObjectType);
   if (json_doc.Parse(serialized_keyset->c_str()).HasParseError()) {
-    return ToStatusF(tinkutil::error::INVALID_ARGUMENT,
-                     "Invalid JSON Keyset: Error (offset %u): %s",
-                     json_doc.GetErrorOffset(),
-                     rapidjson::GetParseError_En(json_doc.GetParseError()));
+    return util::Status(
+        util::error::INVALID_ARGUMENT,
+        absl::StrCat(
+            "Invalid JSON Keyset: Error (offset ", json_doc.GetErrorOffset(),
+            "): ", rapidjson::GetParseError_En(json_doc.GetParseError())));
   }
   return KeysetFromJson(json_doc);
 }
 
-tinkutil::StatusOr<std::unique_ptr<EncryptedKeyset>>
+util::StatusOr<std::unique_ptr<EncryptedKeyset>>
 JsonKeysetReader::ReadEncrypted() {
   std::string serialized_keyset_from_stream;
   std::string* serialized_keyset;
@@ -279,10 +280,11 @@
   }
   rapidjson::Document json_doc;
   if (json_doc.Parse(serialized_keyset->c_str()).HasParseError()) {
-    return ToStatusF(tinkutil::error::INVALID_ARGUMENT,
-                     "Invalid JSON EncryptedKeyset: Error (offset %u): %s",
-                     json_doc.GetErrorOffset(),
-                     rapidjson::GetParseError_En(json_doc.GetParseError()));
+    return util::Status(
+        util::error::INVALID_ARGUMENT,
+        absl::StrCat("Invalid JSON EncryptedKeyset: Error (offset ",
+                     json_doc.GetErrorOffset(), "): ",
+                     rapidjson::GetParseError_En(json_doc.GetParseError())));
   }
   return EncryptedKeysetFromJson(json_doc);
 }
diff --git a/cc/core/json_keyset_writer.cc b/cc/core/json_keyset_writer.cc
index afa42a9..d60ef1c 100644
--- a/cc/core/json_keyset_writer.cc
+++ b/cc/core/json_keyset_writer.cc
@@ -34,18 +34,17 @@
 namespace crypto {
 namespace tink {
 
-namespace tinkutil = crypto::tink::util;
 
 using google::crypto::tink::EncryptedKeyset;
 using google::crypto::tink::KeyData;
 using google::crypto::tink::Keyset;
 using google::crypto::tink::KeysetInfo;
-using tinkutil::Enums;
+using util::Enums;
 
 namespace {
 
 // Helpers for transoforming Keyset-protos to  JSON strings.
-tinkutil::Status ToJson(const KeyData& key_data,
+util::Status ToJson(const KeyData& key_data,
                         rapidjson::Value* json_key_data,
                         rapidjson::Document::AllocatorType* allocator) {
   rapidjson::Value type_url(rapidjson::kStringType);
@@ -63,10 +62,10 @@
   key_value.SetString(base64_string.c_str(), *allocator);
   json_key_data->AddMember("value", key_value, *allocator);
 
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
-tinkutil::Status ToJson(const Keyset::Key& key,
+util::Status ToJson(const Keyset::Key& key,
                         rapidjson::Value* json_key,
                         rapidjson::Document::AllocatorType* allocator) {
   rapidjson::Value key_id(rapidjson::kNumberType);
@@ -86,10 +85,10 @@
   auto status = ToJson(key.key_data(), &json_key_data, allocator);
   if (!status.ok()) return status;
   json_key->AddMember("keyData", json_key_data, *allocator);
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
-tinkutil::StatusOr<std::string> ToJsonString(const Keyset& keyset) {
+util::StatusOr<std::string> ToJsonString(const Keyset& keyset) {
   rapidjson::Document json_doc(rapidjson::kObjectType);
   auto& allocator = json_doc.GetAllocator();
 
@@ -111,7 +110,7 @@
   return std::string(string_buffer.GetString());
 }
 
-tinkutil::Status ToJson(const KeysetInfo::KeyInfo& key_info,
+util::Status ToJson(const KeysetInfo::KeyInfo& key_info,
                         rapidjson::Value* json_key_info,
                         rapidjson::Document::AllocatorType* allocator) {
   rapidjson::Value type_url(rapidjson::kStringType);
@@ -130,10 +129,10 @@
   prefix_type.SetString(Enums::OutputPrefixName(key_info.output_prefix_type()),
                         *allocator);
   json_key_info->AddMember("outputPrefixType", prefix_type, *allocator);
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
-tinkutil::Status ToJson(const KeysetInfo& keyset_info,
+util::Status ToJson(const KeysetInfo& keyset_info,
                         rapidjson::Value* json_keyset_info,
                         rapidjson::Document::AllocatorType* allocator) {
   rapidjson::Value primary_key_id(rapidjson::kNumberType);
@@ -148,10 +147,10 @@
     key_info_array.PushBack(json_key_info, *allocator);
   }
   json_keyset_info->AddMember("keyInfo", key_info_array, *allocator);
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
-tinkutil::StatusOr<std::string> ToJsonString(const EncryptedKeyset& keyset) {
+util::StatusOr<std::string> ToJsonString(const EncryptedKeyset& keyset) {
   rapidjson::Document json_doc(rapidjson::kObjectType);
   auto& allocator = json_doc.GetAllocator();
 
@@ -174,23 +173,23 @@
   return std::string(string_buffer.GetString());
 }
 
-tinkutil::Status WriteData(absl::string_view data, std::ostream* destination) {
+util::Status WriteData(absl::string_view data, std::ostream* destination) {
   (*destination) << data;
   if (destination->fail()) {
-    return tinkutil::Status(tinkutil::error::UNKNOWN,
+    return util::Status(util::error::UNKNOWN,
                             "Error writing to the destination stream.");
   }
-  return tinkutil::Status::OK;
+  return util::Status::OK;
 }
 
 }  // anonymous namespace
 
 
 //  static
-tinkutil::StatusOr<std::unique_ptr<JsonKeysetWriter>> JsonKeysetWriter::New(
+util::StatusOr<std::unique_ptr<JsonKeysetWriter>> JsonKeysetWriter::New(
     std::unique_ptr<std::ostream> destination_stream) {
   if (destination_stream == nullptr) {
-    return tinkutil::Status(tinkutil::error::INVALID_ARGUMENT,
+    return util::Status(util::error::INVALID_ARGUMENT,
                             "destination_stream must be non-null.");
   }
   std::unique_ptr<JsonKeysetWriter> writer(
@@ -198,13 +197,13 @@
   return std::move(writer);
 }
 
-tinkutil::Status JsonKeysetWriter::Write(const Keyset& keyset) {
+util::Status JsonKeysetWriter::Write(const Keyset& keyset) {
   auto json_string_result = ToJsonString(keyset);
   if (!json_string_result.ok()) return json_string_result.status();
   return WriteData(json_string_result.ValueOrDie(), destination_stream_.get());
 }
 
-tinkutil::Status JsonKeysetWriter::Write(
+util::Status JsonKeysetWriter::Write(
     const EncryptedKeyset& encrypted_keyset) {
   auto json_string_result = ToJsonString(encrypted_keyset);
   if (!json_string_result.ok()) return json_string_result.status();
diff --git a/cc/signature/rsa_ssa_pss_verify_key_manager.cc b/cc/signature/rsa_ssa_pss_verify_key_manager.cc
index e3461e4..7f442f0 100644
--- a/cc/signature/rsa_ssa_pss_verify_key_manager.cc
+++ b/cc/signature/rsa_ssa_pss_verify_key_manager.cc
@@ -16,6 +16,7 @@
 
 #include "tink/signature/rsa_ssa_pss_verify_key_manager.h"
 
+#include "absl/strings/str_cat.h"
 #include "absl/strings/string_view.h"
 #include "tink/public_key_verify.h"
 #include "tink/subtle/rsa_ssa_pss_verify_boringssl.h"
@@ -88,9 +89,12 @@
   //
   //  - Conscrypt/BouncyCastle do not support different hashes.
   if (params.mgf1_hash() != params.sig_hash()) {
-    return ToStatusF(util::error::INVALID_ARGUMENT,
-                     "MGF1 hash '%d' is different from signature hash '%d'",
-                     params.mgf1_hash(), params.sig_hash());
+    return util::Status(util::error::INVALID_ARGUMENT,
+                        absl::StrCat("MGF1 hash '",
+                                     params.mgf1_hash(),
+                                     "' is different from signature hash '",
+                                     params.sig_hash(),
+                                     "'"));
   }
   if (params.salt_length() < 0) {
     return util::Status(util::error::INVALID_ARGUMENT,
diff --git a/cc/streamingaead/BUILD.bazel b/cc/streamingaead/BUILD.bazel
index 0f62f43..409eaa0 100644
--- a/cc/streamingaead/BUILD.bazel
+++ b/cc/streamingaead/BUILD.bazel
@@ -37,6 +37,7 @@
         ":streaming_aead_wrapper",
         "//:registry",
         "//config:config_util",
+        "//config:tink_fips",
         "//proto:config_cc_proto",
         "//util:status",
         "@com_google_absl//absl/base:core_headers",
@@ -303,6 +304,7 @@
         "//:keyset_handle",
         "//:registry",
         "//:streaming_aead",
+        "//config:tink_fips",
         "//util:status",
         "//util:test_matchers",
         "//util:test_util",
diff --git a/cc/streamingaead/CMakeLists.txt b/cc/streamingaead/CMakeLists.txt
index f123d9f..36d26fb 100644
--- a/cc/streamingaead/CMakeLists.txt
+++ b/cc/streamingaead/CMakeLists.txt
@@ -31,6 +31,7 @@
   DEPS
     absl::memory
     tink::config::config_util
+    tink::config::tink_fips
     tink::core::registry
     tink::proto::config_cc_proto
     tink::streamingaead::aes_ctr_hmac_streaming_key_manager
@@ -267,6 +268,7 @@
   SRCS streaming_aead_config_test.cc
   DEPS
     absl::memory
+    tink::config::tink_fips
     tink::core::config
     tink::core::keyset_handle
     tink::core::registry
diff --git a/cc/streamingaead/streaming_aead_config.cc b/cc/streamingaead/streaming_aead_config.cc
index edb46b3..cdf2a5e 100644
--- a/cc/streamingaead/streaming_aead_config.cc
+++ b/cc/streamingaead/streaming_aead_config.cc
@@ -18,6 +18,7 @@
 
 #include "absl/memory/memory.h"
 #include "tink/config/config_util.h"
+#include "tink/config/tink_fips.h"
 #include "tink/registry.h"
 #include "tink/streamingaead/aes_ctr_hmac_streaming_key_manager.h"
 #include "tink/streamingaead/aes_gcm_hkdf_streaming_key_manager.h"
@@ -37,8 +38,17 @@
 
 // static
 util::Status StreamingAeadConfig::Register() {
-  // Register key managers.
-  auto status = Registry::RegisterKeyTypeManager(
+  // Register primitive wrapper.
+  auto status = Registry::RegisterPrimitiveWrapper(
+      absl::make_unique<StreamingAeadWrapper>());
+
+  // Currently there are no streaming encryption key managers which only use
+  // FIPS-validated implementations, therefore none will be registered in
+  if (kUseOnlyFips) {
+    return util::OkStatus();
+  }
+
+  status = Registry::RegisterKeyTypeManager(
       absl::make_unique<AesGcmHkdfStreamingKeyManager>(), true);
   if (!status.ok()) return status;
 
@@ -46,9 +56,7 @@
       absl::make_unique<AesCtrHmacStreamingKeyManager>(), true);
   if (!status.ok()) return status;
 
-  // Register primitive wrapper.
-  return Registry::RegisterPrimitiveWrapper(
-      absl::make_unique<StreamingAeadWrapper>());
+  return util::OkStatus();
 }
 
 }  // namespace tink
diff --git a/cc/streamingaead/streaming_aead_config_test.cc b/cc/streamingaead/streaming_aead_config_test.cc
index c0079b2..4a61fd2 100644
--- a/cc/streamingaead/streaming_aead_config_test.cc
+++ b/cc/streamingaead/streaming_aead_config_test.cc
@@ -16,12 +16,14 @@
 
 #include "tink/streamingaead/streaming_aead_config.h"
 
+#include <list>
 #include <sstream>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/memory/memory.h"
 #include "tink/config.h"
+#include "tink/config/tink_fips.h"
 #include "tink/keyset_handle.h"
 #include "tink/registry.h"
 #include "tink/streaming_aead.h"
@@ -46,6 +48,10 @@
 };
 
 TEST_F(StreamingAeadConfigTest, Basic) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   EXPECT_THAT(Registry::get_key_manager<StreamingAead>(
                   AesGcmHkdfStreamingKeyManager().get_key_type())
                   .status(),
@@ -68,6 +74,10 @@
 // Tests that the StreamingAeadWrapper has been properly registered
 // and we can wrap primitives.
 TEST_F(StreamingAeadConfigTest, WrappersRegistered) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   ASSERT_TRUE(StreamingAeadConfig::Register().ok());
 
   google::crypto::tink::Keyset::Key key;
@@ -86,6 +96,33 @@
   ASSERT_TRUE(primitive_result.ok()) << primitive_result.status();
 }
 
+// FIPS-only mode tests
+TEST_F(StreamingAeadConfigTest, RegisterNonFipsTemplates) {
+  if (!kUseOnlyFips) {
+    GTEST_SKIP() << "Only supported in FIPS-only mode";
+  }
+
+  EXPECT_THAT(StreamingAeadConfig::Register(), IsOk());
+
+  // Check that we can not retrieve non-FIPS keyset handle
+  std::list<google::crypto::tink::KeyTemplate> non_fips_key_templates;
+  non_fips_key_templates.push_back(
+      StreamingAeadKeyTemplates::Aes128CtrHmacSha256Segment4KB());
+  non_fips_key_templates.push_back(
+      StreamingAeadKeyTemplates::Aes128GcmHkdf4KB());
+  non_fips_key_templates.push_back(
+      StreamingAeadKeyTemplates::Aes256CtrHmacSha256Segment4KB());
+  non_fips_key_templates.push_back(
+      StreamingAeadKeyTemplates::Aes256GcmHkdf1MB());
+  non_fips_key_templates.push_back(
+      StreamingAeadKeyTemplates::Aes256GcmHkdf4KB());
+
+  for (auto key_template : non_fips_key_templates) {
+    EXPECT_THAT(KeysetHandle::GenerateNew(key_template).status(),
+                StatusIs(util::error::NOT_FOUND));
+  }
+}
+
 }  // namespace
 }  // namespace tink
 }  // namespace crypto
diff --git a/cc/subtle/BUILD.bazel b/cc/subtle/BUILD.bazel
index 9d107ed..c3ed5f9 100644
--- a/cc/subtle/BUILD.bazel
+++ b/cc/subtle/BUILD.bazel
@@ -110,7 +110,6 @@
     deps = [
         ":subtle_util_boringssl",
         "//:public_key_sign",
-        "//util:errors",
         "//util:secret_data",
         "//util:statusor",
         "@boringssl//:crypto",
@@ -128,7 +127,7 @@
     deps = [
         ":subtle_util_boringssl",
         "//:public_key_verify",
-        "//util:errors",
+        "//config:tink_fips",
         "//util:statusor",
         "@boringssl//:crypto",
         "@com_google_absl//absl/strings",
@@ -163,6 +162,7 @@
         ":subtle_util",
         ":subtle_util_boringssl",
         "//:mac",
+        "//config:tink_fips",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
@@ -180,6 +180,7 @@
         ":common_enums",
         ":subtle_util_boringssl",
         "//:mac",
+        "//config:tink_fips",
         "//util:errors",
         "//util:secret_data",
         "//util:status",
@@ -199,6 +200,7 @@
         ":common_enums",
         ":subtle_util_boringssl",
         "//:public_key_sign",
+        "//config:tink_fips",
         "//util:errors",
         "//util:status",
         "//util:statusor",
@@ -216,6 +218,7 @@
         ":common_enums",
         ":subtle_util_boringssl",
         "//:public_key_verify",
+        "//config:tink_fips",
         "//util:errors",
         "//util:status",
         "//util:statusor",
@@ -285,6 +288,7 @@
         ":common_enums",
         ":subtle_util_boringssl",
         "//:public_key_sign",
+        "//config:tink_fips",
         "//util:errors",
         "//util:status",
         "//util:statusor",
@@ -520,6 +524,7 @@
         ":random",
         ":subtle_util_boringssl",
         "//:deterministic_aead",
+        "//config:tink_fips",
         "//util:errors",
         "//util:secret_data",
         "//util:status",
@@ -888,9 +893,11 @@
         ":aes_cmac_boringssl",
         ":common_enums",
         "//:mac",
+        "//config:tink_fips",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
+        "//util:test_matchers",
         "//util:test_util",
         "@com_google_googletest//:gtest_main",
     ],
@@ -905,9 +912,11 @@
         ":common_enums",
         ":hmac_boringssl",
         "//:mac",
+        "//config:tink_fips",
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
+        "//util:test_matchers",
         "//util:test_util",
         "@com_google_googletest//:gtest_main",
     ],
@@ -1121,6 +1130,7 @@
         "//:public_key_verify",
         "//util:status",
         "//util:statusor",
+        "//util:test_matchers",
         "//util:test_util",
         "@com_google_googletest//:gtest_main",
     ],
@@ -1143,6 +1153,7 @@
         "//:public_key_verify",
         "//util:status",
         "//util:statusor",
+        "//util:test_matchers",
         "//util:test_util",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
@@ -1165,6 +1176,7 @@
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
+        "//util:test_matchers",
         "//util:test_util",
         "@boringssl//:crypto",
         "@com_google_absl//absl/strings",
@@ -1190,6 +1202,7 @@
         "//util:secret_data",
         "//util:status",
         "//util:statusor",
+        "//util:test_matchers",
         "//util:test_util",
         "@boringssl//:crypto",
         "@com_google_absl//absl/strings",
@@ -1211,9 +1224,12 @@
         ":wycheproof_util",
         "//:public_key_sign",
         "//:public_key_verify",
+        "//config:tink_fips",
         "//util:status",
         "//util:statusor",
+        "//util:test_matchers",
         "//util:test_util",
+        "@boringssl//:crypto",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
         "@rapidjson",
@@ -1229,7 +1245,9 @@
         ":rsa_ssa_pss_sign_boringssl",
         ":rsa_ssa_pss_verify_boringssl",
         ":subtle_util_boringssl",
+        "//config:tink_fips",
         "//util:test_matchers",
+        "@boringssl//:crypto",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
@@ -1249,9 +1267,12 @@
         ":wycheproof_util",
         "//:public_key_sign",
         "//:public_key_verify",
+        "//config:tink_fips",
         "//util:status",
         "//util:statusor",
+        "//util:test_matchers",
         "//util:test_util",
+        "@boringssl//:crypto",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
         "@rapidjson",
@@ -1267,7 +1288,9 @@
         ":rsa_ssa_pkcs1_sign_boringssl",
         ":rsa_ssa_pkcs1_verify_boringssl",
         ":subtle_util_boringssl",
+        "//config:tink_fips",
         "//util:test_matchers",
+        "@boringssl//:crypto",
         "@com_google_absl//absl/strings",
         "@com_google_googletest//:gtest_main",
     ],
diff --git a/cc/subtle/CMakeLists.txt b/cc/subtle/CMakeLists.txt
index 3fb416f..1de7881 100644
--- a/cc/subtle/CMakeLists.txt
+++ b/cc/subtle/CMakeLists.txt
@@ -106,6 +106,7 @@
     ed25519_sign_boringssl.h
   DEPS
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::public_key_sign
     tink::util::errors
     tink::util::secret_data
@@ -123,6 +124,7 @@
     ed25519_verify_boringssl.h
   DEPS
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::public_key_verify
     tink::util::errors
     tink::util::statusor
@@ -156,6 +158,7 @@
   DEPS
     tink::subtle::subtle_util
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::mac
     tink::util::secret_data
     tink::util::status
@@ -172,6 +175,7 @@
   DEPS
     tink::subtle::common_enums
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::mac
     tink::util::errors
     tink::util::secret_data
@@ -190,6 +194,7 @@
   DEPS
     tink::subtle::common_enums
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::public_key_sign
     tink::util::errors
     tink::util::status
@@ -206,6 +211,7 @@
   DEPS
     tink::subtle::common_enums
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::public_key_verify
     tink::util::errors
     tink::util::status
@@ -222,6 +228,7 @@
   DEPS
     tink::subtle::common_enums
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::public_key_verify
     tink::util::errors
     tink::util::status
@@ -238,6 +245,7 @@
   DEPS
     tink::subtle::common_enums
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::public_key_sign
     tink::util::errors
     tink::util::status
@@ -255,6 +263,7 @@
   DEPS
     tink::subtle::common_enums
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::public_key_verify
     tink::util::errors
     tink::util::status
@@ -271,6 +280,7 @@
   DEPS
     tink::subtle::common_enums
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::public_key_sign
     tink::util::errors
     tink::util::status
@@ -490,6 +500,7 @@
   DEPS
     tink::subtle::random
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::core::deterministic_aead
     tink::util::errors
     tink::util::secret_data
@@ -813,10 +824,12 @@
   DEPS
     tink::subtle::common_enums
     tink::subtle::aes_cmac_boringssl
+    tink::config::tink_fips
     tink::core::mac
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
+    tink::util::test_matchers
     tink::util::test_util
 )
 
@@ -826,10 +839,12 @@
   DEPS
     tink::subtle::common_enums
     tink::subtle::hmac_boringssl
+    tink::config::tink_fips
     tink::core::mac
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
+    tink::util::test_matchers
     tink::util::test_util
 )
 
@@ -1002,6 +1017,7 @@
     tink::core::public_key_verify
     tink::util::status
     tink::util::statusor
+    tink::util::test_matchers
     tink::util::test_util
 )
 
@@ -1018,6 +1034,7 @@
     tink::core::public_key_verify
     tink::util::status
     tink::util::statusor
+    tink::util::test_matchers
     tink::util::test_util
     absl::strings
     rapidjson
@@ -1036,6 +1053,7 @@
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
+    tink::util::test_matchers
     tink::util::test_util
     crypto
     absl::strings
@@ -1055,6 +1073,7 @@
     tink::util::secret_data
     tink::util::status
     tink::util::statusor
+    tink::util::test_matchers
     tink::util::test_util
     crypto
     absl::strings
@@ -1068,12 +1087,15 @@
     tink::subtle::common_enums
     tink::subtle::rsa_ssa_pss_verify_boringssl
     tink::subtle::wycheproof_util
+    tink::config::tink_fips
     tink::core::public_key_sign
     tink::core::public_key_verify
     tink::util::status
     tink::util::statusor
+    tink::util::test_matchers
     tink::util::test_util
     absl::strings
+    crypto
     rapidjson
 )
 
@@ -1084,8 +1106,10 @@
     tink::subtle::rsa_ssa_pss_sign_boringssl
     tink::subtle::rsa_ssa_pss_verify_boringssl
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::util::test_matchers
     absl::strings
+    crypto
 )
 
 tink_cc_test(
@@ -1096,12 +1120,15 @@
     tink::subtle::common_enums
     tink::subtle::rsa_ssa_pkcs1_verify_boringssl
     tink::subtle::wycheproof_util
+    tink::config::tink_fips
     tink::core::public_key_sign
     tink::core::public_key_verify
     tink::util::status
     tink::util::statusor
+    tink::util::test_matchers
     tink::util::test_util
     absl::strings
+    crypto
     rapidjson
 )
 
@@ -1112,8 +1139,10 @@
     tink::subtle::rsa_ssa_pkcs1_sign_boringssl
     tink::subtle::rsa_ssa_pkcs1_verify_boringssl
     tink::subtle::subtle_util_boringssl
+    tink::config::tink_fips
     tink::util::test_matchers
     absl::strings
+    crypto
 )
 
 tink_cc_test(
diff --git a/cc/subtle/aes_cmac_boringssl.cc b/cc/subtle/aes_cmac_boringssl.cc
index 4a9dce1..7c40768 100644
--- a/cc/subtle/aes_cmac_boringssl.cc
+++ b/cc/subtle/aes_cmac_boringssl.cc
@@ -32,6 +32,9 @@
 // static
 util::StatusOr<std::unique_ptr<Mac>> AesCmacBoringSsl::New(util::SecretData key,
                                                            uint32_t tag_size) {
+  auto status = CheckFipsCompatibility<AesCmacBoringSsl>();
+  if (!status.ok()) return status;
+
   if (key.size() != kSmallKeySize && key.size() != kBigKeySize) {
     return util::Status(util::error::INVALID_ARGUMENT, "invalid key size");
   }
diff --git a/cc/subtle/aes_cmac_boringssl.h b/cc/subtle/aes_cmac_boringssl.h
index d7c4b76..a4d0540 100644
--- a/cc/subtle/aes_cmac_boringssl.h
+++ b/cc/subtle/aes_cmac_boringssl.h
@@ -21,6 +21,7 @@
 #include <utility>
 
 #include "tink/mac.h"
+#include "tink/config/tink_fips.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/statusor.h"
 
@@ -42,6 +43,9 @@
   crypto::tink::util::Status VerifyMac(absl::string_view mac,
                                        absl::string_view data) const override;
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kNotFips;
+
  private:
   // CMAC key sizes in bytes.
   // The small key size is used only to check RFC 4493's test vectors due to
diff --git a/cc/subtle/aes_cmac_boringssl_test.cc b/cc/subtle/aes_cmac_boringssl_test.cc
index 8563259..669a97b 100644
--- a/cc/subtle/aes_cmac_boringssl_test.cc
+++ b/cc/subtle/aes_cmac_boringssl_test.cc
@@ -19,11 +19,13 @@
 #include <string>
 
 #include "gtest/gtest.h"
+#include "tink/config/tink_fips.h"
 #include "tink/mac.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 namespace crypto {
@@ -31,10 +33,16 @@
 namespace subtle {
 namespace {
 
+using ::crypto::tink::test::StatusIs;
+
 constexpr uint32_t kTagSize = 16;
 constexpr uint32_t kSmallTagSize = 10;
 
 TEST(AesCmacBoringSslTest, Basic) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"));
   auto cmac_result = AesCmacBoringSsl::New(key, kTagSize);
@@ -63,6 +71,10 @@
 }
 
 TEST(AesCmacBoringSslTest, Modification) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"));
   auto cmac_result = AesCmacBoringSsl::New(key, kTagSize);
@@ -83,6 +95,10 @@
 }
 
 TEST(AesCmacBoringSslTest, Truncation) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"));
   auto cmac_result = AesCmacBoringSsl::New(key, kTagSize);
@@ -101,6 +117,10 @@
 }
 
 TEST(AesCmacBoringSslTest, BasicSmallTag) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"));
   auto cmac_result = AesCmacBoringSsl::New(key, kSmallTagSize);
@@ -129,6 +149,10 @@
 }
 
 TEST(AesCmacBoringSslTest, ModificationSmallTag) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"));
   auto cmac_result = AesCmacBoringSsl::New(key, kSmallTagSize);
@@ -149,6 +173,10 @@
 }
 
 TEST(AesCmacBoringSslTest, TruncationOrAdditionSmallTag) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"));
   auto cmac_result = AesCmacBoringSsl::New(key, kSmallTagSize);
@@ -173,6 +201,10 @@
 }
 
 TEST(AesCmacBoringSslTest, InvalidKeySizes) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   for (int keysize = 0; keysize < 65; keysize++) {
     util::SecretData key(keysize, 'x');
     auto cmac_result = AesCmacBoringSsl::New(key, kTagSize);
@@ -185,6 +217,10 @@
 }
 
 TEST(AesCmacBoringSslTest, InvalidTagSizes) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   for (int tagsize = 0; tagsize < 65; tagsize++) {
     util::SecretData key(32, 'x');
     auto cmac_result = AesCmacBoringSsl::New(key, tagsize);
@@ -216,6 +252,10 @@
 };
 
 TEST_P(AesCmacBoringSslTestVectorTest, RfcTestVectors) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
+
   // Test vectors from RFC 4493.
   std::string key("2b7e151628aed2a6abf7158809cf4f3c");
   std::string data(
@@ -232,6 +272,21 @@
         std::make_pair(40, "dfa66747de9ae63030ca32611497c827"),
         std::make_pair(64, "51f0bebf7e3b9d92fc49741779363cfe")));
 
+TEST(AesCmacBoringSslTest, TestFipsOnly) {
+  if (!kUseOnlyFips) {
+    GTEST_SKIP() << "Only supported in FIPS-only mode";
+  }
+
+  util::SecretData key128 = util::SecretDataFromStringView(
+      test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
+  util::SecretData key256 = util::SecretDataFromStringView(test::HexDecodeOrDie(
+      "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"));
+
+  EXPECT_THAT(subtle::AesCmacBoringSsl::New(key128, 16).status(),
+              StatusIs(util::error::INTERNAL));
+  EXPECT_THAT(subtle::AesCmacBoringSsl::New(key256, 16).status(),
+              StatusIs(util::error::INTERNAL));
+}
 }  // namespace
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/subtle/aes_siv_boringssl.cc b/cc/subtle/aes_siv_boringssl.cc
index 1b174bc..e628ad5 100644
--- a/cc/subtle/aes_siv_boringssl.cc
+++ b/cc/subtle/aes_siv_boringssl.cc
@@ -47,6 +47,9 @@
 // static
 crypto::tink::util::StatusOr<std::unique_ptr<DeterministicAead>>
 AesSivBoringSsl::New(const util::SecretData& key) {
+  auto status = CheckFipsCompatibility<AesSivBoringSsl>();
+  if (!status.ok()) return status;
+
   if (!IsValidKeySizeInBytes(key.size())) {
     return util::Status(util::error::INVALID_ARGUMENT, "invalid key size");
   }
diff --git a/cc/subtle/aes_siv_boringssl.h b/cc/subtle/aes_siv_boringssl.h
index 1f9d3ac..f44d061 100644
--- a/cc/subtle/aes_siv_boringssl.h
+++ b/cc/subtle/aes_siv_boringssl.h
@@ -23,6 +23,7 @@
 #include "absl/types/span.h"
 #include "openssl/aes.h"
 #include "tink/deterministic_aead.h"
+#include "tink/config/tink_fips.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
@@ -70,6 +71,9 @@
     return size == 64;
   }
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kNotFips;
+
  private:
   static constexpr size_t kBlockSize = 16;
 
diff --git a/cc/subtle/aes_siv_boringssl_test.cc b/cc/subtle/aes_siv_boringssl_test.cc
index 23efcb1..c124d8d 100644
--- a/cc/subtle/aes_siv_boringssl_test.cc
+++ b/cc/subtle/aes_siv_boringssl_test.cc
@@ -24,6 +24,7 @@
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 namespace crypto {
@@ -31,7 +32,12 @@
 namespace subtle {
 namespace {
 
+using ::crypto::tink::test::StatusIs;
+
 TEST(AesSivBoringSslTest, testCarryComputation) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
   uint8_t value = 0;
   for (int i = 0; i < 256; i++) {
     uint8_t carry = *reinterpret_cast<int8_t*>(&value) >> 7;
@@ -45,6 +51,9 @@
 }
 
 TEST(AesSivBoringSslTest, testEncryptDecrypt) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
       "00112233445566778899aabbccddeefff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"));
@@ -61,6 +70,9 @@
 }
 
 TEST(AesSivBoringSslTest, testNullPtrStringView) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
       "00112233445566778899aabbccddeefff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"));
@@ -91,6 +103,9 @@
 
 // Only 64 byte key sizes are supported.
 TEST(AesSivBoringSslTest, testEncryptDecryptKeySizes) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
   util::SecretData keymaterial =
       util::SecretDataFromStringView(test::HexDecodeOrDie(
           "198371900187498172316311acf81d238ff7619873a61983d619c87b63a1987f"
@@ -111,6 +126,9 @@
 
 // Checks a range of message sizes.
 TEST(AesSivBoringSslTest, testEncryptDecryptMessageSize) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
       "00112233445566778899aabbccddeefff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"));
@@ -138,6 +156,9 @@
 
 // Checks a range of aad sizes.
 TEST(AesSivBoringSslTest, testEncryptDecryptAadSize) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
       "00112233445566778899aabbccddeefff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"));
@@ -156,6 +177,9 @@
 }
 
 TEST(AesSivBoringSslTest, testDecryptModification) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
   util::SecretData key = util::SecretDataFromStringView(test::HexDecodeOrDie(
       "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
       "00112233445566778899aabbccddeefff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"));
@@ -234,11 +258,30 @@
 }
 
 TEST(AesSivBoringSslTest, TestVectors) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Not supported in FIPS-only mode";
+  }
   std::unique_ptr<rapidjson::Document> root =
       WycheproofUtil::ReadTestVectors("aes_siv_cmac_test.json");
   WycheproofTest(*root);
 }
 
+TEST(AesEaxBoringSslTest, TestFipsOnly) {
+  if (!kUseOnlyFips) {
+    GTEST_SKIP() << "Only supported in FIPS-only mode";
+  }
+
+  util::SecretData key128 = util::SecretDataFromStringView(
+      test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
+  util::SecretData key256 = util::SecretDataFromStringView(test::HexDecodeOrDie(
+      "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"));
+
+  EXPECT_THAT(subtle::AesSivBoringSsl::New(key128).status(),
+              StatusIs(util::error::INTERNAL));
+  EXPECT_THAT(subtle::AesSivBoringSsl::New(key256).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
 }  // namespace
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/subtle/ecdsa_sign_boringssl.cc b/cc/subtle/ecdsa_sign_boringssl.cc
index 38c84a0..3b5f1ac 100644
--- a/cc/subtle/ecdsa_sign_boringssl.cc
+++ b/cc/subtle/ecdsa_sign_boringssl.cc
@@ -70,6 +70,9 @@
 util::StatusOr<std::unique_ptr<EcdsaSignBoringSsl>> EcdsaSignBoringSsl::New(
     const SubtleUtilBoringSSL::EcKey& ec_key, HashType hash_type,
     EcdsaSignatureEncoding encoding) {
+  auto status = CheckFipsCompatibility<EcdsaSignBoringSsl>();
+  if (!status.ok()) return status;
+
   // Check hash.
   auto hash_status = SubtleUtilBoringSSL::ValidateSignatureHash(hash_type);
   if (!hash_status.ok()) {
diff --git a/cc/subtle/ecdsa_sign_boringssl.h b/cc/subtle/ecdsa_sign_boringssl.h
index 5c1e7a1..cd31312 100644
--- a/cc/subtle/ecdsa_sign_boringssl.h
+++ b/cc/subtle/ecdsa_sign_boringssl.h
@@ -20,6 +20,7 @@
 #include <memory>
 
 #include "absl/strings/string_view.h"
+#include "tink/config/tink_fips.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/subtle/subtle_util_boringssl.h"
 #include "tink/public_key_sign.h"
@@ -42,6 +43,9 @@
   crypto::tink::util::StatusOr<std::string> Sign(
       absl::string_view data) const override;
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kRequiresBoringCrypto;
+
  private:
   EcdsaSignBoringSsl(bssl::UniquePtr<EC_KEY> key, const EVP_MD* hash,
                      EcdsaSignatureEncoding encoding);
diff --git a/cc/subtle/ecdsa_sign_boringssl_test.cc b/cc/subtle/ecdsa_sign_boringssl_test.cc
index c9d7648..a60832f 100644
--- a/cc/subtle/ecdsa_sign_boringssl_test.cc
+++ b/cc/subtle/ecdsa_sign_boringssl_test.cc
@@ -27,6 +27,7 @@
 #include "tink/subtle/subtle_util_boringssl.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 namespace crypto {
@@ -34,10 +35,16 @@
 namespace subtle {
 namespace {
 
+using ::crypto::tink::test::StatusIs;
+
 class EcdsaSignBoringSslTest : public ::testing::Test {
 };
 
 TEST_F(EcdsaSignBoringSslTest, testBasicSigning) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   subtle::EcdsaSignatureEncoding encodings[2] = {
       EcdsaSignatureEncoding::DER, EcdsaSignatureEncoding::IEEE_P1363};
   for (EcdsaSignatureEncoding encoding : encodings) {
@@ -75,6 +82,10 @@
 }
 
 TEST_F(EcdsaSignBoringSslTest, testEncodingsMismatch) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   subtle::EcdsaSignatureEncoding encodings[2] = {
       EcdsaSignatureEncoding::DER, EcdsaSignatureEncoding::IEEE_P1363};
   for (EcdsaSignatureEncoding encoding : encodings) {
@@ -102,6 +113,10 @@
 }
 
 TEST_F(EcdsaSignBoringSslTest, testSignatureSizesWithIEEE_P1364Encoding) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   EllipticCurveType curves[3] = {EllipticCurveType::NIST_P256,
                                  EllipticCurveType::NIST_P384,
                                  EllipticCurveType::NIST_P521};
@@ -130,6 +145,10 @@
 }
 
 TEST_F(EcdsaSignBoringSslTest, testNewErrors) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   auto ec_key = SubtleUtilBoringSSL::GetNewEcKey(EllipticCurveType::NIST_P256)
                     .ValueOrDie();
   auto signer_result = EcdsaSignBoringSsl::New(ec_key, HashType::SHA1,
@@ -139,6 +158,32 @@
 
 // TODO(bleichen): add Wycheproof tests.
 
+// FIPS-only mode test
+TEST_F(EcdsaSignBoringSslTest, TestFipsFailWithoutBoringCrypto) {
+  if (!kUseOnlyFips || FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+
+  auto ec_key = SubtleUtilBoringSSL::GetNewEcKey(EllipticCurveType::NIST_P256)
+                    .ValueOrDie();
+  EXPECT_THAT(EcdsaSignBoringSsl::New(ec_key, HashType::SHA256,
+                                      EcdsaSignatureEncoding::DER).status(),
+              StatusIs(util::error::INTERNAL));
+
+  ec_key = SubtleUtilBoringSSL::GetNewEcKey(EllipticCurveType::NIST_P384)
+                    .ValueOrDie();
+  EXPECT_THAT(EcdsaSignBoringSsl::New(ec_key, HashType::SHA256,
+                                      EcdsaSignatureEncoding::DER).status(),
+              StatusIs(util::error::INTERNAL));
+
+  ec_key = SubtleUtilBoringSSL::GetNewEcKey(EllipticCurveType::NIST_P521)
+                    .ValueOrDie();
+  EXPECT_THAT(EcdsaSignBoringSsl::New(ec_key, HashType::SHA256,
+                                      EcdsaSignatureEncoding::DER).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
 }  // namespace
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/subtle/ecdsa_verify_boringssl.cc b/cc/subtle/ecdsa_verify_boringssl.cc
index c040500..8e392c2 100644
--- a/cc/subtle/ecdsa_verify_boringssl.cc
+++ b/cc/subtle/ecdsa_verify_boringssl.cc
@@ -58,6 +58,9 @@
 util::StatusOr<std::unique_ptr<EcdsaVerifyBoringSsl>> EcdsaVerifyBoringSsl::New(
     bssl::UniquePtr<EC_KEY> ec_key, HashType hash_type,
     EcdsaSignatureEncoding encoding) {
+  auto status = CheckFipsCompatibility<EcdsaVerifyBoringSsl>();
+  if (!status.ok()) return status;
+
   // Check hash.
   auto hash_status = SubtleUtilBoringSSL::ValidateSignatureHash(hash_type);
   if (!hash_status.ok()) {
diff --git a/cc/subtle/ecdsa_verify_boringssl.h b/cc/subtle/ecdsa_verify_boringssl.h
index 17cd00e..0884c69 100644
--- a/cc/subtle/ecdsa_verify_boringssl.h
+++ b/cc/subtle/ecdsa_verify_boringssl.h
@@ -20,6 +20,7 @@
 #include <memory>
 
 #include "absl/strings/string_view.h"
+#include "tink/config/tink_fips.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/subtle/subtle_util_boringssl.h"
 #include "tink/public_key_verify.h"
@@ -47,6 +48,9 @@
       absl::string_view signature,
       absl::string_view data) const override;
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kRequiresBoringCrypto;
+
  private:
   EcdsaVerifyBoringSsl(bssl::UniquePtr<EC_KEY> key, const EVP_MD* hash,
                        EcdsaSignatureEncoding encoding)
diff --git a/cc/subtle/ecdsa_verify_boringssl_test.cc b/cc/subtle/ecdsa_verify_boringssl_test.cc
index a4695b2..47bd68e 100644
--- a/cc/subtle/ecdsa_verify_boringssl_test.cc
+++ b/cc/subtle/ecdsa_verify_boringssl_test.cc
@@ -29,6 +29,7 @@
 #include "tink/subtle/wycheproof_util.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 #include "gtest/gtest.h"
 
@@ -37,9 +38,15 @@
 namespace subtle {
 namespace {
 
+using ::crypto::tink::test::StatusIs;
+
 class EcdsaVerifyBoringSslTest : public ::testing::Test {};
 
 TEST_F(EcdsaVerifyBoringSslTest, BasicSigning) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   subtle::EcdsaSignatureEncoding encodings[2] = {
       EcdsaSignatureEncoding::DER, EcdsaSignatureEncoding::IEEE_P1363};
   for (EcdsaSignatureEncoding encoding : encodings) {
@@ -78,6 +85,10 @@
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, EncodingsMismatch) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   subtle::EcdsaSignatureEncoding encodings[2] = {
       EcdsaSignatureEncoding::DER, EcdsaSignatureEncoding::IEEE_P1363};
   for (EcdsaSignatureEncoding encoding : encodings) {
@@ -110,6 +121,10 @@
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, NewErrors) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   auto ec_key = SubtleUtilBoringSSL::GetNewEcKey(EllipticCurveType::NIST_P256)
                     .ValueOrDie();
   auto verifier_result = EcdsaVerifyBoringSsl::New(
@@ -203,25 +218,67 @@
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, WycheproofCurveP256) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   ASSERT_TRUE(TestSignatures("ecdsa_secp256r1_sha256_test.json", false,
                              subtle::EcdsaSignatureEncoding::DER));
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, WycheproofCurveP384) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   ASSERT_TRUE(TestSignatures("ecdsa_secp384r1_sha512_test.json", false,
                              subtle::EcdsaSignatureEncoding::DER));
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, WycheproofCurveP521) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   ASSERT_TRUE(TestSignatures("ecdsa_secp521r1_sha512_test.json", false,
                              subtle::EcdsaSignatureEncoding::DER));
 }
 
 TEST_F(EcdsaVerifyBoringSslTest, WycheproofWithIeeeP1363Encoding) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   ASSERT_TRUE(TestSignatures("ecdsa_webcrypto_test.json", true,
                              subtle::EcdsaSignatureEncoding::IEEE_P1363));
 }
 
+// FIPS-only mode test
+TEST_F(EcdsaVerifyBoringSslTest, TestFipsFailWithoutBoringCrypto) {
+  if (!kUseOnlyFips || FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+
+  auto ec_key = SubtleUtilBoringSSL::GetNewEcKey(EllipticCurveType::NIST_P256)
+                    .ValueOrDie();
+  EXPECT_THAT(EcdsaVerifyBoringSsl::New(ec_key, HashType::SHA256,
+                                      EcdsaSignatureEncoding::DER).status(),
+              StatusIs(util::error::INTERNAL));
+
+  ec_key = SubtleUtilBoringSSL::GetNewEcKey(EllipticCurveType::NIST_P384)
+                    .ValueOrDie();
+  EXPECT_THAT(EcdsaVerifyBoringSsl::New(ec_key, HashType::SHA256,
+                                      EcdsaSignatureEncoding::DER).status(),
+              StatusIs(util::error::INTERNAL));
+
+  ec_key = SubtleUtilBoringSSL::GetNewEcKey(EllipticCurveType::NIST_P521)
+                    .ValueOrDie();
+  EXPECT_THAT(EcdsaVerifyBoringSsl::New(ec_key, HashType::SHA256,
+                                      EcdsaSignatureEncoding::DER).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
 }  // namespace
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/subtle/ed25519_sign_boringssl.cc b/cc/subtle/ed25519_sign_boringssl.cc
index 637d17d..09603ff 100644
--- a/cc/subtle/ed25519_sign_boringssl.cc
+++ b/cc/subtle/ed25519_sign_boringssl.cc
@@ -34,6 +34,9 @@
 // static
 util::StatusOr<std::unique_ptr<PublicKeySign>> Ed25519SignBoringSsl::New(
     util::SecretData private_key) {
+  auto status = CheckFipsCompatibility<Ed25519SignBoringSsl>();
+  if (!status.ok()) return status;
+
   if (private_key.size() != ED25519_PRIVATE_KEY_LEN) {
     return util::Status(
         util::error::INVALID_ARGUMENT,
diff --git a/cc/subtle/ed25519_sign_boringssl.h b/cc/subtle/ed25519_sign_boringssl.h
index 89ba8ff..1dfbd70 100644
--- a/cc/subtle/ed25519_sign_boringssl.h
+++ b/cc/subtle/ed25519_sign_boringssl.h
@@ -20,6 +20,7 @@
 #include <memory>
 #include <utility>
 
+#include "tink/config/tink_fips.h"
 #include "tink/public_key_sign.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/statusor.h"
@@ -37,6 +38,9 @@
   crypto::tink::util::StatusOr<std::string> Sign(
       absl::string_view data) const override;
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kNotFips;
+
  private:
   explicit Ed25519SignBoringSsl(util::SecretData private_key)
       : private_key_(std::move(private_key)) {}
diff --git a/cc/subtle/ed25519_sign_boringssl_test.cc b/cc/subtle/ed25519_sign_boringssl_test.cc
index b89d8fa..bb55bb9 100644
--- a/cc/subtle/ed25519_sign_boringssl_test.cc
+++ b/cc/subtle/ed25519_sign_boringssl_test.cc
@@ -29,6 +29,7 @@
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 namespace crypto {
@@ -36,9 +37,15 @@
 namespace subtle {
 namespace {
 
+using ::crypto::tink::test::StatusIs;
+
 class Ed25519SignBoringSslTest : public ::testing::Test {};
 
 TEST_F(Ed25519SignBoringSslTest, testBasicSign) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips is false.";
+  }
+
   // Generate a new key pair.
   uint8_t out_public_key[ED25519_PUBLIC_KEY_LEN];
   uint8_t out_private_key[ED25519_PRIVATE_KEY_LEN];
@@ -89,6 +96,10 @@
 }
 
 TEST_F(Ed25519SignBoringSslTest, testInvalidPrivateKeys) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips is false.";
+  }
+
   for (int keysize = 0; keysize < 128; keysize++) {
     if (keysize == ED25519_PRIVATE_KEY_LEN) {
       // Valid key size.
@@ -100,6 +111,10 @@
 }
 
 TEST_F(Ed25519SignBoringSslTest, testMessageEmptyVersusNullStringView) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips is false.";
+  }
+
   // Generate a new key pair.
   uint8_t out_public_key[ED25519_PUBLIC_KEY_LEN];
   uint8_t out_private_key[ED25519_PRIVATE_KEY_LEN];
@@ -153,6 +168,10 @@
 };
 
 TEST_F(Ed25519SignBoringSslTest, testWithTestVectors) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips is false.";
+  }
+
   // These test vectors are taken from:
   // https://tools.ietf.org/html/draft-josefsson-eddsa-ed25519-02#section-6.
   TestVector ed25519_vectors[] = {
@@ -306,6 +325,21 @@
   }
 }
 
+TEST_F(Ed25519SignBoringSslTest, testFipsMode) {
+  if (!kUseOnlyFips) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips.";
+  }
+
+  // Generate a new key pair.
+  uint8_t out_public_key[ED25519_PUBLIC_KEY_LEN];
+  util::SecretData private_key(ED25519_PRIVATE_KEY_LEN);
+  ED25519_keypair(out_public_key, private_key.data());
+
+  // Create a new signer.
+  EXPECT_THAT(Ed25519SignBoringSsl::New(private_key).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
 }  // namespace
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/subtle/ed25519_verify_boringssl.cc b/cc/subtle/ed25519_verify_boringssl.cc
index 0be2dc3..549defc 100644
--- a/cc/subtle/ed25519_verify_boringssl.cc
+++ b/cc/subtle/ed25519_verify_boringssl.cc
@@ -32,6 +32,9 @@
 // static
 util::StatusOr<std::unique_ptr<PublicKeyVerify>> Ed25519VerifyBoringSsl::New(
     absl::string_view public_key) {
+  auto status = CheckFipsCompatibility<Ed25519VerifyBoringSsl>();
+  if (!status.ok()) return status;
+
   if (public_key.length() != ED25519_PUBLIC_KEY_LEN) {
     return util::Status(
         util::error::INVALID_ARGUMENT,
diff --git a/cc/subtle/ed25519_verify_boringssl.h b/cc/subtle/ed25519_verify_boringssl.h
index 8733446..a21d4d2 100644
--- a/cc/subtle/ed25519_verify_boringssl.h
+++ b/cc/subtle/ed25519_verify_boringssl.h
@@ -21,6 +21,7 @@
 #include <string>
 
 #include "absl/strings/string_view.h"
+#include "tink/config/tink_fips.h"
 #include "tink/public_key_verify.h"
 #include "tink/util/statusor.h"
 
@@ -37,6 +38,9 @@
   crypto::tink::util::Status Verify(absl::string_view signature,
                                     absl::string_view data) const override;
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kNotFips;
+
  private:
   const std::string public_key_;
 
diff --git a/cc/subtle/ed25519_verify_boringssl_test.cc b/cc/subtle/ed25519_verify_boringssl_test.cc
index 14fe22b..522f4e8 100644
--- a/cc/subtle/ed25519_verify_boringssl_test.cc
+++ b/cc/subtle/ed25519_verify_boringssl_test.cc
@@ -29,6 +29,7 @@
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 namespace crypto {
@@ -36,9 +37,16 @@
 namespace subtle {
 namespace {
 
+using ::crypto::tink::test::StatusIs;
+
 class Ed25519VerifyBoringSslTest : public ::testing::Test {};
 
 TEST_F(Ed25519VerifyBoringSslTest, testBasicSign) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips is false.";
+  }
+
   // Generate a new key pair.
   uint8_t out_public_key[ED25519_PUBLIC_KEY_LEN];
   uint8_t out_private_key[ED25519_PRIVATE_KEY_LEN];
@@ -76,6 +84,11 @@
 }
 
 TEST_F(Ed25519VerifyBoringSslTest, testInvalidPublicKeys) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips is false.";
+  }
+
   // Null public key.
   const absl::string_view null_public_key;
   EXPECT_FALSE(Ed25519VerifyBoringSsl::New(null_public_key).ok());
@@ -91,6 +104,11 @@
 }
 
 TEST_F(Ed25519VerifyBoringSslTest, testMessageEmptyVersusNullStringView) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips is false.";
+  }
+
   // Generate a new key pair.
   uint8_t out_public_key[ED25519_PUBLIC_KEY_LEN];
   uint8_t out_private_key[ED25519_PRIVATE_KEY_LEN];
@@ -215,9 +233,33 @@
 }
 
 TEST_F(Ed25519VerifyBoringSslTest, WycheproofCurve25519) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips is false.";
+  }
+
   ASSERT_TRUE(TestSignatures("eddsa_test.json", false));
 }
 
+TEST_F(Ed25519VerifyBoringSslTest, testFipsMode) {
+  if (!kUseOnlyFips) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips.";
+  }
+
+  // Generate a new key pair.
+  uint8_t out_public_key[ED25519_PUBLIC_KEY_LEN];
+  util::SecretData private_key(ED25519_PRIVATE_KEY_LEN);
+  ED25519_keypair(out_public_key, private_key.data());
+
+  std::string public_key(reinterpret_cast<const char *>(out_public_key),
+                         ED25519_PUBLIC_KEY_LEN);
+
+  // Create a new signer.
+  EXPECT_THAT(Ed25519VerifyBoringSsl::New(public_key).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
 }  // namespace
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/subtle/hmac_boringssl.cc b/cc/subtle/hmac_boringssl.cc
index 6fd6c64..7c1ff3e 100644
--- a/cc/subtle/hmac_boringssl.cc
+++ b/cc/subtle/hmac_boringssl.cc
@@ -40,6 +40,9 @@
 util::StatusOr<std::unique_ptr<Mac>> HmacBoringSsl::New(HashType hash_type,
                                                         uint32_t tag_size,
                                                         util::SecretData key) {
+  auto status = CheckFipsCompatibility<HmacBoringSsl>();
+  if (!status.ok()) return status;
+
   util::StatusOr<const EVP_MD*> res = SubtleUtilBoringSSL::EvpHash(hash_type);
   if (!res.ok()) {
     return res.status();
diff --git a/cc/subtle/hmac_boringssl.h b/cc/subtle/hmac_boringssl.h
index eca0a36..6b00aab 100644
--- a/cc/subtle/hmac_boringssl.h
+++ b/cc/subtle/hmac_boringssl.h
@@ -23,6 +23,7 @@
 #include "absl/strings/string_view.h"
 #include "openssl/evp.h"
 #include "tink/mac.h"
+#include "tink/config/tink_fips.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
@@ -47,6 +48,9 @@
       absl::string_view mac,
       absl::string_view data) const override;
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kRequiresBoringCrypto;
+
  private:
   // Minimum HMAC key size in bytes.
   static constexpr size_t kMinKeySize = 16;
diff --git a/cc/subtle/hmac_boringssl_test.cc b/cc/subtle/hmac_boringssl_test.cc
index 865cf1d..92d6525 100644
--- a/cc/subtle/hmac_boringssl_test.cc
+++ b/cc/subtle/hmac_boringssl_test.cc
@@ -20,10 +20,12 @@
 
 #include "gtest/gtest.h"
 #include "tink/mac.h"
+#include "tink/config/tink_fips.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/util/secret_data.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 namespace crypto {
@@ -31,6 +33,8 @@
 namespace subtle {
 namespace {
 
+using ::crypto::tink::test::StatusIs;
+
 class HmacBoringSslTest : public ::testing::Test {
  public:
   // Utility to simplify testing with test vectors.
@@ -51,6 +55,11 @@
 };
 
 TEST_F(HmacBoringSslTest, testBasic) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
+  }
+
   util::SecretData key = util::SecretDataFromStringView(
       test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
   size_t tag_size = 16;
@@ -82,6 +91,11 @@
 }
 
 TEST_F(HmacBoringSslTest, testModification) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
+  }
+
   util::SecretData key = util::SecretDataFromStringView(
       test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
   auto hmac_result = HmacBoringSsl::New(HashType::SHA1, 16, key);
@@ -102,6 +116,11 @@
 }
 
 TEST_F(HmacBoringSslTest, testTruncation) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
+  }
+
   util::SecretData key = util::SecretDataFromStringView(
       test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
   auto hmac_result = HmacBoringSsl::New(HashType::SHA1, 20, key);
@@ -120,6 +139,11 @@
 }
 
 TEST_F(HmacBoringSslTest, testInvalidKeySizes) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test should not run in FIPS mode when BoringCrypto is unavailable.";
+  }
+
   size_t tag_size = 16;
 
   for (int keysize = 0; keysize < 65; keysize++) {
@@ -133,6 +157,26 @@
   }
 }
 
+TEST_F(HmacBoringSslTest, TestFipsFailWithoutBoringCrypto) {
+  if (!kUseOnlyFips || FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+
+  util::SecretData key128 = util::SecretDataFromStringView(
+      test::HexDecodeOrDie("000102030405060708090a0b0c0d0e0f"));
+  util::SecretData key256 = util::SecretDataFromStringView(test::HexDecodeOrDie(
+      "000102030405060708090a0b0c0d0e0f000102030405060708090a0b0c0d0e0f"));
+
+  EXPECT_THAT(subtle::HmacBoringSsl::New(HashType::SHA1, 16, key128).status(),
+              StatusIs(util::error::INTERNAL));
+  EXPECT_THAT(subtle::HmacBoringSsl::New(HashType::SHA256, 16, key128).status(),
+              StatusIs(util::error::INTERNAL));
+  EXPECT_THAT(subtle::HmacBoringSsl::New(HashType::SHA384, 16, key128).status(),
+              StatusIs(util::error::INTERNAL));
+  EXPECT_THAT(subtle::HmacBoringSsl::New(HashType::SHA512, 16, key128).status(),
+              StatusIs(util::error::INTERNAL));
+}
 // TODO(bleichen): Stuff to test
 //  - Generate test vectors and share with Wycheproof.
 //  - Tag size wrong for construction
diff --git a/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.cc b/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.cc
index 2b22438..084bb0e 100644
--- a/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.cc
+++ b/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.cc
@@ -34,6 +34,9 @@
 util::StatusOr<std::unique_ptr<PublicKeySign>> RsaSsaPkcs1SignBoringSsl::New(
     const SubtleUtilBoringSSL::RsaPrivateKey& private_key,
     const SubtleUtilBoringSSL::RsaSsaPkcs1Params& params) {
+  auto status = CheckFipsCompatibility<RsaSsaPkcs1SignBoringSsl>();
+  if (!status.ok()) return status;
+
   // Check hash.
   util::Status sig_hash_valid =
       SubtleUtilBoringSSL::ValidateSignatureHash(params.hash_type);
diff --git a/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.h b/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.h
index 7f48a43..fab53de 100644
--- a/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.h
+++ b/cc/subtle/rsa_ssa_pkcs1_sign_boringssl.h
@@ -23,6 +23,7 @@
 #include "openssl/base.h"
 #include "openssl/ec.h"
 #include "openssl/rsa.h"
+#include "tink/config/tink_fips.h"
 #include "tink/public_key_sign.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/subtle/subtle_util_boringssl.h"
@@ -48,6 +49,9 @@
 
   ~RsaSsaPkcs1SignBoringSsl() override = default;
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kRequiresBoringCrypto;
+
  private:
   RsaSsaPkcs1SignBoringSsl(bssl::UniquePtr<RSA> private_key,
                            const EVP_MD* sig_hash)
diff --git a/cc/subtle/rsa_ssa_pkcs1_sign_boringssl_test.cc b/cc/subtle/rsa_ssa_pkcs1_sign_boringssl_test.cc
index 7ab3e3b..180541c 100644
--- a/cc/subtle/rsa_ssa_pkcs1_sign_boringssl_test.cc
+++ b/cc/subtle/rsa_ssa_pkcs1_sign_boringssl_test.cc
@@ -15,13 +15,16 @@
 ///////////////////////////////////////////////////////////////////////////////
 
 #include "tink/subtle/rsa_ssa_pkcs1_sign_boringssl.h"
+#include <cstdint>
 
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include "absl/strings/escaping.h"
 #include "openssl/base.h"
 #include "openssl/bn.h"
+#include "openssl/crypto.h"
 #include "openssl/rsa.h"
+#include "tink/config/tink_fips.h"
 #include "tink/subtle/rsa_ssa_pkcs1_verify_boringssl.h"
 #include "tink/subtle/subtle_util_boringssl.h"
 #include "tink/util/test_matchers.h"
@@ -30,6 +33,7 @@
 namespace tink {
 namespace subtle {
 namespace {
+
 using ::crypto::tink::test::IsOk;
 using ::crypto::tink::test::StatusIs;
 using ::testing::IsEmpty;
@@ -51,6 +55,10 @@
 };
 
 TEST_F(RsaPkcs1SignBoringsslTest, EncodesPkcs1) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaSsaPkcs1Params params{/*sig_hash=*/HashType::SHA256};
 
   auto signer_or = RsaSsaPkcs1SignBoringSsl::New(private_key_, params);
@@ -68,6 +76,10 @@
 }
 
 TEST_F(RsaPkcs1SignBoringsslTest, EncodesPkcs1WithSeparateHashes) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaSsaPkcs1Params params{/*sig_hash=*/HashType::SHA256};
 
   auto signer_or = RsaSsaPkcs1SignBoringSsl::New(private_key_, params);
@@ -85,12 +97,20 @@
 }
 
 TEST_F(RsaPkcs1SignBoringsslTest, RejectsUnsafeHash) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaSsaPkcs1Params params{/*sig_hash=*/HashType::SHA1};
   ASSERT_THAT(RsaSsaPkcs1SignBoringSsl::New(private_key_, params).status(),
               StatusIs(util::error::INVALID_ARGUMENT));
 }
 
 TEST_F(RsaPkcs1SignBoringsslTest, RejectsInvalidCrtParams) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaSsaPkcs1Params params{/*sig_hash=*/HashType::SHA256};
   ASSERT_THAT(private_key_.crt, Not(IsEmpty()));
   ASSERT_THAT(private_key_.dq, Not(IsEmpty()));
@@ -117,6 +137,52 @@
   }
 }
 
+// FIPS-only mode test
+TEST_F(RsaPkcs1SignBoringsslTest, TestFipsFailWithoutBoringCrypto) {
+  if (!kUseOnlyFips || FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+
+  SubtleUtilBoringSSL::RsaSsaPkcs1Params params{/*sig_hash=*/HashType::SHA256};
+  EXPECT_THAT(RsaSsaPkcs1SignBoringSsl::New(private_key_, params).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
+TEST_F(RsaPkcs1SignBoringsslTest, TestRestrictedFipsModuli) {
+  if (!kUseOnlyFips || !FIPS_mode()) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
+  }
+
+  SubtleUtilBoringSSL::RsaPrivateKey private_key;
+  SubtleUtilBoringSSL::RsaPublicKey public_key;
+
+  EXPECT_THAT(SubtleUtilBoringSSL::GetNewRsaKeyPair(
+                  4096, rsa_f4_.get(), &private_key, &public_key),
+              IsOk());
+
+  SubtleUtilBoringSSL::RsaSsaPkcs1Params params{/*sig_hash=*/HashType::SHA256};
+  EXPECT_THAT(RsaSsaPkcs1SignBoringSsl::New(private_key, params).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
+TEST_F(RsaPkcs1SignBoringsslTest, TestAllowedFipsModuli) {
+  if (!kUseOnlyFips || !FIPS_mode()) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
+  }
+
+  SubtleUtilBoringSSL::RsaPrivateKey private_key;
+  SubtleUtilBoringSSL::RsaPublicKey public_key;
+
+  EXPECT_THAT(SubtleUtilBoringSSL::GetNewRsaKeyPair(
+                  3072, rsa_f4_.get(), &private_key, &public_key),
+              IsOk());
+
+  SubtleUtilBoringSSL::RsaSsaPkcs1Params params{/*sig_hash=*/HashType::SHA256};
+  EXPECT_THAT(RsaSsaPkcs1SignBoringSsl::New(private_key, params).status(),
+              IsOk());
+}
+
 }  // namespace
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.cc b/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.cc
index 1e7991d..2add0b6 100644
--- a/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.cc
+++ b/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.cc
@@ -33,6 +33,9 @@
 RsaSsaPkcs1VerifyBoringSsl::New(
     const SubtleUtilBoringSSL::RsaPublicKey& pub_key,
     const SubtleUtilBoringSSL::RsaSsaPkcs1Params& params) {
+  auto status = CheckFipsCompatibility<RsaSsaPkcs1VerifyBoringSsl>();
+  if (!status.ok()) return status;
+
   // Check hash.
   auto hash_status =
       SubtleUtilBoringSSL::ValidateSignatureHash(params.hash_type);
diff --git a/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.h b/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.h
index 6a4efb0..0606258 100644
--- a/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.h
+++ b/cc/subtle/rsa_ssa_pkcs1_verify_boringssl.h
@@ -24,6 +24,7 @@
 #include "openssl/evp.h"
 #include "openssl/rsa.h"
 #include "tink/public_key_verify.h"
+#include "tink/config/tink_fips.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/subtle/subtle_util_boringssl.h"
 #include "tink/util/status.h"
@@ -49,6 +50,9 @@
 
   ~RsaSsaPkcs1VerifyBoringSsl() override = default;
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kRequiresBoringCrypto;
+
  private:
   // To reach 128-bit security strength, RSA's modulus must be at least 3072-bit
   // while 2048-bit RSA key only has 112-bit security. Nevertheless, a 2048-bit
diff --git a/cc/subtle/rsa_ssa_pkcs1_verify_boringssl_test.cc b/cc/subtle/rsa_ssa_pkcs1_verify_boringssl_test.cc
index 2adf912..6cae7e7 100644
--- a/cc/subtle/rsa_ssa_pkcs1_verify_boringssl_test.cc
+++ b/cc/subtle/rsa_ssa_pkcs1_verify_boringssl_test.cc
@@ -22,14 +22,17 @@
 #include "gtest/gtest.h"
 #include "absl/strings/escaping.h"
 #include "absl/strings/str_cat.h"
+#include "openssl/bn.h"
 #include "include/rapidjson/document.h"
 #include "tink/public_key_sign.h"
 #include "tink/public_key_verify.h"
+#include "tink/config/tink_fips.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/subtle/subtle_util_boringssl.h"
 #include "tink/subtle/wycheproof_util.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 namespace crypto {
@@ -37,6 +40,9 @@
 namespace subtle {
 namespace {
 
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+
 class RsaSsaPkcs1VerifyBoringSslTest : public ::testing::Test {};
 
 // Test vector from
@@ -77,6 +83,10 @@
     HashType::SHA256};
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, BasicVerify) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaPublicKey pub_key{nist_test_vector.n,
                                             nist_test_vector.e};
   SubtleUtilBoringSSL::RsaSsaPkcs1Params params{nist_test_vector.sig_hash};
@@ -90,6 +100,10 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, NewErrors) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaPublicKey nist_pub_key{nist_test_vector.n,
                                                  nist_test_vector.e};
   SubtleUtilBoringSSL::RsaSsaPkcs1Params nist_params{nist_test_vector.sig_hash};
@@ -118,6 +132,10 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, Modification) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaPublicKey pub_key{nist_test_vector.n,
                                             nist_test_vector.e};
   SubtleUtilBoringSSL::RsaSsaPkcs1Params params{nist_test_vector.sig_hash};
@@ -240,25 +258,99 @@
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, WycheproofRsaPkcs12048SHA256) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
   ASSERT_TRUE(TestSignatures("rsa_signature_2048_sha256_test.json",
                              /*allow_skipping=*/false));
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, WycheproofRsaPkcs13072SHA256) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   ASSERT_TRUE(TestSignatures("rsa_signature_3072_sha256_test.json",
                              /*allow_skipping=*/false));
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, WycheproofRsaPkcs13072SHA512) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   ASSERT_TRUE(TestSignatures("rsa_signature_3072_sha512_test.json",
                              /*allow_skipping=*/false));
 }
 
 TEST_F(RsaSsaPkcs1VerifyBoringSslTest, WycheproofRsaPkcs14096SHA512) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
   ASSERT_TRUE(TestSignatures("rsa_signature_4096_sha512_test.json",
                              /*allow_skipping=*/false));
 }
 
+// FIPS-only mode test
+TEST_F(RsaSsaPkcs1VerifyBoringSslTest, TestFipsFailWithoutBoringCrypto) {
+  if (!kUseOnlyFips || FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+
+  SubtleUtilBoringSSL::RsaPublicKey pub_key{nist_test_vector.n,
+                                            nist_test_vector.e};
+  SubtleUtilBoringSSL::RsaSsaPkcs1Params params{/*sig_hash=*/HashType::SHA256};
+  EXPECT_THAT(RsaSsaPkcs1VerifyBoringSsl::New(pub_key, params).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
+TEST_F(RsaSsaPkcs1VerifyBoringSslTest, TestAllowedFipsModuli) {
+  if (!kUseOnlyFips || !FIPS_mode()) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
+  }
+
+  bssl::UniquePtr<BIGNUM> rsa_f4(BN_new());
+  SubtleUtilBoringSSL::RsaPrivateKey private_key;
+  SubtleUtilBoringSSL::RsaPublicKey public_key;
+  BN_set_u64(rsa_f4.get(), RSA_F4);
+
+  EXPECT_THAT(SubtleUtilBoringSSL::GetNewRsaKeyPair(
+                  3072, rsa_f4.get(), &private_key, &public_key),
+              IsOk());
+
+  SubtleUtilBoringSSL::RsaSsaPkcs1Params params{/*sig_hash=*/HashType::SHA256};
+  EXPECT_THAT(RsaSsaPkcs1VerifyBoringSsl::New(public_key, params).status(),
+              IsOk());
+}
+
+TEST_F(RsaSsaPkcs1VerifyBoringSslTest, TestRestrictedFipsModuli) {
+  if (!kUseOnlyFips || !FIPS_mode()) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
+  }
+
+  bssl::UniquePtr<BIGNUM> rsa_f4(BN_new());
+  SubtleUtilBoringSSL::RsaPrivateKey private_key;
+  SubtleUtilBoringSSL::RsaPublicKey public_key;
+  SubtleUtilBoringSSL::RsaSsaPkcs1Params params{/*sig_hash=*/HashType::SHA256};
+  BN_set_u64(rsa_f4.get(), RSA_F4);
+
+
+  EXPECT_THAT(SubtleUtilBoringSSL::GetNewRsaKeyPair(
+                  2048, rsa_f4.get(), &private_key, &public_key),
+              IsOk());
+
+  EXPECT_THAT(RsaSsaPkcs1VerifyBoringSsl::New(public_key, params).status(),
+              StatusIs(util::error::INTERNAL));
+
+  EXPECT_THAT(SubtleUtilBoringSSL::GetNewRsaKeyPair(
+                  4096, rsa_f4.get(), &private_key, &public_key),
+              IsOk());
+
+  EXPECT_THAT(RsaSsaPkcs1VerifyBoringSsl::New(public_key, params).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
 }  // namespace
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/subtle/rsa_ssa_pss_sign_boringssl.cc b/cc/subtle/rsa_ssa_pss_sign_boringssl.cc
index 13045f2..996cbe4 100644
--- a/cc/subtle/rsa_ssa_pss_sign_boringssl.cc
+++ b/cc/subtle/rsa_ssa_pss_sign_boringssl.cc
@@ -33,6 +33,9 @@
 util::StatusOr<std::unique_ptr<PublicKeySign>> RsaSsaPssSignBoringSsl::New(
     const SubtleUtilBoringSSL::RsaPrivateKey& private_key,
     const SubtleUtilBoringSSL::RsaSsaPssParams& params) {
+  auto status = CheckFipsCompatibility<RsaSsaPssSignBoringSsl>();
+  if (!status.ok()) return status;
+
   // Check hash.
   util::Status sig_hash_valid =
       SubtleUtilBoringSSL::ValidateSignatureHash(params.sig_hash);
diff --git a/cc/subtle/rsa_ssa_pss_sign_boringssl.h b/cc/subtle/rsa_ssa_pss_sign_boringssl.h
index 1ae374b..01cb972 100644
--- a/cc/subtle/rsa_ssa_pss_sign_boringssl.h
+++ b/cc/subtle/rsa_ssa_pss_sign_boringssl.h
@@ -23,6 +23,7 @@
 #include "openssl/base.h"
 #include "openssl/ec.h"
 #include "openssl/rsa.h"
+#include "tink/config/tink_fips.h"
 #include "tink/public_key_sign.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/subtle/subtle_util_boringssl.h"
@@ -48,6 +49,9 @@
 
   ~RsaSsaPssSignBoringSsl() override = default;
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kRequiresBoringCrypto;
+
  private:
   const bssl::UniquePtr<RSA> private_key_;
   const EVP_MD* sig_hash_;   // Owned by BoringSSL.
diff --git a/cc/subtle/rsa_ssa_pss_sign_boringssl_test.cc b/cc/subtle/rsa_ssa_pss_sign_boringssl_test.cc
index ffcdb27..1e3850d 100644
--- a/cc/subtle/rsa_ssa_pss_sign_boringssl_test.cc
+++ b/cc/subtle/rsa_ssa_pss_sign_boringssl_test.cc
@@ -30,6 +30,7 @@
 namespace tink {
 namespace subtle {
 namespace {
+
 using ::crypto::tink::test::IsOk;
 using ::crypto::tink::test::StatusIs;
 using ::testing::IsEmpty;
@@ -51,6 +52,10 @@
 };
 
 TEST_F(RsaPssSignBoringsslTest, EncodesPss) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaSsaPssParams params{/*sig_hash=*/HashType::SHA256,
                                               /*mgf1_hash=*/HashType::SHA256,
                                               /*salt_length=*/32};
@@ -70,6 +75,10 @@
 }
 
 TEST_F(RsaPssSignBoringsslTest, EncodesPssWithSeparateHashes) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaSsaPssParams params{/*sig_hash=*/HashType::SHA256,
                                               /*mgf1_hash=*/HashType::SHA1,
                                               /*salt_length=*/32};
@@ -89,6 +98,10 @@
 }
 
 TEST_F(RsaPssSignBoringsslTest, RejectsInvalidPaddingHash) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaSsaPssParams params{
       /*sig_hash=*/HashType::SHA256, /*mgf1_hash=*/HashType::UNKNOWN_HASH,
       /*salt_length=*/0};
@@ -97,6 +110,10 @@
 }
 
 TEST_F(RsaPssSignBoringsslTest, RejectsUnsafePaddingHash) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaSsaPssParams params{/*sig_hash=*/HashType::SHA1,
                                               /*mgf1_hash=*/HashType::SHA1,
                                               /*salt_length=*/0};
@@ -105,6 +122,10 @@
 }
 
 TEST_F(RsaPssSignBoringsslTest, RejectsInvalidCrtParams) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
+
   SubtleUtilBoringSSL::RsaSsaPssParams params{/*sig_hash=*/HashType::SHA256,
                                               /*mgf1_hash=*/HashType::SHA256,
                                               /*salt_length=*/32};
@@ -133,6 +154,56 @@
   }
 }
 
+// FIPS-only mode test
+TEST_F(RsaPssSignBoringsslTest, TestFipsFailWithoutBoringCrypto) {
+  if (!kUseOnlyFips || FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+
+  SubtleUtilBoringSSL::RsaSsaPssParams params{/*sig_hash=*/HashType::SHA256,
+                                              /*mgf1_hash=*/HashType::SHA256,
+                                              /*salt_length=*/32};
+  EXPECT_THAT(RsaSsaPssSignBoringSsl::New(private_key_, params).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
+TEST_F(RsaPssSignBoringsslTest, TestRestrictedFipsModuli) {
+  if (!kUseOnlyFips || !FIPS_mode()) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
+  }
+  SubtleUtilBoringSSL::RsaPrivateKey private_key;
+  SubtleUtilBoringSSL::RsaPublicKey public_key;
+
+  EXPECT_THAT(SubtleUtilBoringSSL::GetNewRsaKeyPair(
+                  4096, rsa_f4_.get(), &private_key, &public_key),
+              IsOk());
+
+  SubtleUtilBoringSSL::RsaSsaPssParams params{/*sig_hash=*/HashType::SHA256,
+                                              /*mgf1_hash=*/HashType::SHA256,
+                                              /*salt_length=*/32};
+  EXPECT_THAT(RsaSsaPssSignBoringSsl::New(private_key, params).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
+TEST_F(RsaPssSignBoringsslTest, TestAllowedFipsModuli) {
+  if (!kUseOnlyFips || !FIPS_mode()) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
+  }
+  SubtleUtilBoringSSL::RsaPrivateKey private_key;
+  SubtleUtilBoringSSL::RsaPublicKey public_key;
+
+  EXPECT_THAT(SubtleUtilBoringSSL::GetNewRsaKeyPair(
+                  3072, rsa_f4_.get(), &private_key, &public_key),
+              IsOk());
+
+  SubtleUtilBoringSSL::RsaSsaPssParams params{/*sig_hash=*/HashType::SHA256,
+                                              /*mgf1_hash=*/HashType::SHA256,
+                                              /*salt_length=*/32};
+  EXPECT_THAT(RsaSsaPssSignBoringSsl::New(private_key, params).status(),
+              IsOk());
+}
+
 }  // namespace
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/subtle/rsa_ssa_pss_verify_boringssl.cc b/cc/subtle/rsa_ssa_pss_verify_boringssl.cc
index 3f08ee7..eca04cf 100644
--- a/cc/subtle/rsa_ssa_pss_verify_boringssl.cc
+++ b/cc/subtle/rsa_ssa_pss_verify_boringssl.cc
@@ -32,6 +32,9 @@
 RsaSsaPssVerifyBoringSsl::New(
     const SubtleUtilBoringSSL::RsaPublicKey& pub_key,
     const SubtleUtilBoringSSL::RsaSsaPssParams& params) {
+  auto status = CheckFipsCompatibility<RsaSsaPssVerifyBoringSsl>();
+  if (!status.ok()) return status;
+
   // Check hash.
   auto hash_status =
       SubtleUtilBoringSSL::ValidateSignatureHash(params.sig_hash);
diff --git a/cc/subtle/rsa_ssa_pss_verify_boringssl.h b/cc/subtle/rsa_ssa_pss_verify_boringssl.h
index 6b3b4a7..2f472dd 100644
--- a/cc/subtle/rsa_ssa_pss_verify_boringssl.h
+++ b/cc/subtle/rsa_ssa_pss_verify_boringssl.h
@@ -24,6 +24,7 @@
 #include "openssl/evp.h"
 #include "openssl/rsa.h"
 #include "tink/public_key_verify.h"
+#include "tink/config/tink_fips.h"
 #include "tink/subtle/common_enums.h"
 #include "tink/subtle/subtle_util_boringssl.h"
 #include "tink/util/status.h"
@@ -48,6 +49,9 @@
 
   ~RsaSsaPssVerifyBoringSsl() override = default;
 
+  static constexpr crypto::tink::FipsCompatibility kFipsStatus =
+      crypto::tink::FipsCompatibility::kRequiresBoringCrypto;
+
  private:
   RsaSsaPssVerifyBoringSsl(bssl::UniquePtr<RSA> rsa, const EVP_MD* sig_hash,
                            const EVP_MD* mgf1_hash, int salt_length)
diff --git a/cc/subtle/rsa_ssa_pss_verify_boringssl_test.cc b/cc/subtle/rsa_ssa_pss_verify_boringssl_test.cc
index cdd8ba8..60d885c 100644
--- a/cc/subtle/rsa_ssa_pss_verify_boringssl_test.cc
+++ b/cc/subtle/rsa_ssa_pss_verify_boringssl_test.cc
@@ -29,6 +29,7 @@
 #include "tink/subtle/wycheproof_util.h"
 #include "tink/util/status.h"
 #include "tink/util/statusor.h"
+#include "tink/util/test_matchers.h"
 #include "tink/util/test_util.h"
 
 // TODO(quannguyen):
@@ -38,6 +39,9 @@
 namespace subtle {
 namespace {
 
+using ::crypto::tink::test::IsOk;
+using ::crypto::tink::test::StatusIs;
+
 class RsaSsaPssVerifyBoringSslTest : public ::testing::Test {};
 
 // Test vector from
@@ -82,6 +86,9 @@
     32};
 
 TEST_F(RsaSsaPssVerifyBoringSslTest, BasicVerify) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
   SubtleUtilBoringSSL::RsaPublicKey pub_key{nist_test_vector.n,
                                             nist_test_vector.e};
   SubtleUtilBoringSSL::RsaSsaPssParams params{nist_test_vector.sig_hash,
@@ -97,6 +104,9 @@
 }
 
 TEST_F(RsaSsaPssVerifyBoringSslTest, NewErrors) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
   SubtleUtilBoringSSL::RsaPublicKey nist_pub_key{nist_test_vector.n,
                                                  nist_test_vector.e};
   SubtleUtilBoringSSL::RsaSsaPssParams nist_params{
@@ -127,6 +137,9 @@
 }
 
 TEST_F(RsaSsaPssVerifyBoringSslTest, Modification) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
   SubtleUtilBoringSSL::RsaPublicKey pub_key{nist_test_vector.n,
                                             nist_test_vector.e};
   SubtleUtilBoringSSL::RsaSsaPssParams params{nist_test_vector.sig_hash,
@@ -252,30 +265,112 @@
 }
 
 TEST_F(RsaSsaPssVerifyBoringSslTest, WycheproofRsaPss2048Sha2560) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
   ASSERT_TRUE(TestSignatures("rsa_pss_2048_sha256_mgf1_0_test.json",
                              /*allow_skipping=*/false));
 }
 
 TEST_F(RsaSsaPssVerifyBoringSslTest, WycheproofRsaPss2048Sha25632) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
   ASSERT_TRUE(TestSignatures("rsa_pss_2048_sha256_mgf1_32_test.json",
                              /*allow_skipping=*/false));
 }
 
 TEST_F(RsaSsaPssVerifyBoringSslTest, WycheproofRsaPss3072Sha25632) {
+  if (kUseOnlyFips && !FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test is skipped if kOnlyUseFips but BoringCrypto is unavailable.";
+  }
   ASSERT_TRUE(TestSignatures("rsa_pss_3072_sha256_mgf1_32_test.json",
                              /*allow_skipping=*/false));
 }
 
 TEST_F(RsaSsaPssVerifyBoringSslTest, WycheproofRsaPss4096Sha25632) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
   ASSERT_TRUE(TestSignatures("rsa_pss_4096_sha256_mgf1_32_test.json",
                              /*allow_skipping=*/false));
 }
 
 TEST_F(RsaSsaPssVerifyBoringSslTest, WycheproofRsaPss4096Sha51232) {
+  if (kUseOnlyFips) {
+    GTEST_SKIP() << "Test not run in FIPS-only mode";
+  }
   ASSERT_TRUE(TestSignatures("rsa_pss_4096_sha512_mgf1_32_test.json",
                              /*allow_skipping=*/false));
 }
 
+// FIPS-only mode test
+TEST_F(RsaSsaPssVerifyBoringSslTest, TestFipsFailWithoutBoringCrypto) {
+  if (!kUseOnlyFips || FIPS_mode()) {
+    GTEST_SKIP()
+        << "Test assumes kOnlyUseFips but BoringCrypto is unavailable.";
+  }
+
+  SubtleUtilBoringSSL::RsaPublicKey pub_key{nist_test_vector.n,
+                                            nist_test_vector.e};
+  SubtleUtilBoringSSL::RsaSsaPssParams params{/*sig_hash=*/HashType::SHA256,
+                                              /*mgf1_hash=*/HashType::SHA256,
+                                              /*salt_length=*/32};
+  EXPECT_THAT(RsaSsaPssVerifyBoringSsl::New(pub_key, params).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
+TEST_F(RsaSsaPssVerifyBoringSslTest, TestAllowedFipsModuli) {
+  if (!kUseOnlyFips || !FIPS_mode()) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
+  }
+
+  bssl::UniquePtr<BIGNUM> rsa_f4(BN_new());
+  SubtleUtilBoringSSL::RsaPrivateKey private_key;
+  SubtleUtilBoringSSL::RsaPublicKey public_key;
+  SubtleUtilBoringSSL::RsaSsaPssParams params{/*sig_hash=*/HashType::SHA256,
+                                              /*mgf1_hash=*/HashType::SHA256,
+                                              /*salt_length=*/32};
+  BN_set_u64(rsa_f4.get(), RSA_F4);
+
+  EXPECT_THAT(SubtleUtilBoringSSL::GetNewRsaKeyPair(
+                  3072, rsa_f4.get(), &private_key, &public_key),
+              IsOk());
+
+  EXPECT_THAT(RsaSsaPssVerifyBoringSsl::New(public_key, params).status(),
+              IsOk());
+}
+
+TEST_F(RsaSsaPssVerifyBoringSslTest, TestRestrictedFipsModuli) {
+  if (!kUseOnlyFips || !FIPS_mode()) {
+    GTEST_SKIP() << "Test assumes kOnlyUseFips and BoringCrypto.";
+  }
+
+  bssl::UniquePtr<BIGNUM> rsa_f4(BN_new());
+  SubtleUtilBoringSSL::RsaPrivateKey private_key;
+  SubtleUtilBoringSSL::RsaPublicKey public_key;
+  SubtleUtilBoringSSL::RsaSsaPssParams params{/*sig_hash=*/HashType::SHA256,
+                                              /*mgf1_hash=*/HashType::SHA256,
+                                              /*salt_length=*/32};
+  BN_set_u64(rsa_f4.get(), RSA_F4);
+
+
+  EXPECT_THAT(SubtleUtilBoringSSL::GetNewRsaKeyPair(
+                  2048, rsa_f4.get(), &private_key, &public_key),
+              IsOk());
+
+  EXPECT_THAT(RsaSsaPssVerifyBoringSsl::New(public_key, params).status(),
+              StatusIs(util::error::INTERNAL));
+
+  EXPECT_THAT(SubtleUtilBoringSSL::GetNewRsaKeyPair(
+                  4096, rsa_f4.get(), &private_key, &public_key),
+              IsOk());
+
+  EXPECT_THAT(RsaSsaPssVerifyBoringSsl::New(public_key, params).status(),
+              StatusIs(util::error::INTERNAL));
+}
+
 }  // namespace
 }  // namespace subtle
 }  // namespace tink
diff --git a/cc/version_script.lds b/cc/version_script.lds
index 0f1e264..5f1ad06 100644
--- a/cc/version_script.lds
+++ b/cc/version_script.lds
@@ -1,4 +1,4 @@
-VERS_1.4.0-rc2 {
+VERS_1.4.0 {
   global:
     *tink*;
     *absl*;
diff --git a/docs/JAVA-HOWTO.md b/docs/JAVA-HOWTO.md
index dbc71d1..6e106d1 100644
--- a/docs/JAVA-HOWTO.md
+++ b/docs/JAVA-HOWTO.md
@@ -9,8 +9,8 @@
 ## Setup instructions
 
 The most recent release is
-[1.4.0-rc2](https://github.com/google/tink/releases/tag/v1.4.0-rc2), released
-2020-05-14.
+[1.4.0](https://github.com/google/tink/releases/tag/v1.4.0), released
+2020-07-13.
 
 In addition to the versioned releases, snapshots of Tink are regurlarly built
 using the master branch of the Tink GitHub repository.
@@ -35,7 +35,7 @@
   <dependency>
     <groupId>com.google.crypto.tink</groupId>
     <artifactId>tink</artifactId>
-    <version>1.4.0-rc2</version>
+    <version>1.4.0</version>
   </dependency>
 </dependencies>
 ```
@@ -72,16 +72,16 @@
 
 ### AWS/GCP integration
 
-Since 1.3.0-rc2 the support for AWS/GCP KMS has been moved to a separate
-package. To use AWS KMS, one should also add dependency on `tink-awskms`, and
-similarly `tink-gcpkms` for GCP KMS.
+Since 1.3.0 the support for AWS/GCP KMS has been moved to a separate package. To
+use AWS KMS, one should also add dependency on `tink-awskms`, and similarly
+`tink-gcpkms` for GCP KMS.
 
 ```xml
 <dependencies>
   <dependency>
     <groupId>com.google.crypto.tink</groupId>
     <artifactId>tink-awskms</artifactId>
-    <version>1.4.0-rc2</version>
+    <version>1.4.0</version>
   </dependency>
 </dependencies>
 ```
@@ -91,7 +91,7 @@
   <dependency>
     <groupId>com.google.crypto.tink</groupId>
     <artifactId>tink-gcpkms</artifactId>
-    <version>1.4.0-rc2</version>
+    <version>1.4.0</version>
   </dependency>
 </dependencies>
 ```
@@ -105,7 +105,7 @@
 
 ```
 dependencies {
-  implementation 'com.google.crypto.tink:tink-android:1.4.0-rc2'
+  implementation 'com.google.crypto.tink:tink-android:1.4.0'
 }
 ```
 
@@ -125,10 +125,10 @@
 ## API documentation
 
 *   Java:
-    *   [1.4.0-rc2](https://google.github.com/tink/javadoc/tink/1.4.0-rc2)
+    *   [1.4.0](https://google.github.com/tink/javadoc/tink/1.4.0)
     *   [HEAD-SNAPSHOT](https://google.github.com/tink/javadoc/tink/HEAD-SNAPSHOT)
 *   Android:
-    *   [1.4.0-rc2](https://google.github.com/tink/javadoc/tink-android/1.4.0-rc2)
+    *   [1.4.0](https://google.github.com/tink/javadoc/tink-android/1.4.0)
     *   [HEAD-SNAPSHOT](https://google.github.com/tink/javadoc/tink-android/HEAD-SNAPSHOT)
 
 ## Important warnings
diff --git a/docs/KNOWN-ISSUES.md b/docs/KNOWN-ISSUES.md
index 5b87a20..24ed73a 100644
--- a/docs/KNOWN-ISSUES.md
+++ b/docs/KNOWN-ISSUES.md
@@ -10,10 +10,13 @@
     subtle implementation may be vulnerable to chosen-ciphertext attacks. An
     attacker can generate ciphertexts that bypass the HMAC verification if and
     only if all of the following conditions are true:
+
     -   Tink C++ is used on systems where `size_t` is a 32-bit integer. This is
         usually the case on 32-bit machines.
     -   The attacker can specify long (>= 2^29 bytes ~ 536MB) associated data.
 
+    This issue was reported by Quan Nguyen of Snap security team.
+
 ## Java
 
 *   Tink supports Java 7 or newer. Please file a ticket if you want to support
@@ -75,3 +78,12 @@
     this violates the CCA2 property for this interface, although the ciphertext
     will still decrypt to the correct DEK. When using this interface one should
     not rely on that for each DEK there only exists a single *encrypted DEK*.
+
+## Streaming AEAD - potential integer overflow issues
+
+*   Streaming AEAD implementations encrypt the plaintext in segments. Tink uses
+    a 4-byte segment counter. When encrypting a stream consisting of more than
+    2^32 segments, the segment counter might overflow and lead to leakage of key
+    material or plaintext. This problem was found in the Java and Go
+    implementations of the AES-GCM-HKDF-Streaming key type, and has been fixed
+    since 1.4.0.
diff --git a/go/integration/hcvault/go.mod b/go/integration/hcvault/go.mod
index 6ae0e01..9c53ebc 100644
--- a/go/integration/hcvault/go.mod
+++ b/go/integration/hcvault/go.mod
@@ -3,6 +3,6 @@
 go 1.12
 
 require (
-  github.com/google/tink/go v1.4.0-rc2
+  github.com/google/tink/go v1.4.0
   github.com/hashicorp/vault/api v1.0.4
 )
diff --git a/go/testutil/testutil.go b/go/testutil/testutil.go
index 004dab8..0473e25 100644
--- a/go/testutil/testutil.go
+++ b/go/testutil/testutil.go
@@ -26,6 +26,7 @@
 	"golang.org/x/crypto/ed25519"
 	"github.com/golang/protobuf/proto"
 	"github.com/google/tink/go/core/registry"
+	subtledaead "github.com/google/tink/go/daead/subtle"
 	subtlehybrid "github.com/google/tink/go/hybrid/subtle"
 	"github.com/google/tink/go/keyset"
 	"github.com/google/tink/go/mac"
@@ -33,7 +34,6 @@
 	"github.com/google/tink/go/subtle"
 	"github.com/google/tink/go/tink"
 
-	subtedaead "github.com/google/tink/go/daead/subtle"
 	cmacpb "github.com/google/tink/go/proto/aes_cmac_go_proto"
 	aescmacprfpb "github.com/google/tink/go/proto/aes_cmac_prf_go_proto"
 	gcmpb "github.com/google/tink/go/proto/aes_gcm_go_proto"
@@ -146,7 +146,7 @@
 
 // NewTestAESSIVKeyset creates a new Keyset containing an AesSivKey.
 func NewTestAESSIVKeyset(primaryOutputPrefixType tinkpb.OutputPrefixType) *tinkpb.Keyset {
-	keyValue := random.GetRandomBytes(subtedaead.AESSIVKeySize)
+	keyValue := random.GetRandomBytes(subtledaead.AESSIVKeySize)
 	key := &aspb.AesSivKey{
 		Version:  AESSIVKeyVersion,
 		KeyValue: keyValue,
diff --git a/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeysetManager.java b/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeysetManager.java
index 4227d78..85ad893 100644
--- a/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeysetManager.java
+++ b/java_src/src/main/java/com/google/crypto/tink/integration/android/AndroidKeysetManager.java
@@ -105,27 +105,29 @@
  * generated. If the master key already exists but is unusable, a {@link KeyStoreException} is
  * thrown.
  *
+ * <p>This class is thread-safe.
+ *
  * @since 1.0.0
  */
 public final class AndroidKeysetManager {
   private static final String TAG = AndroidKeysetManager.class.getSimpleName();
-  private final KeysetReader reader;
   private final KeysetWriter writer;
   private final Aead masterKey;
-  private final KeyTemplate keyTemplate;
 
   @GuardedBy("this")
   private KeysetManager keysetManager;
 
   private AndroidKeysetManager(Builder builder) throws GeneralSecurityException, IOException {
-    reader = builder.reader;
     writer = builder.writer;
     masterKey = builder.masterKey;
-    keyTemplate = builder.keyTemplate;
-    keysetManager = readOrGenerateNewKeyset();
+    keysetManager = builder.keysetManager;
   }
 
-  /** A builder for {@link AndroidKeysetManager}. */
+  /**
+   * A builder for {@link AndroidKeysetManager}.
+   *
+   * <p>This class is thread-safe.
+   */
   public static final class Builder {
     private KeysetReader reader = null;
     private KeysetWriter writer = null;
@@ -135,6 +137,9 @@
     private KeyTemplate keyTemplate = null;
     private KeyStore keyStore = null;
 
+    @GuardedBy("this")
+    private KeysetManager keysetManager;
+
     public Builder() {}
 
     /** Reads and writes the keyset from shared preferences. */
@@ -218,12 +223,103 @@
      * @throws KeystoreException If a master key is found but unusable.
      * @throws GeneralSecurityException If cannot read an existing keyset or generate a new one.
      */
-    public AndroidKeysetManager build() throws GeneralSecurityException, IOException {
+    public synchronized AndroidKeysetManager build() throws GeneralSecurityException, IOException {
       if (masterKeyUri != null) {
-        masterKey = readOrGenerateNewMasterKey(masterKeyUri, keyStore);
+        masterKey = readOrGenerateNewMasterKey();
       }
+      this.keysetManager = readOrGenerateNewKeyset();
+
       return new AndroidKeysetManager(this);
     }
+
+    private Aead readOrGenerateNewMasterKey() throws GeneralSecurityException {
+      if (!isAtLeastM()) {
+        Log.w(TAG, "Android Keystore requires at least Android M");
+        return null;
+      }
+
+      AndroidKeystoreKmsClient client;
+      if (keyStore != null) {
+        client = new AndroidKeystoreKmsClient.Builder().setKeyStore(keyStore).build();
+      } else {
+        client = new AndroidKeystoreKmsClient();
+      }
+
+      boolean existed = client.hasKey(masterKeyUri);
+      if (!existed) {
+        try {
+          AndroidKeystoreKmsClient.generateNewAeadKey(masterKeyUri);
+        } catch (GeneralSecurityException ex) {
+          Log.w(TAG, "cannot use Android Keystore, it'll be disabled", ex);
+          return null;
+        }
+      }
+
+      try {
+        return client.getAead(masterKeyUri);
+      } catch (GeneralSecurityException | ProviderException ex) {
+        // Throw the exception if the key exists but is unusable. We can't recover by generating a
+        // new
+        // key because there might be existing encrypted data under the unusable key.
+        // Users can provide a master key that is stored in StrongBox, which may throw a
+        // ProviderException if there's any problem with it.
+        if (existed) {
+          throw new KeyStoreException(
+              String.format("the master key %s exists but is unusable", masterKeyUri), ex);
+        }
+        // Otherwise swallow the exception if the key doesn't exist yet. We can recover by disabling
+        // Keystore.
+        Log.w(TAG, "cannot use Android Keystore, it'll be disabled", ex);
+      }
+
+      return null;
+    }
+
+    private KeysetManager readOrGenerateNewKeyset() throws GeneralSecurityException, IOException {
+      try {
+        return read();
+      } catch (FileNotFoundException ex) {
+        // Not found, handle below.
+        Log.w(TAG, "keyset not found, will generate a new one", ex);
+      }
+
+      // Not found.
+      if (keyTemplate != null) {
+        KeysetManager manager = KeysetManager.withEmptyKeyset().add(keyTemplate);
+        int keyId = manager.getKeysetHandle().getKeysetInfo().getKeyInfo(0).getKeyId();
+        manager = manager.setPrimary(keyId);
+        if (masterKey != null) {
+          manager.getKeysetHandle().write(writer, masterKey);
+        } else {
+          CleartextKeysetHandle.write(manager.getKeysetHandle(), writer);
+        }
+        return manager;
+      }
+      throw new GeneralSecurityException("cannot read or generate keyset");
+    }
+
+    private KeysetManager read() throws GeneralSecurityException, IOException {
+      if (masterKey != null) {
+        try {
+          return KeysetManager.withKeysetHandle(KeysetHandle.read(reader, masterKey));
+        } catch (InvalidProtocolBufferException | GeneralSecurityException ex) {
+          // Swallow the exception and attempt to read the keyset in cleartext.
+          // This edge case may happen when either
+          //   - the keyset was generated on a pre M phone which is then upgraded to M or newer, or
+          //   - the keyset was generated with Keystore being disabled, then Keystore is enabled.
+          // By ignoring the security failure here, an adversary with write access to private
+          // preferences can replace an encrypted keyset (that it cannot read or write) with a
+          // cleartext value that it controls. This does not introduce new security risks because to
+          // overwrite the encrypted keyset in private preferences of an app, said adversaries must
+          // have the same privilege as the app, thus they can call Android Keystore to read or
+          // write
+          // the encrypted keyset in the first place.
+          Log.w(TAG, "cannot decrypt keyset: ", ex);
+        }
+      }
+
+      return KeysetManager.withKeysetHandle(CleartextKeysetHandle.read(reader));
+    }
   }
 
   /** @return a {@link KeysetHandle} of the managed keyset */
@@ -351,47 +447,6 @@
     return shouldUseKeystore();
   }
 
-  private KeysetManager readOrGenerateNewKeyset() throws GeneralSecurityException, IOException {
-    try {
-      return read();
-    } catch (FileNotFoundException ex) {
-      // Not found, handle below.
-      Log.w(TAG, "keyset not found, will generate a new one", ex);
-    }
-
-    // Not found.
-    if (keyTemplate != null) {
-      KeysetManager manager = KeysetManager.withEmptyKeyset().add(keyTemplate);
-      int keyId = manager.getKeysetHandle().getKeysetInfo().getKeyInfo(0).getKeyId();
-      manager = manager.setPrimary(keyId);
-      write(manager);
-      return manager;
-    }
-    throw new GeneralSecurityException("cannot read or generate keyset");
-  }
-
-  private KeysetManager read() throws GeneralSecurityException, IOException {
-    if (shouldUseKeystore()) {
-      try {
-        return KeysetManager.withKeysetHandle(KeysetHandle.read(reader, masterKey));
-      } catch (InvalidProtocolBufferException | GeneralSecurityException ex) {
-        // Swallow the exception and attempt to read the keyset in cleartext.
-        // This edge case may happen when either
-        //   - the keyset was generated on a pre M phone which is then upgraded to M or newer, or
-        //   - the keyset was generated with Keystore being disabled, then Keystore is enabled.
-        // By ignoring the security failure here, an adversary with write access to private
-        // preferences can replace an encrypted keyset (that it cannot read or write) with a
-        // cleartext value that it controls. This does not introduce new security risks because to
-        // overwrite the encrypted keyset in private preferences of an app, said adversaries must
-        // have the same privilege as the app, thus they can call Android Keystore to read or write
-        // the encrypted keyset in the first place.
-        Log.w(TAG, "cannot decrypt keyset: ", ex);
-      }
-    }
-
-    return KeysetManager.withKeysetHandle(CleartextKeysetHandle.read(reader));
-  }
-
   private void write(KeysetManager manager) throws GeneralSecurityException {
     try {
       if (shouldUseKeystore()) {
@@ -426,47 +481,4 @@
   private static boolean isAtLeastM() {
     return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M;
   }
-
-  private static Aead readOrGenerateNewMasterKey(String keyId, KeyStore keyStore)
-      throws GeneralSecurityException {
-    if (!isAtLeastM()) {
-      Log.w(TAG, "Android Keystore requires at least Android M");
-      return null;
-    }
-
-    AndroidKeystoreKmsClient client;
-    if (keyStore != null) {
-      client = new AndroidKeystoreKmsClient.Builder().setKeyStore(keyStore).build();
-    } else {
-      client = new AndroidKeystoreKmsClient();
-    }
-
-    boolean existed = client.hasKey(keyId);
-    if (!existed) {
-      try {
-        AndroidKeystoreKmsClient.generateNewAeadKey(keyId);
-      } catch (GeneralSecurityException ex) {
-        Log.w(TAG, "cannot use Android Keystore, it'll be disabled", ex);
-        return null;
-      }
-    }
-
-    try {
-      return client.getAead(keyId);
-    } catch (GeneralSecurityException | ProviderException ex) {
-      // Throw the exception if the key exists but is unusable. We can't recover by generating a new
-      // key because there might be existing encrypted data under the unusable key.
-      // Users can provide a master key that is stored in StrongBox, which may throw a
-      // ProviderException if there's any problem with it.
-      if (existed) {
-        throw new KeyStoreException(
-            String.format("the master key %s exists but is unusable", keyId), ex);
-      }
-      // Otherwise swallow the exception if the key doesn't exist yet. We can recover by disabling
-      // Keystore.
-      Log.w(TAG, "cannot use Android Keystore, it'll be disabled", ex);
-    }
-
-    return null;
-  }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/testing/TestUtil.java b/java_src/src/main/java/com/google/crypto/tink/testing/TestUtil.java
index 909a66f..aabc86a 100644
--- a/java_src/src/main/java/com/google/crypto/tink/testing/TestUtil.java
+++ b/java_src/src/main/java/com/google/crypto/tink/testing/TestUtil.java
@@ -724,4 +724,124 @@
     res.add(new BytesMutation(Arrays.copyOf(bytes, bytes.length + 1), "Append an extra zero byte"));
     return res;
   }
+
+
+  /**
+   * Uses a z test on the given byte string, expecting all bits to be uniformly set with probability
+   * 1/2. Returns non ok status if the z test fails by more than 10 standard deviations.
+   *
+   * <p>With less statistics jargon: This counts the number of bits set and expects the number to be
+   * roughly half of the length of the string. The law of large numbers suggests that we can assume
+   * that the longer the string is, the more accurate that estimate becomes for a random string.
+   * This test is useful to detect things like strings that are entirely zero.
+   *
+   * <p>Note: By itself, this is a very weak test for randomness.
+   *
+   * @throws GeneralSecurityException if uniformity error is detected, otherwise returns normally.
+   */
+  public static void ztestUniformString(byte[] string) throws GeneralSecurityException {
+    final double minAcceptableStdDevs = 10.0;
+    double totalBits = string.length * 8;
+    double expected = totalBits / 2.0;
+    double stddev = Math.sqrt(totalBits / 4.0);
+
+    // This test is very limited at low string lengths. Below a certain threshold it tests nothing.
+    if (expected < stddev * minAcceptableStdDevs) {
+      throw new GeneralSecurityException(
+          "Test will always succeed with strings of the given length "
+              + string.length
+              + ". Use more bytes.");
+    }
+
+    long numSetBits = 0;
+    for (byte b : string) {
+      int unsignedInt = toUnsignedInt(b);
+      // Counting the number of bits set in byte:
+      while (unsignedInt != 0) {
+        numSetBits++;
+        unsignedInt = (unsignedInt & (unsignedInt - 1));
+      }
+    }
+    // Check that the number of bits is within 10 stddevs.
+    if (Math.abs((double) numSetBits - expected) < minAcceptableStdDevs * stddev) {
+      return;
+    }
+    throw new GeneralSecurityException(
+        "Z test for uniformly distributed variable out of bounds; "
+            + "Actual number of set bits was "
+            + numSetBits
+            + " expected was "
+            + expected
+            + " 10 * standard deviation is 10 * "
+            + stddev
+            + " = "
+            + 10.0 * stddev);
+  }
+
+  /**
+   * Tests that the crosscorrelation of two strings of equal length points to independent and
+   * uniformly distributed strings. Returns non ok status if the z test fails by more than 10
+   * standard deviations.
+   *
+   * <p>With less statistics jargon: This xors two strings and then performs the ZTestUniformString
+   * on the result. If the two strings are independent and uniformly distributed, the xor'ed string
+   * is as well. A cross correlation test will find whether two strings overlap more or less than it
+   * would be expected.
+   *
+   * <p>Note: Having a correlation of zero is only a necessary but not sufficient condition for
+   * independence.
+   *
+   * @throws GeneralSecurityException if uniformity error is detected, otherwise returns normally.
+   */
+  public static void ztestCrossCorrelationUniformStrings(byte[] string1, byte[] string2)
+      throws GeneralSecurityException {
+    if (string1.length != string2.length) {
+      throw new GeneralSecurityException("Strings are not of equal length");
+    }
+    byte[] crossed = new byte[string1.length];
+    for (int i = 0; i < string1.length; i++) {
+      crossed[i] = (byte) (string1[i] ^ string2[i]);
+    }
+    ztestUniformString(crossed);
+  }
+
+  /**
+   * Tests that the autocorrelation of a string points to the bits being independent and uniformly
+   * distributed. Rotates the string in a cyclic fashion. Returns non ok status if the z test fails
+   * by more than 10 standard deviations.
+   *
+   * <p>With less statistics jargon: This rotates the string bit by bit and performs
+   * ZTestCrosscorrelationUniformStrings on each of the rotated strings and the original. This will
+   * find self similarity of the input string, especially periodic self similarity. For example, it
+   * is a decent test to find English text (needs about 180 characters with the current settings).
+   *
+   * <p>Note: Having a correlation of zero is only a necessary but not sufficient condition for
+   * independence.
+   *
+   * @throws GeneralSecurityException if uniformity error is detected, otherwise returns normally.
+   */
+  public static void ztestAutocorrelationUniformString(byte[] string)
+      throws GeneralSecurityException {
+    byte[] rotated = Arrays.copyOf(string, string.length);
+
+    for (int i = 1; i < string.length * 8; i++) {
+      rotate(rotated);
+      ztestCrossCorrelationUniformStrings(string, rotated);
+    }
+  }
+
+  /** Manual implementation of Byte.toUnsignedByte. The Android JDK does not have this method. */
+  private static int toUnsignedInt(byte b) {
+    return b & 0xff;
+  }
+
+  private static void rotate(byte[] string) {
+    byte[] ref = Arrays.copyOf(string, string.length);
+    for (int i = 0; i < string.length; i++) {
+      string[i] =
+          (byte)
+              ((toUnsignedInt(string[i]) >> 1)
+                  | ((1 & toUnsignedInt(ref[(i == 0 ? string.length : i) - 1])) << 7));
+    }
+  }
 }
diff --git a/java_src/src/main/java/com/google/crypto/tink/util/BUILD.bazel b/java_src/src/main/java/com/google/crypto/tink/util/BUILD.bazel
index 4b7e486..b54518e 100644
--- a/java_src/src/main/java/com/google/crypto/tink/util/BUILD.bazel
+++ b/java_src/src/main/java/com/google/crypto/tink/util/BUILD.bazel
@@ -15,11 +15,6 @@
     ],
 )
 
-java_library(
-    name = "test_util",
-    srcs = ["TestUtil.java"],
-)
-
 android_library(
     name = "keys_downloader-android",
     srcs = ["KeysDownloader.java"],
diff --git a/java_src/src/main/java/com/google/crypto/tink/util/TestUtil.java b/java_src/src/main/java/com/google/crypto/tink/util/TestUtil.java
deleted file mode 100644
index 26e66b4..0000000
--- a/java_src/src/main/java/com/google/crypto/tink/util/TestUtil.java
+++ /dev/null
@@ -1,143 +0,0 @@
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-//
-////////////////////////////////////////////////////////////////////////////////
-
-package com.google.crypto.tink.util;
-
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-
-/** Provides various utility methods for testing. */
-public class TestUtil {
-
-  private TestUtil() {}
-
-  /**
-   * Uses a z test on the given byte string, expecting all bits to be uniformly set with probability
-   * 1/2. Returns non ok status if the z test fails by more than 10 standard deviations.
-   *
-   * <p>With less statistics jargon: This counts the number of bits set and expects the number to be
-   * roughly half of the length of the string. The law of large numbers suggests that we can assume
-   * that the longer the string is, the more accurate that estimate becomes for a random string.
-   * This test is useful to detect things like strings that are entirely zero.
-   *
-   * <p>Note: By itself, this is a very weak test for randomness.
-   *
-   * @throws GeneralSecurityException if uniformity error is detected, otherwise returns normally.
-   */
-  public static void ztestUniformString(byte[] string) throws GeneralSecurityException {
-    final double minAcceptableStdDevs = 10.0;
-    double totalBits = string.length * 8;
-    double expected = totalBits / 2.0;
-    double stddev = Math.sqrt(totalBits / 4.0);
-
-    // This test is very limited at low string lengths. Below a certain threshold it tests nothing.
-    if (expected < stddev * minAcceptableStdDevs) {
-      throw new GeneralSecurityException(
-          "Test will always succeed with strings of the given length "
-              + string.length
-              + ". Use more bytes.");
-    }
-
-    long numSetBits = 0;
-    for (byte b : string) {
-      int unsignedInt = toUnsignedInt(b);
-      // Counting the number of bits set in byte:
-      while (unsignedInt != 0) {
-        numSetBits++;
-        unsignedInt = (unsignedInt & (unsignedInt - 1));
-      }
-    }
-    // Check that the number of bits is within 10 stddevs.
-    if (Math.abs((double) numSetBits - expected) < minAcceptableStdDevs * stddev) {
-      return;
-    }
-    throw new GeneralSecurityException(
-        "Z test for uniformly distributed variable out of bounds; "
-            + "Actual number of set bits was "
-            + numSetBits
-            + " expected was "
-            + expected
-            + " 10 * standard deviation is 10 * "
-            + stddev
-            + " = "
-            + 10.0 * stddev);
-  }
-
-  /**
-   * Tests that the crosscorrelation of two strings of equal length points to independent and
-   * uniformly distributed strings. Returns non ok status if the z test fails by more than 10
-   * standard deviations.
-   *
-   * <p>With less statistics jargon: This xors two strings and then performs the ZTestUniformString
-   * on the result. If the two strings are independent and uniformly distributed, the xor'ed string
-   * is as well. A cross correlation test will find whether two strings overlap more or less than it
-   * would be expected.
-   *
-   * <p>Note: Having a correlation of zero is only a necessary but not sufficient condition for
-   * independence.
-   *
-   * @throws GeneralSecurityException if uniformity error is detected, otherwise returns normally.
-   */
-  public static void ztestCrossCorrelationUniformStrings(byte[] string1, byte[] string2)
-      throws GeneralSecurityException {
-    if (string1.length != string2.length) {
-      throw new GeneralSecurityException("Strings are not of equal length");
-    }
-    byte[] crossed = new byte[string1.length];
-    for (int i = 0; i < string1.length; i++) {
-      crossed[i] = (byte) (string1[i] ^ string2[i]);
-    }
-    ztestUniformString(crossed);
-  }
-
-  /**
-   * Tests that the autocorrelation of a string points to the bits being independent and uniformly
-   * distributed. Rotates the string in a cyclic fashion. Returns non ok status if the z test fails
-   * by more than 10 standard deviations.
-   *
-   * <p>With less statistics jargon: This rotates the string bit by bit and performs
-   * ZTestCrosscorrelationUniformStrings on each of the rotated strings and the original. This will
-   * find self similarity of the input string, especially periodic self similarity. For example, it
-   * is a decent test to find English text (needs about 180 characters with the current settings).
-   *
-   * <p>Note: Having a correlation of zero is only a necessary but not sufficient condition for
-   * independence.
-   *
-   * @throws GeneralSecurityException if uniformity error is detected, otherwise returns normally.
-   */
-  public static void ztestAutocorrelationUniformString(byte[] string)
-      throws GeneralSecurityException {
-    byte[] rotated = Arrays.copyOf(string, string.length);
-
-    for (int i = 1; i < string.length * 8; i++) {
-      rotate(rotated);
-      ztestCrossCorrelationUniformStrings(string, rotated);
-    }
-  }
-
-  /** Manual implementation of Byte.toUnsignedByte. The Android JDK does not have this method. */
-  private static int toUnsignedInt(byte b) {
-    return b & 0xff;
-  }
-
-  private static void rotate(byte[] string) {
-    byte[] ref = Arrays.copyOf(string, string.length);
-    for (int i = 0; i < string.length; i++) {
-      string[i] =
-          (byte)
-              ((toUnsignedInt(string[i]) >> 1)
-                  | ((1 & toUnsignedInt(ref[(i == 0 ? string.length : i) - 1])) << 7));
-    }
-  }
-}
diff --git a/java_src/src/test/BUILD.bazel b/java_src/src/test/BUILD.bazel
index 71ecef0..de00021 100644
--- a/java_src/src/test/BUILD.bazel
+++ b/java_src/src/test/BUILD.bazel
@@ -185,7 +185,6 @@
         "//src/main/java/com/google/crypto/tink/testing:test_util",
         "//src/main/java/com/google/crypto/tink/testing:wycheproof_test_util",
         "//src/main/java/com/google/crypto/tink/util:keys_downloader",
-        "//src/main/java/com/google/crypto/tink/util:test_util",
         "@com_google_protobuf//:protobuf_javalite",
         "@maven//:com_amazonaws_aws_java_sdk_core",
         "@maven//:com_amazonaws_aws_java_sdk_kms",
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/PrfHmacJceTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/PrfHmacJceTest.java
index 894cd0c..b77ad49 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/PrfHmacJceTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/PrfHmacJceTest.java
@@ -22,7 +22,7 @@
 
 import com.google.crypto.tink.Mac;
 import com.google.crypto.tink.prf.Prf;
-import com.google.crypto.tink.util.TestUtil;
+import com.google.crypto.tink.testing.TestUtil;
 import java.security.GeneralSecurityException;
 import java.security.InvalidAlgorithmParameterException;
 import java.util.Arrays;
diff --git a/java_src/src/test/java/com/google/crypto/tink/subtle/prf/HkdfStreamingPrfTest.java b/java_src/src/test/java/com/google/crypto/tink/subtle/prf/HkdfStreamingPrfTest.java
index ef63a13..3d08ac4 100644
--- a/java_src/src/test/java/com/google/crypto/tink/subtle/prf/HkdfStreamingPrfTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/subtle/prf/HkdfStreamingPrfTest.java
@@ -21,7 +21,7 @@
 import com.google.crypto.tink.subtle.Hex;
 import com.google.crypto.tink.subtle.Hkdf;
 import com.google.crypto.tink.subtle.Random;
-import com.google.crypto.tink.util.TestUtil;
+import com.google.crypto.tink.testing.TestUtil;
 import java.io.InputStream;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/java_src/src/test/java/com/google/crypto/tink/util/TestUtilTest.java b/java_src/src/test/java/com/google/crypto/tink/testing/TestUtilTest.java
similarity index 99%
rename from java_src/src/test/java/com/google/crypto/tink/util/TestUtilTest.java
rename to java_src/src/test/java/com/google/crypto/tink/testing/TestUtilTest.java
index 8edda06..c8c33be 100644
--- a/java_src/src/test/java/com/google/crypto/tink/util/TestUtilTest.java
+++ b/java_src/src/test/java/com/google/crypto/tink/testing/TestUtilTest.java
@@ -12,7 +12,7 @@
 //
 ////////////////////////////////////////////////////////////////////////////////
 
-package com.google.crypto.tink.util;
+package com.google.crypto.tink.testing;
 
 import static com.google.common.truth.Truth.assertThat;
 import static org.junit.Assert.assertThrows;
diff --git a/javascript/internal/binary_keyset_writer.ts b/javascript/internal/binary_keyset_writer.ts
index dc7e256..4737926 100644
--- a/javascript/internal/binary_keyset_writer.ts
+++ b/javascript/internal/binary_keyset_writer.ts
@@ -13,6 +13,7 @@
 ////////////////////////////////////////////////////////////////////////////////
 import {SecurityException} from '../exception/security_exception';
 import {KeysetWriter} from './keyset_writer';
+import {PbEncryptedKeyset, PbKeyset} from './proto';
 
 /**
  * KeysetWriter knows how to write a keyset or an encrypted keyset.
@@ -21,7 +22,7 @@
  */
 export class BinaryKeysetWriter implements KeysetWriter {
   /** @override */
-  write(keyset: AnyDuringMigration) {
+  write(keyset: PbKeyset|PbEncryptedKeyset): Uint8Array {
     if (!keyset) {
       throw new SecurityException('keyset has to be non-null.');
     }
diff --git a/objc/CHANGELOG b/objc/CHANGELOG
index 90b8fcb..1ccd1e8 100644
--- a/objc/CHANGELOG
+++ b/objc/CHANGELOG
@@ -1,3 +1,7 @@
+Version 1.4.0
+==================================
+Obj-C protobufs removed.
+
 Version 1.4.0-rc2
 ==================================
 No changes to the Obj-C implementation since 1.4.0-rc1.
diff --git a/python/VERSION b/python/VERSION
index 092c7ac..364dba2 100644
--- a/python/VERSION
+++ b/python/VERSION
@@ -1,2 +1,2 @@
 """ Version of the current release of Tink """
-TINK_VERSION_LABEL = "1.4.0-rc2"
+TINK_VERSION_LABEL = "1.4.0"
diff --git a/python/tink/aead/BUILD.bazel b/python/tink/aead/BUILD.bazel
index 9b0e9ff..28ef5bd 100644
--- a/python/tink/aead/BUILD.bazel
+++ b/python/tink/aead/BUILD.bazel
@@ -51,6 +51,7 @@
         "//tink/proto:aes_ctr_hmac_aead_py_pb2",
         "//tink/proto:aes_eax_py_pb2",
         "//tink/proto:aes_gcm_py_pb2",
+        "//tink/proto:aes_gcm_siv_py_pb2",
         "//tink/proto:common_py_pb2",
         "//tink/proto:tink_py_pb2",
         "//tink/proto:xchacha20_poly1305_py_pb2",
@@ -89,6 +90,7 @@
         "//tink/proto:aes_ctr_hmac_aead_py_pb2",
         "//tink/proto:aes_eax_py_pb2",
         "//tink/proto:aes_gcm_py_pb2",
+        "//tink/proto:aes_gcm_siv_py_pb2",
         "//tink/proto:common_py_pb2",
         "//tink/proto:tink_py_pb2",
     ],
@@ -104,6 +106,7 @@
         "//tink/proto:aes_ctr_hmac_aead_py_pb2",
         "//tink/proto:aes_eax_py_pb2",
         "//tink/proto:aes_gcm_py_pb2",
+        "//tink/proto:aes_gcm_siv_py_pb2",
         "//tink/proto:common_py_pb2",
         "//tink/proto:tink_py_pb2",
     ],
diff --git a/python/tink/aead/_aead_key_manager_test.py b/python/tink/aead/_aead_key_manager_test.py
index 6301002..a1f0cf0 100644
--- a/python/tink/aead/_aead_key_manager_test.py
+++ b/python/tink/aead/_aead_key_manager_test.py
@@ -23,6 +23,7 @@
 from tink.proto import aes_ctr_hmac_aead_pb2
 from tink.proto import aes_eax_pb2
 from tink.proto import aes_gcm_pb2
+from tink.proto import aes_gcm_siv_pb2
 from tink.proto import common_pb2
 from tink.proto import tink_pb2
 from tink.proto import xchacha20_poly1305_pb2
@@ -84,6 +85,18 @@
     self.assertEqual(key.hmac_key.params.tag_size, 16)
     self.assertEqual(key.hmac_key.params.hash, common_pb2.SHA256)
 
+  def test_new_key_data_aes_gcm_siv(self):
+    key_template = aead.aead_key_templates.create_aes_gcm_siv_key_template(
+        key_size=16)
+    key_manager = core.Registry.key_manager(key_template.type_url)
+    key_data = key_manager.new_key_data(key_template)
+    self.assertEqual(key_data.type_url, key_template.type_url)
+    self.assertEqual(key_data.key_material_type, tink_pb2.KeyData.SYMMETRIC)
+    key = aes_gcm_siv_pb2.AesGcmSivKey()
+    key.ParseFromString(key_data.value)
+    self.assertEqual(key.version, 0)
+    self.assertLen(key.key_value, 16)
+
   def test_new_key_data_xchacha20_poly1305(self):
     template = aead.aead_key_templates.XCHACHA20_POLY1305
     key_manager = core.Registry.key_manager(template.type_url)
@@ -122,6 +135,8 @@
       aead.aead_key_templates.AES256_EAX,
       aead.aead_key_templates.AES128_GCM,
       aead.aead_key_templates.AES256_GCM,
+      aead.aead_key_templates.AES128_GCM_SIV,
+      aead.aead_key_templates.AES256_GCM_SIV,
       aead.aead_key_templates.AES128_CTR_HMAC_SHA256,
       aead.aead_key_templates.AES256_CTR_HMAC_SHA256,
       aead.aead_key_templates.XCHACHA20_POLY1305])
@@ -138,6 +153,8 @@
       aead.aead_key_templates.AES256_EAX,
       aead.aead_key_templates.AES128_GCM,
       aead.aead_key_templates.AES256_GCM,
+      aead.aead_key_templates.AES128_GCM_SIV,
+      aead.aead_key_templates.AES256_GCM_SIV,
       aead.aead_key_templates.AES128_CTR_HMAC_SHA256,
       aead.aead_key_templates.AES256_CTR_HMAC_SHA256,
       aead.aead_key_templates.XCHACHA20_POLY1305])
diff --git a/python/tink/aead/_aead_key_templates.py b/python/tink/aead/_aead_key_templates.py
index ecf9a14..7e795af 100644
--- a/python/tink/aead/_aead_key_templates.py
+++ b/python/tink/aead/_aead_key_templates.py
@@ -28,11 +28,14 @@
 from tink.proto import aes_ctr_hmac_aead_pb2
 from tink.proto import aes_eax_pb2
 from tink.proto import aes_gcm_pb2
+from tink.proto import aes_gcm_siv_pb2
 from tink.proto import common_pb2
 from tink.proto import tink_pb2
 
 _AES_EAX_KEY_TYPE_URL = 'type.googleapis.com/google.crypto.tink.AesEaxKey'
 _AES_GCM_KEY_TYPE_URL = 'type.googleapis.com/google.crypto.tink.AesGcmKey'
+_AES_GCM_SIV_KEY_TYPE_URL = (
+    'type.googleapis.com/google.crypto.tink.AesGcmSivKey')
 _AES_CTR_HMAC_AEAD_KEY_TYPE_URL = (
     'type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey')
 _CHACHA20_POLY1305_KEY_TYPE_URL = (
@@ -65,6 +68,17 @@
   return key_template
 
 
+def create_aes_gcm_siv_key_template(key_size: int) -> tink_pb2.KeyTemplate:
+  """Creates an AES GCM SIV KeyTemplate, and fills in its values."""
+  key_format = aes_gcm_siv_pb2.AesGcmSivKeyFormat()
+  key_format.key_size = key_size
+  key_template = tink_pb2.KeyTemplate()
+  key_template.value = key_format.SerializeToString()
+  key_template.type_url = _AES_GCM_SIV_KEY_TYPE_URL
+  key_template.output_prefix_type = tink_pb2.TINK
+  return key_template
+
+
 def create_aes_ctr_hmac_aead_key_template(
     aes_key_size: int, iv_size: int, hmac_key_size: int, tag_size: int,
     hash_type: common_pb2.HashType) -> tink_pb2.KeyTemplate:
@@ -86,6 +100,8 @@
 AES256_EAX = create_aes_eax_key_template(key_size=32, iv_size=16)
 AES128_GCM = create_aes_gcm_key_template(key_size=16)
 AES256_GCM = create_aes_gcm_key_template(key_size=32)
+AES128_GCM_SIV = create_aes_gcm_siv_key_template(key_size=16)
+AES256_GCM_SIV = create_aes_gcm_siv_key_template(key_size=32)
 AES128_CTR_HMAC_SHA256 = create_aes_ctr_hmac_aead_key_template(
     aes_key_size=16,
     iv_size=16,
diff --git a/python/tink/aead/_aead_key_templates_test.py b/python/tink/aead/_aead_key_templates_test.py
index 241cc2b..37391fa 100644
--- a/python/tink/aead/_aead_key_templates_test.py
+++ b/python/tink/aead/_aead_key_templates_test.py
@@ -22,6 +22,7 @@
 from tink.proto import aes_ctr_hmac_aead_pb2
 from tink.proto import aes_eax_pb2
 from tink.proto import aes_gcm_pb2
+from tink.proto import aes_gcm_siv_pb2
 from tink.proto import common_pb2
 from tink.proto import tink_pb2
 from tink import aead
@@ -91,6 +92,34 @@
     key_format.ParseFromString(template.value)
     self.assertEqual(42, key_format.key_size)
 
+  def test_aes128_gcm_siv(self):
+    template = aead.aead_key_templates.AES128_GCM_SIV
+    self.assertEqual('type.googleapis.com/google.crypto.tink.AesGcmSivKey',
+                     template.type_url)
+    self.assertEqual(tink_pb2.TINK, template.output_prefix_type)
+    key_format = aes_gcm_siv_pb2.AesGcmSivKeyFormat()
+    key_format.ParseFromString(template.value)
+    self.assertEqual(16, key_format.key_size)
+
+  def test_aes256_gcm_siv(self):
+    template = aead.aead_key_templates.AES256_GCM_SIV
+    self.assertEqual('type.googleapis.com/google.crypto.tink.AesGcmSivKey',
+                     template.type_url)
+    self.assertEqual(tink_pb2.TINK, template.output_prefix_type)
+    key_format = aes_gcm_siv_pb2.AesGcmSivKeyFormat()
+    key_format.ParseFromString(template.value)
+    self.assertEqual(32, key_format.key_size)
+
+  def test_create_aes_gcm_siv_key_template(self):
+    template = aead.aead_key_templates.create_aes_gcm_siv_key_template(
+        key_size=42)
+    self.assertEqual('type.googleapis.com/google.crypto.tink.AesGcmSivKey',
+                     template.type_url)
+    self.assertEqual(tink_pb2.TINK, template.output_prefix_type)
+    key_format = aes_gcm_siv_pb2.AesGcmSivKeyFormat()
+    key_format.ParseFromString(template.value)
+    self.assertEqual(42, key_format.key_size)
+
   def test_aes256_ctr_hmac_sha256(self):
     template = aead.aead_key_templates.AES128_CTR_HMAC_SHA256
     self.assertEqual('type.googleapis.com/google.crypto.tink.AesCtrHmacAeadKey',
diff --git a/python/tink/aead/_aead_wrapper.py b/python/tink/aead/_aead_wrapper.py
index 310e988..aab414b 100644
--- a/python/tink/aead/_aead_wrapper.py
+++ b/python/tink/aead/_aead_wrapper.py
@@ -19,9 +19,8 @@
 # Placeholder for import for type annotations
 from __future__ import print_function
 
-from absl import logging
-
 from typing import Type
+from absl import logging
 
 from tink import core
 from tink.aead import _aead
diff --git a/python/tink/daead/BUILD.bazel b/python/tink/daead/BUILD.bazel
index bf01716..9ee5eae 100644
--- a/python/tink/daead/BUILD.bazel
+++ b/python/tink/daead/BUILD.bazel
@@ -44,6 +44,7 @@
     deps = [
         ":daead",
         requirement("absl-py"),
+        "//tink:tink_python",
         "//tink/core",
         "//tink/proto:aes_siv_py_pb2",
         "//tink/proto:tink_py_pb2",
diff --git a/python/tink/daead/_deterministic_aead_key_manager_test.py b/python/tink/daead/_deterministic_aead_key_manager_test.py
index 0691afa..c237f19 100644
--- a/python/tink/daead/_deterministic_aead_key_manager_test.py
+++ b/python/tink/daead/_deterministic_aead_key_manager_test.py
@@ -19,8 +19,10 @@
 from __future__ import print_function
 
 from absl.testing import absltest
+
 from tink.proto import aes_siv_pb2
 from tink.proto import tink_pb2
+import tink
 from tink import core
 from tink import daead
 
@@ -31,23 +33,11 @@
 
 class DeterministicAeadKeyManagerTest(absltest.TestCase):
 
-  def setUp(self):
-    super(DeterministicAeadKeyManagerTest, self).setUp()
-    self.key_manager = core.Registry.key_manager(
-        'type.googleapis.com/google.crypto.tink.AesSivKey')
-
-  def test_primitive_class(self):
-    self.assertEqual(self.key_manager.primitive_class(),
-                     daead.DeterministicAead)
-
-  def test_key_type(self):
-    self.assertEqual(self.key_manager.key_type(),
-                     'type.googleapis.com/google.crypto.tink.AesSivKey')
-
   def test_new_key_data(self):
     key_template = daead.deterministic_aead_key_templates.AES256_SIV
-    key_data = self.key_manager.new_key_data(key_template)
-    self.assertEqual(key_data.type_url, self.key_manager.key_type())
+    key_manager = core.Registry.key_manager(key_template.type_url)
+    key_data = key_manager.new_key_data(key_template)
+    self.assertEqual(key_data.type_url, key_manager.key_type())
     self.assertEqual(key_data.key_material_type, tink_pb2.KeyData.SYMMETRIC)
     key = aes_siv_pb2.AesSivKey()
     key.ParseFromString(key_data.value)
@@ -59,12 +49,12 @@
                     .create_aes_siv_key_template(63))
     with self.assertRaisesRegex(core.TinkError,
                                 'Invalid key size'):
-      self.key_manager.new_key_data(key_template)
+      tink.new_keyset_handle(key_template)
 
   def test_encrypt_decrypt(self):
-    daead_primitive = self.key_manager.primitive(
-        self.key_manager.new_key_data(
-            daead.deterministic_aead_key_templates.AES256_SIV))
+    keyset_handle = tink.new_keyset_handle(
+        daead.deterministic_aead_key_templates.AES256_SIV)
+    daead_primitive = keyset_handle.primitive(daead.DeterministicAead)
     plaintext = b'plaintext'
     associated_data = b'associated_data'
     ciphertext = daead_primitive.encrypt_deterministically(
@@ -73,6 +63,14 @@
         daead_primitive.decrypt_deterministically(ciphertext, associated_data),
         plaintext)
 
+  def test_invalid_decrypt_raises_error(self):
+    keyset_handle = tink.new_keyset_handle(
+        daead.deterministic_aead_key_templates.AES256_SIV)
+    daead_primitive = keyset_handle.primitive(daead.DeterministicAead)
+    with self.assertRaises(core.TinkError):
+      daead_primitive.decrypt_deterministically(
+          b'bad ciphertext', b'associated_data')
+
 
 if __name__ == '__main__':
   absltest.main()
diff --git a/python/tink/hybrid/BUILD.bazel b/python/tink/hybrid/BUILD.bazel
index cd2e3df..a85c701 100644
--- a/python/tink/hybrid/BUILD.bazel
+++ b/python/tink/hybrid/BUILD.bazel
@@ -53,6 +53,7 @@
     deps = [
         ":hybrid",
         requirement("absl-py"),
+        "//tink:tink_python",
         "//tink/aead",
         "//tink/core",
         "//tink/proto:aes_gcm_py_pb2",
diff --git a/python/tink/hybrid/_hybrid_key_manager_test.py b/python/tink/hybrid/_hybrid_key_manager_test.py
index a7759da..7ef308f 100644
--- a/python/tink/hybrid/_hybrid_key_manager_test.py
+++ b/python/tink/hybrid/_hybrid_key_manager_test.py
@@ -19,11 +19,13 @@
 # Placeholder for import for type annotations
 from __future__ import print_function
 
-from absl.testing import absltest
 from typing import cast
+from absl.testing import absltest
+from absl.testing import parameterized
 from tink.proto import common_pb2
 from tink.proto import ecies_aead_hkdf_pb2
 from tink.proto import tink_pb2
+import tink
 from tink import aead
 from tink import core
 from tink import hybrid
@@ -33,43 +35,12 @@
   hybrid.register()
 
 
-def _hybrid_decrypt_key_manager() -> core.PrivateKeyManager:
-  key_manager = core.Registry.key_manager(
-      'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey')
-  if not isinstance(key_manager, core.PrivateKeyManager):
-    raise core.TinkError('key_manager is not a PrivateKeyManager')
-  return key_manager
-
-
-def _hybrid_encrypt_key_manager() -> core.KeyManager:
-  return core.Registry.key_manager(
-      'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey')
-
-
-class HybridKeyManagerTest(absltest.TestCase):
-
-  def test_hybrid_decrypt_primitive_class(self):
-    self.assertEqual(_hybrid_decrypt_key_manager().primitive_class(),
-                     hybrid.HybridDecrypt)
-
-  def test_hybrid_encrypt_primitive_class(self):
-    self.assertEqual(_hybrid_encrypt_key_manager().primitive_class(),
-                     hybrid.HybridEncrypt)
-
-  def test_hybrid_decrypt_key_type(self):
-    self.assertEqual(
-        _hybrid_decrypt_key_manager().key_type(),
-        'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPrivateKey')
-
-  def test_hybrid_encrypt_key_type(self):
-    self.assertEqual(
-        _hybrid_encrypt_key_manager().key_type(),
-        'type.googleapis.com/google.crypto.tink.EciesAeadHkdfPublicKey')
+class HybridKeyManagerTest(parameterized.TestCase):
 
   def test_new_key_data(self):
-    key_manager = _hybrid_decrypt_key_manager()
-    key_data = key_manager.new_key_data(
-        hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM)
+    tmpl = hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM
+    key_manager = core.Registry.key_manager(tmpl.type_url)
+    key_data = key_manager.new_key_data(tmpl)
     self.assertEqual(key_data.type_url, key_manager.key_type())
     self.assertEqual(key_data.key_material_type,
                      tink_pb2.KeyData.ASYMMETRIC_PRIVATE)
@@ -79,16 +50,17 @@
     self.assertEqual(key.public_key.params.kem_params.curve_type,
                      common_pb2.NIST_P256)
 
-  def test_new_key_data_invalid_params_throw_exception(self):
+  def test_new_keyset_handle_invalid_params_throw_exception(self):
+    templates = hybrid.hybrid_key_templates
+    key_template = templates.create_ecies_aead_hkdf_key_template(
+        curve_type=cast(common_pb2.EllipticCurveType, 100),
+        ec_point_format=common_pb2.UNCOMPRESSED,
+        hash_type=common_pb2.SHA256,
+        dem_key_template=aead.aead_key_templates.AES128_GCM)
     with self.assertRaises(core.TinkError):
-      _hybrid_decrypt_key_manager().new_key_data(
-          hybrid.hybrid_key_templates.create_ecies_aead_hkdf_key_template(
-              curve_type=cast(common_pb2.EllipticCurveType, 100),
-              ec_point_format=common_pb2.UNCOMPRESSED,
-              hash_type=common_pb2.SHA256,
-              dem_key_template=aead.aead_key_templates.AES128_GCM))
+      tink.new_keyset_handle(key_template)
 
-  def test_new_key_data_on_public_key_manager_fails(self):
+  def test_new_keyset_hanlde_on_public_key_fails(self):
     key_format = ecies_aead_hkdf_pb2.EciesAeadHkdfKeyFormat()
     key_template = tink_pb2.KeyTemplate()
     key_template.type_url = (
@@ -96,26 +68,28 @@
     key_template.value = key_format.SerializeToString()
     key_template.output_prefix_type = tink_pb2.TINK
     with self.assertRaises(core.TinkError):
-      key_manager = _hybrid_encrypt_key_manager()
-      key_manager.new_key_data(key_template)
+      tink.new_keyset_handle(key_template)
 
-  def test_encrypt_decrypt(self):
-    decrypt_key_manager = _hybrid_decrypt_key_manager()
-    encrypt_key_manager = _hybrid_encrypt_key_manager()
-    key_data = decrypt_key_manager.new_key_data(
-        hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM)
-    public_key_data = decrypt_key_manager.public_key_data(key_data)
-    hybrid_enc = encrypt_key_manager.primitive(public_key_data)
+  @parameterized.parameters([
+      hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM, hybrid
+      .hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256
+  ])
+  def test_encrypt_decrypt(self, template):
+    private_handle = tink.new_keyset_handle(template)
+    public_handle = private_handle.public_keyset_handle()
+    hybrid_enc = public_handle.primitive(hybrid.HybridEncrypt)
     ciphertext = hybrid_enc.encrypt(b'some plaintext', b'some context info')
-    hybrid_dec = decrypt_key_manager.primitive(key_data)
+    hybrid_dec = private_handle.primitive(hybrid.HybridDecrypt)
     self.assertEqual(hybrid_dec.decrypt(ciphertext, b'some context info'),
                      b'some plaintext')
 
-  def test_decrypt_fails(self):
-    decrypt_key_manager = _hybrid_decrypt_key_manager()
-    key_data = decrypt_key_manager.new_key_data(
-        hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM)
-    hybrid_dec = decrypt_key_manager.primitive(key_data)
+  @parameterized.parameters([
+      hybrid.hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM, hybrid
+      .hybrid_key_templates.ECIES_P256_HKDF_HMAC_SHA256_AES128_CTR_HMAC_SHA256
+  ])
+  def test_decrypt_fails(self, template):
+    private_handle = tink.new_keyset_handle(template)
+    hybrid_dec = private_handle.primitive(hybrid.HybridDecrypt)
     with self.assertRaises(core.TinkError):
       hybrid_dec.decrypt(b'bad ciphertext', b'some context info')
 
diff --git a/python/tink/mac/BUILD.bazel b/python/tink/mac/BUILD.bazel
index 1c99cee..05e2ba2 100644
--- a/python/tink/mac/BUILD.bazel
+++ b/python/tink/mac/BUILD.bazel
@@ -44,6 +44,7 @@
     deps = [
         ":mac",
         requirement("absl-py"),
+        "//tink:tink_python",
         "//tink/core",
         "//tink/proto:common_py_pb2",
         "//tink/proto:hmac_py_pb2",
diff --git a/python/tink/mac/_mac_key_manager_test.py b/python/tink/mac/_mac_key_manager_test.py
index 58b1448..956f231 100644
--- a/python/tink/mac/_mac_key_manager_test.py
+++ b/python/tink/mac/_mac_key_manager_test.py
@@ -18,11 +18,12 @@
 from __future__ import print_function
 
 from absl.testing import absltest
+from absl.testing import parameterized
 
 from tink.proto import common_pb2
 from tink.proto import hmac_pb2
-from tink.proto import tink_pb2
 
+import tink
 from tink import core
 from tink import mac
 
@@ -31,34 +32,14 @@
   mac.register()
 
 
-class MacKeyManagerTest(absltest.TestCase):
+class MacKeyManagerTest(parameterized.TestCase):
 
-  def setUp(self):
-    super(MacKeyManagerTest, self).setUp()
-    self.key_manager = core.Registry.key_manager(
-        'type.googleapis.com/google.crypto.tink.HmacKey')
-
-  def new_hmac_key_template(self, hash_type, tag_size, key_size):
-    key_format = hmac_pb2.HmacKeyFormat()
-    key_format.params.hash = hash_type
-    key_format.params.tag_size = tag_size
-    key_format.key_size = key_size
-    key_template = tink_pb2.KeyTemplate()
-    key_template.type_url = ('type.googleapis.com/google.crypto.tink.HmacKey')
-    key_template.value = key_format.SerializeToString()
-    return key_template
-
-  def test_primitive_class(self):
-    self.assertEqual(self.key_manager.primitive_class(), mac.Mac)
-
-  def test_key_type(self):
-    self.assertEqual(self.key_manager.key_type(),
-                     'type.googleapis.com/google.crypto.tink.HmacKey')
-
-  def test_new_key_data(self):
-    key_template = self.new_hmac_key_template(common_pb2.SHA256, 24, 16)
-    key_data = self.key_manager.new_key_data(key_template)
-    self.assertEqual(key_data.type_url, self.key_manager.key_type())
+  def test_new_key_data_hmac(self):
+    key_template = mac.mac_key_templates.create_hmac_key_template(
+        key_size=16, tag_size=24, hash_type=common_pb2.SHA256)
+    key_manager = core.Registry.key_manager(key_template.type_url)
+    key_data = key_manager.new_key_data(key_template)
+    self.assertEqual(key_data.type_url, key_manager.key_type())
     key = hmac_pb2.HmacKey()
     key.ParseFromString(key_data.value)
     self.assertEqual(key.version, 0)
@@ -67,25 +48,36 @@
     self.assertLen(key.key_value, 16)
 
   def test_invalid_params_throw_exception(self):
-    key_template = self.new_hmac_key_template(common_pb2.SHA256, 9, 16)
-    with self.assertRaisesRegex(core.TinkError, 'Invalid HmacParams'):
-      self.key_manager.new_key_data(key_template)
+    key_template = mac.mac_key_templates.create_hmac_key_template(
+        key_size=16, tag_size=9, hash_type=common_pb2.SHA256)
+    with self.assertRaises(core.TinkError):
+      tink.new_keyset_handle(key_template)
 
-  def test_mac_success(self):
-    mac_primitive = self.key_manager.primitive(
-        self.key_manager.new_key_data(
-            self.new_hmac_key_template(common_pb2.SHA256, 24, 16)))
+  @parameterized.parameters([
+      mac.mac_key_templates.HMAC_SHA256_128BITTAG,
+      mac.mac_key_templates.HMAC_SHA256_256BITTAG,
+      mac.mac_key_templates.HMAC_SHA512_256BITTAG,
+      mac.mac_key_templates.HMAC_SHA512_512BITTAG,
+  ])
+  def test_mac_success(self, key_template):
+    keyset_handle = tink.new_keyset_handle(key_template)
+    mac_primitive = keyset_handle.primitive(mac.Mac)
     data = b'data'
     tag = mac_primitive.compute_mac(data)
-    self.assertLen(tag, 24)
+    self.assertGreaterEqual(len(tag), 16)
     # No exception raised, no return value.
     self.assertIsNone(mac_primitive.verify_mac(tag, data))
 
-  def test_mac_wrong(self):
-    mac_primitive = self.key_manager.primitive(
-        self.key_manager.new_key_data(
-            self.new_hmac_key_template(common_pb2.SHA256, 16, 16)))
-    with self.assertRaisesRegex(core.TinkError, 'verification failed'):
+  @parameterized.parameters([
+      mac.mac_key_templates.HMAC_SHA256_128BITTAG,
+      mac.mac_key_templates.HMAC_SHA256_256BITTAG,
+      mac.mac_key_templates.HMAC_SHA512_256BITTAG,
+      mac.mac_key_templates.HMAC_SHA512_512BITTAG,
+  ])
+  def test_mac_wrong(self, key_template):
+    keyset_handle = tink.new_keyset_handle(key_template)
+    mac_primitive = keyset_handle.primitive(mac.Mac)
+    with self.assertRaises(core.TinkError):
       mac_primitive.verify_mac(b'0123456789ABCDEF', b'data')
 
 
diff --git a/python/tink/signature/BUILD.bazel b/python/tink/signature/BUILD.bazel
index 239da0f..ec37bbf 100644
--- a/python/tink/signature/BUILD.bazel
+++ b/python/tink/signature/BUILD.bazel
@@ -54,6 +54,7 @@
         ":signature",
         requirement("absl-py"),
         "//tink:tink_config",
+        "//tink:tink_python",
         "//tink/core",
         "//tink/proto:common_py_pb2",
         "//tink/proto:ecdsa_py_pb2",
diff --git a/python/tink/signature/_signature_key_manager_test.py b/python/tink/signature/_signature_key_manager_test.py
index 92df0e3..817b2c0 100644
--- a/python/tink/signature/_signature_key_manager_test.py
+++ b/python/tink/signature/_signature_key_manager_test.py
@@ -19,10 +19,12 @@
 from __future__ import print_function
 
 from absl.testing import absltest
+from absl.testing import parameterized
 
 from tink.proto import common_pb2
 from tink.proto import ecdsa_pb2
 from tink.proto import tink_pb2
+import tink
 from tink import core
 from tink import signature
 
@@ -31,57 +33,14 @@
   signature.register()
 
 
-def _sign_key_manager() -> core.PrivateKeyManager:
-  key_manager = core.Registry.key_manager(
-      'type.googleapis.com/google.crypto.tink.EcdsaPrivateKey')
-  if not isinstance(key_manager, core.PrivateKeyManager):
-    raise core.TinkError('key_manager is not a PrivateKeyManager')
-  return key_manager
+class PublicKeySignKeyManagerTest(parameterized.TestCase):
 
-
-def _verify_key_manager() -> core.KeyManager:
-  return core.Registry.key_manager(
-      'type.googleapis.com/google.crypto.tink.EcdsaPublicKey')
-
-
-def new_ecdsa_key_template(hash_type, curve_type, encoding, public=True):
-  params = ecdsa_pb2.EcdsaParams(
-      hash_type=hash_type, curve=curve_type, encoding=encoding)
-  key_format = ecdsa_pb2.EcdsaKeyFormat(params=params)
-  key_template = tink_pb2.KeyTemplate()
-  if public:
-    append = 'EcdsaPublicKey'
-  else:
-    append = 'EcdsaPrivateKey'
-  key_template.type_url = 'type.googleapis.com/google.crypto.tink.' + append
-  key_template.value = key_format.SerializeToString()
-  return key_template
-
-
-class PublicKeySignKeyManagerTest(absltest.TestCase):
-
-  def setUp(self):
-    super(PublicKeySignKeyManagerTest, self).setUp()
-    self.key_manager_sign = _sign_key_manager()
-    self.key_manager_verify = _verify_key_manager()
-
-  def test_primitive_class(self):
-    self.assertEqual(self.key_manager_sign.primitive_class(),
-                     signature.PublicKeySign)
-    self.assertEqual(self.key_manager_verify.primitive_class(),
-                     signature.PublicKeyVerify)
-
-  def test_key_type(self):
-    self.assertEqual(self.key_manager_sign.key_type(),
-                     'type.googleapis.com/google.crypto.tink.EcdsaPrivateKey')
-    self.assertEqual(self.key_manager_verify.key_type(),
-                     'type.googleapis.com/google.crypto.tink.EcdsaPublicKey')
-
-  def test_new_key_data(self):
-    key_template = new_ecdsa_key_template(
-        common_pb2.SHA256, common_pb2.NIST_P256, ecdsa_pb2.DER, public=False)
-    key_data = self.key_manager_sign.new_key_data(key_template)
-    self.assertEqual(key_data.type_url, self.key_manager_sign.key_type())
+  def test_new_key_data_ecdsa(self):
+    template = signature.signature_key_templates.create_ecdsa_key_template(
+        common_pb2.SHA256, common_pb2.NIST_P256, ecdsa_pb2.DER)
+    key_manager = core.Registry.key_manager(template.type_url)
+    key_data = key_manager.new_key_data(template)
+    self.assertEqual(key_data.type_url, template.type_url)
     key = ecdsa_pb2.EcdsaPrivateKey()
     key.ParseFromString(key_data.value)
     public_key = key.public_key
@@ -92,47 +51,65 @@
     self.assertEqual(public_key.params.encoding, ecdsa_pb2.DER)
     self.assertLen(key.key_value, 32)
 
-  def test_new_public_key_data_fails(self):
-    key_template = new_ecdsa_key_template(
-        common_pb2.SHA256, common_pb2.NIST_P256, ecdsa_pb2.DER, public=True)
+  def test_new_public_keyset_handle_fails(self):
+    params = ecdsa_pb2.EcdsaParams(
+        hash_type=common_pb2.SHA256,
+        curve=common_pb2.NIST_P256,
+        encoding=ecdsa_pb2.DER)
+    key_format = ecdsa_pb2.EcdsaKeyFormat(params=params)
+    template = tink_pb2.KeyTemplate()
+    template.type_url = 'type.googleapis.com/google.crypto.tink.EcdsaPublicKey'
+    template.value = key_format.SerializeToString()
     with self.assertRaises(core.TinkError):
-      self.key_manager_verify.new_key_data(key_template)
+      tink.new_keyset_handle(template)
 
-  def test_sign_verify_success(self):
-    priv_key = self.key_manager_sign.new_key_data(
-        new_ecdsa_key_template(
-            common_pb2.SHA256,
-            common_pb2.NIST_P256,
-            ecdsa_pb2.DER,
-            public=False))
-    pub_key = self.key_manager_sign.public_key_data(priv_key)
-
-    verifier = self.key_manager_verify.primitive(pub_key)
-    signer = self.key_manager_sign.primitive(priv_key)
+  @parameterized.parameters([
+      signature.signature_key_templates.ECDSA_P256,
+      signature.signature_key_templates.ECDSA_P384,
+      signature.signature_key_templates.ECDSA_P521,
+      signature.signature_key_templates.ECDSA_P256_IEEE_P1363,
+      signature.signature_key_templates.ECDSA_P384_IEEE_P1363,
+      signature.signature_key_templates.ECDSA_P521_IEEE_P1363,
+      signature.signature_key_templates.ED25519,
+      signature.signature_key_templates.RSA_SSA_PKCS1_3072_SHA256_F4,
+      signature.signature_key_templates.RSA_SSA_PKCS1_4096_SHA512_F4,
+      signature.signature_key_templates.RSA_SSA_PSS_3072_SHA256_SHA256_32_F4,
+      signature.signature_key_templates.RSA_SSA_PSS_4096_SHA512_SHA512_64_F4,
+  ])
+  def test_sign_verify_success(self, template):
+    private_handle = tink.new_keyset_handle(template)
+    public_handle = private_handle.public_keyset_handle()
+    verifier = public_handle.primitive(signature.PublicKeyVerify)
+    signer = private_handle.primitive(signature.PublicKeySign)
 
     data = b'data'
     data_signature = signer.sign(data)
-
-    # Starts with a DER sequence
-    self.assertEqual(bytearray(data_signature)[0], 0x30)
-
     verifier.verify(data_signature, data)
 
-  def test_verify_wrong(self):
-    key_template = new_ecdsa_key_template(
-        common_pb2.SHA256, common_pb2.NIST_P256, ecdsa_pb2.DER, public=False)
-    priv_key = self.key_manager_sign.new_key_data(key_template)
-    pub_key = self.key_manager_sign.public_key_data(priv_key)
-
-    signer = self.key_manager_sign.primitive(priv_key)
-    verifier = self.key_manager_verify.primitive(pub_key)
-
-    data = b'data'
-    with self.assertRaises(core.TinkError):
-      verifier.verify(signer.sign(data), b'wrongdata')
+  @parameterized.parameters([
+      signature.signature_key_templates.ECDSA_P256,
+      signature.signature_key_templates.ECDSA_P384,
+      signature.signature_key_templates.ECDSA_P521,
+      signature.signature_key_templates.ECDSA_P256_IEEE_P1363,
+      signature.signature_key_templates.ECDSA_P384_IEEE_P1363,
+      signature.signature_key_templates.ECDSA_P521_IEEE_P1363,
+      signature.signature_key_templates.ED25519,
+      signature.signature_key_templates.RSA_SSA_PKCS1_3072_SHA256_F4,
+      signature.signature_key_templates.RSA_SSA_PKCS1_4096_SHA512_F4,
+      signature.signature_key_templates.RSA_SSA_PSS_3072_SHA256_SHA256_32_F4,
+      signature.signature_key_templates.RSA_SSA_PSS_4096_SHA512_SHA512_64_F4,
+  ])
+  def test_verify_wrong_fails(self, template):
+    private_handle = tink.new_keyset_handle(template)
+    public_handle = private_handle.public_keyset_handle()
+    verifier = public_handle.primitive(signature.PublicKeyVerify)
+    signer = private_handle.primitive(signature.PublicKeySign)
 
     with self.assertRaises(core.TinkError):
-      verifier.verify(b'wrongsignature', data)
+      verifier.verify(signer.sign(b'data'), b'wrongdata')
+
+    with self.assertRaises(core.TinkError):
+      verifier.verify(b'wrongsignature', b'data')
 
 
 if __name__ == '__main__':
diff --git a/testing/cross_language/BUILD.bazel b/testing/cross_language/BUILD.bazel
index e1ed097..a682699 100644
--- a/testing/cross_language/BUILD.bazel
+++ b/testing/cross_language/BUILD.bazel
@@ -71,9 +71,11 @@
     name = "deterministic_aead_test",
     srcs = ["deterministic_aead_test.py"],
     deps = [
+        "//util:keyset_builder",
         "//util:supported_key_types",
         "//util:testing_servers",
         requirement("absl-py"),
+        "@tink_py//tink/proto:tink_py_pb2",
         "@tink_py//tink:tink_python",
         "@tink_py//tink/daead",
     ],
@@ -95,9 +97,12 @@
     name = "mac_test",
     srcs = ["mac_test.py"],
     deps = [
+        "//util:keyset_builder",
         "//util:supported_key_types",
         "//util:testing_servers",
         requirement("absl-py"),
+        "@tink_py//tink/proto:common_py_pb2",
+        "@tink_py//tink/proto:tink_py_pb2",
         "@tink_py//tink:tink_python",
         "@tink_py//tink/mac",
     ],
diff --git a/testing/cross_language/aead_test.py b/testing/cross_language/aead_test.py
index f1dd417..4e7eb95 100644
--- a/testing/cross_language/aead_test.py
+++ b/testing/cross_language/aead_test.py
@@ -49,9 +49,10 @@
   @parameterized.parameters(
       supported_key_types.test_cases(supported_key_types.AEAD_KEY_TYPES))
   def test_encrypt_decrypt(self, key_template_name, supported_langs):
+    self.assertNotEmpty(supported_langs)
     key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
-    # use java to generate keys, as it supports all key types.
-    keyset = testing_servers.new_keyset('java', key_template)
+    # Take the first supported language to generate the keyset.
+    keyset = testing_servers.new_keyset(supported_langs[0], key_template)
     supported_aeads = [
         testing_servers.aead(lang, keyset) for lang in supported_langs
     ]
diff --git a/testing/cross_language/deterministic_aead_test.py b/testing/cross_language/deterministic_aead_test.py
index 9d3d29f..885530a 100644
--- a/testing/cross_language/deterministic_aead_test.py
+++ b/testing/cross_language/deterministic_aead_test.py
@@ -16,12 +16,25 @@
 
 import tink
 from tink import daead
+from tink.proto import tink_pb2
+from util import keyset_builder
 from util import supported_key_types
 from util import testing_servers
 
 SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['daead']
 
 
+def key_rotation_test_cases():
+  for enc_lang in SUPPORTED_LANGUAGES:
+    for dec_lang in SUPPORTED_LANGUAGES:
+      for prefix in [tink_pb2.RAW, tink_pb2.TINK]:
+        daead_templates = daead.deterministic_aead_key_templates
+        old_key_tmpl = daead_templates.create_aes_siv_key_template(64)
+        old_key_tmpl.output_prefix_type = prefix
+        new_key_tmpl = daead.deterministic_aead_key_templates.AES256_SIV
+        yield (enc_lang, dec_lang, old_key_tmpl, new_key_tmpl)
+
+
 def setUpModule():
   daead.register()
   testing_servers.start()
@@ -36,8 +49,10 @@
   @parameterized.parameters(
       supported_key_types.test_cases(supported_key_types.DAEAD_KEY_TYPES))
   def test_encrypt_decrypt(self, key_template_name, supported_langs):
+    self.assertNotEmpty(supported_langs)
     key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
-    keyset = testing_servers.new_keyset('java', key_template)
+    # Take the first supported language to generate the keyset.
+    keyset = testing_servers.new_keyset(supported_langs[0], key_template)
     supported_daeads = [
         testing_servers.deterministic_aead(lang, keyset)
         for lang in supported_langs
@@ -71,5 +86,72 @@
       with self.assertRaises(tink.TinkError):
         p.encrypt_deterministically(b'plaintext', b'associated_data')
 
+  @parameterized.parameters(key_rotation_test_cases())
+  def test_key_rotation(self, enc_lang, dec_lang, old_key_tmpl, new_key_tmpl):
+    # Do a key rotation from an old key generated from old_key_tmpl to a new
+    # key generated from new_key_tmpl. Encryption and decryption are done
+    # in languages enc_lang and dec_lang.
+    builder = keyset_builder.new_keyset_builder()
+    older_key_id = builder.add_new_key(old_key_tmpl)
+    builder.set_primary_key(older_key_id)
+    enc_daead1 = testing_servers.deterministic_aead(enc_lang, builder.keyset())
+    dec_daead1 = testing_servers.deterministic_aead(dec_lang, builder.keyset())
+    newer_key_id = builder.add_new_key(new_key_tmpl)
+    enc_daead2 = testing_servers.deterministic_aead(enc_lang, builder.keyset())
+    dec_daead2 = testing_servers.deterministic_aead(dec_lang, builder.keyset())
+
+    builder.set_primary_key(newer_key_id)
+    enc_daead3 = testing_servers.deterministic_aead(enc_lang, builder.keyset())
+    dec_daead3 = testing_servers.deterministic_aead(dec_lang, builder.keyset())
+
+    builder.disable_key(older_key_id)
+    enc_daead4 = testing_servers.deterministic_aead(enc_lang, builder.keyset())
+    dec_daead4 = testing_servers.deterministic_aead(dec_lang, builder.keyset())
+
+    self.assertNotEqual(older_key_id, newer_key_id)
+    # 1 encrypts with the older key. So 1, 2 and 3 can decrypt it, but not 4.
+    ciphertext1 = enc_daead1.encrypt_deterministically(b'plaintext', b'ad')
+    self.assertEqual(dec_daead1.decrypt_deterministically(ciphertext1, b'ad'),
+                     b'plaintext')
+    self.assertEqual(dec_daead2.decrypt_deterministically(ciphertext1, b'ad'),
+                     b'plaintext')
+    self.assertEqual(dec_daead3.decrypt_deterministically(ciphertext1, b'ad'),
+                     b'plaintext')
+    with self.assertRaises(tink.TinkError):
+      _ = dec_daead4.decrypt_deterministically(ciphertext1, b'ad')
+
+    # 2 encrypts with the older key. So 1, 2 and 3 can decrypt it, but not 4.
+    ciphertext2 = enc_daead2.encrypt_deterministically(b'plaintext', b'ad')
+    self.assertEqual(dec_daead1.decrypt_deterministically(ciphertext2, b'ad'),
+                     b'plaintext')
+    self.assertEqual(dec_daead2.decrypt_deterministically(ciphertext2, b'ad'),
+                     b'plaintext')
+    self.assertEqual(dec_daead3.decrypt_deterministically(ciphertext2, b'ad'),
+                     b'plaintext')
+    with self.assertRaises(tink.TinkError):
+      _ = dec_daead4.decrypt_deterministically(ciphertext2, b'ad')
+
+    # 3 encrypts with the newer key. So 2, 3 and 4 can decrypt it, but not 1.
+    ciphertext3 = enc_daead3.encrypt_deterministically(b'plaintext', b'ad')
+    with self.assertRaises(tink.TinkError):
+      _ = dec_daead1.decrypt_deterministically(ciphertext3, b'ad')
+    self.assertEqual(dec_daead2.decrypt_deterministically(ciphertext3, b'ad'),
+                     b'plaintext')
+    self.assertEqual(dec_daead3.decrypt_deterministically(ciphertext3, b'ad'),
+                     b'plaintext')
+    self.assertEqual(dec_daead4.decrypt_deterministically(ciphertext3, b'ad'),
+                     b'plaintext')
+
+    # 4 encrypts with the newer key. So 2, 3 and 4 can decrypt it, but not 1.
+    ciphertext4 = enc_daead4.encrypt_deterministically(b'plaintext', b'ad')
+    with self.assertRaises(tink.TinkError):
+      _ = dec_daead1.decrypt_deterministically(ciphertext4, b'ad')
+    self.assertEqual(dec_daead2.decrypt_deterministically(ciphertext4, b'ad'),
+                     b'plaintext')
+    self.assertEqual(dec_daead3.decrypt_deterministically(ciphertext4, b'ad'),
+                     b'plaintext')
+    self.assertEqual(dec_daead4.decrypt_deterministically(ciphertext4, b'ad'),
+                     b'plaintext')
+
 if __name__ == '__main__':
   absltest.main()
diff --git a/testing/cross_language/hybrid_encryption_test.py b/testing/cross_language/hybrid_encryption_test.py
index 71cfc10..2b9a9f8 100644
--- a/testing/cross_language/hybrid_encryption_test.py
+++ b/testing/cross_language/hybrid_encryption_test.py
@@ -38,8 +38,11 @@
       supported_key_types.test_cases(
           supported_key_types.HYBRID_PRIVATE_KEY_TYPES))
   def test_encrypt_decrypt(self, key_template_name, supported_langs):
+    self.assertNotEmpty(supported_langs)
     key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
-    private_keyset = testing_servers.new_keyset('java', key_template)
+    # Take the first supported language to generate the private keyset.
+    private_keyset = testing_servers.new_keyset(supported_langs[0],
+                                                key_template)
     supported_decs = [
         testing_servers.hybrid_decrypt(lang, private_keyset)
         for lang in supported_langs
diff --git a/testing/cross_language/json_test.py b/testing/cross_language/json_test.py
index d755253..971a804 100644
--- a/testing/cross_language/json_test.py
+++ b/testing/cross_language/json_test.py
@@ -82,8 +82,10 @@
   @parameterized.parameters(
       supported_key_types.test_cases(supported_key_types.ALL_KEY_TYPES))
   def test_to_from_json(self, key_template_name, supported_langs):
+    self.assertNotEmpty(supported_langs)
     key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
-    keyset = testing_servers.new_keyset('java', key_template)
+    # Take the first supported language to generate the keyset.
+    keyset = testing_servers.new_keyset(supported_langs[0], key_template)
     for to_lang in supported_langs:
       json_keyset = testing_servers.keyset_to_json(to_lang, keyset)
       for from_lang in supported_langs:
diff --git a/testing/cross_language/mac_test.py b/testing/cross_language/mac_test.py
index 785cd53..dda9885 100644
--- a/testing/cross_language/mac_test.py
+++ b/testing/cross_language/mac_test.py
@@ -17,12 +17,26 @@
 import tink
 from tink import mac
 
+from tink.proto import common_pb2
+from tink.proto import tink_pb2
+from util import keyset_builder
 from util import supported_key_types
 from util import testing_servers
 
 SUPPORTED_LANGUAGES = testing_servers.SUPPORTED_LANGUAGES_BY_PRIMITIVE['mac']
 
 
+def key_rotation_test_cases():
+  for compute_lang in SUPPORTED_LANGUAGES:
+    for verify_lang in SUPPORTED_LANGUAGES:
+      for prefix in [tink_pb2.RAW, tink_pb2.TINK]:
+        old_key_tmpl = mac.mac_key_templates.create_hmac_key_template(
+            key_size=32, tag_size=16, hash_type=common_pb2.SHA256)
+        old_key_tmpl.output_prefix_type = prefix
+        new_key_tmpl = mac.mac_key_templates.HMAC_SHA512_512BITTAG
+        yield (compute_lang, verify_lang, old_key_tmpl, new_key_tmpl)
+
+
 def setUpModule():
   mac.register()
   testing_servers.start()
@@ -37,8 +51,10 @@
   @parameterized.parameters(
       supported_key_types.test_cases(supported_key_types.MAC_KEY_TYPES))
   def test_encrypt_decrypt(self, key_template_name, supported_langs):
+    self.assertNotEmpty(supported_langs)
     key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
-    keyset = testing_servers.new_keyset('java', key_template)
+    # Take the first supported language to generate the keyset.
+    keyset = testing_servers.new_keyset(supported_langs[0], key_template)
     supported_macs = [
         testing_servers.mac(lang, keyset) for lang in supported_langs
     ]
@@ -62,6 +78,61 @@
       with self.assertRaises(tink.TinkError):
         p.compute_mac(data)
 
+  @parameterized.parameters(key_rotation_test_cases())
+  def test_key_rotation(
+      self, compute_lang, verify_lang, old_key_tmpl, new_key_tmpl):
+    # Do a key rotation from an old key generated from old_key_tmpl to a new
+    # key generated from new_key_tmpl. MAC computation and verification are done
+    # in languages compute_lang and verify_lang.
+    builder = keyset_builder.new_keyset_builder()
+    older_key_id = builder.add_new_key(old_key_tmpl)
+    builder.set_primary_key(older_key_id)
+    compute_mac1 = testing_servers.mac(compute_lang, builder.keyset())
+    verify_mac1 = testing_servers.mac(verify_lang, builder.keyset())
+    newer_key_id = builder.add_new_key(new_key_tmpl)
+    compute_mac2 = testing_servers.mac(compute_lang, builder.keyset())
+    verify_mac2 = testing_servers.mac(verify_lang, builder.keyset())
+
+    builder.set_primary_key(newer_key_id)
+    compute_mac3 = testing_servers.mac(compute_lang, builder.keyset())
+    verify_mac3 = testing_servers.mac(verify_lang, builder.keyset())
+
+    builder.disable_key(older_key_id)
+    compute_mac4 = testing_servers.mac(compute_lang, builder.keyset())
+    verify_mac4 = testing_servers.mac(verify_lang, builder.keyset())
+
+    self.assertNotEqual(older_key_id, newer_key_id)
+    # 1 uses the older key. So 1, 2 and 3 can verify the mac, but not 4.
+    mac_value1 = compute_mac1.compute_mac(b'plaintext')
+    verify_mac1.verify_mac(mac_value1, b'plaintext')
+    verify_mac2.verify_mac(mac_value1, b'plaintext')
+    verify_mac3.verify_mac(mac_value1, b'plaintext')
+    with self.assertRaises(tink.TinkError):
+      verify_mac4.verify_mac(mac_value1, b'plaintext')
+
+    # 2 uses the older key. So 1, 2 and 3 can verify the mac, but not 4.
+    mac_value2 = compute_mac2.compute_mac(b'plaintext')
+    verify_mac1.verify_mac(mac_value2, b'plaintext')
+    verify_mac2.verify_mac(mac_value2, b'plaintext')
+    verify_mac3.verify_mac(mac_value2, b'plaintext')
+    with self.assertRaises(tink.TinkError):
+      verify_mac4.verify_mac(mac_value2, b'plaintext')
+
+    # 3 uses the newer key. So 2, 3 and 4 can verify the mac, but not 1.
+    mac_value3 = compute_mac3.compute_mac(b'plaintext')
+    with self.assertRaises(tink.TinkError):
+      verify_mac1.verify_mac(mac_value3, b'plaintext')
+    verify_mac2.verify_mac(mac_value3, b'plaintext')
+    verify_mac3.verify_mac(mac_value3, b'plaintext')
+    verify_mac4.verify_mac(mac_value3, b'plaintext')
+
+    # 4 uses the newer key. So 2, 3 and 4 can verify the mac, but not 1.
+    mac_value4 = compute_mac4.compute_mac(b'plaintext')
+    with self.assertRaises(tink.TinkError):
+      verify_mac1.verify_mac(mac_value4, b'plaintext')
+    verify_mac2.verify_mac(mac_value4, b'plaintext')
+    verify_mac3.verify_mac(mac_value4, b'plaintext')
+    verify_mac4.verify_mac(mac_value4, b'plaintext')
 
 if __name__ == '__main__':
   absltest.main()
diff --git a/testing/cross_language/signature_test.py b/testing/cross_language/signature_test.py
index 3c508e8..ede4f15 100644
--- a/testing/cross_language/signature_test.py
+++ b/testing/cross_language/signature_test.py
@@ -38,8 +38,11 @@
   @parameterized.parameters(
       supported_key_types.test_cases(supported_key_types.SIGNATURE_KEY_TYPES))
   def test_encrypt_decrypt(self, key_template_name, supported_langs):
+    self.assertNotEmpty(supported_langs)
     key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
-    private_keyset = testing_servers.new_keyset('java', key_template)
+    # Take the first supported language to generate the private keyset.
+    private_keyset = testing_servers.new_keyset(supported_langs[0],
+                                                key_template)
     supported_signers = [
         testing_servers.public_key_sign(lang, private_keyset)
         for lang in supported_langs
diff --git a/testing/cross_language/streaming_aead_test.py b/testing/cross_language/streaming_aead_test.py
index befe0ba..55990d7 100644
--- a/testing/cross_language/streaming_aead_test.py
+++ b/testing/cross_language/streaming_aead_test.py
@@ -41,9 +41,10 @@
       supported_key_types.test_cases(
           supported_key_types.STREAMING_AEAD_KEY_TYPES))
   def test_encrypt_decrypt(self, key_template_name, supported_langs):
+    self.assertNotEmpty(supported_langs)
     key_template = supported_key_types.KEY_TEMPLATE[key_template_name]
-    # Use java to generate keys, as it supports all key types.
-    keyset = testing_servers.new_keyset('java', key_template)
+    # Take the first supported language to generate the keyset.
+    keyset = testing_servers.new_keyset(supported_langs[0], key_template)
     supported_streaming_aeads = [
         testing_servers.streaming_aead(lang, keyset) for lang in supported_langs
     ]
diff --git a/testing/cross_language/util/supported_key_types.py b/testing/cross_language/util/supported_key_types.py
index 91c7fa5..f3929a3 100644
--- a/testing/cross_language/util/supported_key_types.py
+++ b/testing/cross_language/util/supported_key_types.py
@@ -31,6 +31,7 @@
 AEAD_KEY_TYPES = [
     'AesEaxKey',
     'AesGcmKey',
+    'AesGcmSivKey',
     'AesCtrHmacAeadKey',
     'ChaCha20Poly1305Key',
     'XChaCha20Poly1305Key',
@@ -56,6 +57,7 @@
 SUPPORTED_LANGUAGES = {
     'AesEaxKey': ['cc', 'java', 'python'],
     'AesGcmKey': ['cc', 'java', 'go', 'python'],
+    'AesGcmSivKey': ['cc', 'python'],
     'AesCtrHmacAeadKey': ['cc', 'java', 'go', 'python'],
     'ChaCha20Poly1305Key': ['java', 'go'],
     'XChaCha20Poly1305Key': ['cc', 'java', 'go', 'python'],
@@ -78,6 +80,7 @@
 KEY_TEMPLATE_NAMES = {
     'AesEaxKey': ['AES128_EAX', 'AES256_EAX'],
     'AesGcmKey': ['AES128_GCM', 'AES256_GCM'],
+    'AesGcmSivKey': ['AES128_GCM_SIV', 'AES256_GCM_SIV'],
     'AesCtrHmacAeadKey': ['AES128_CTR_HMAC_SHA256', 'AES256_CTR_HMAC_SHA256'],
     'ChaCha20Poly1305Key': ['CHACHA20_POLY1305'],
     'XChaCha20Poly1305Key': ['XCHACHA20_POLY1305'],
@@ -130,6 +133,10 @@
         aead.aead_key_templates.AES128_GCM,
     'AES256_GCM':
         aead.aead_key_templates.AES256_GCM,
+    'AES128_GCM_SIV':
+        aead.aead_key_templates.AES128_GCM_SIV,
+    'AES256_GCM_SIV':
+        aead.aead_key_templates.AES256_GCM_SIV,
     'AES128_CTR_HMAC_SHA256':
         aead.aead_key_templates.AES128_CTR_HMAC_SHA256,
     'AES256_CTR_HMAC_SHA256':
diff --git a/tink_version.bzl b/tink_version.bzl
index 092c7ac..364dba2 100644
--- a/tink_version.bzl
+++ b/tink_version.bzl
@@ -1,2 +1,2 @@
 """ Version of the current release of Tink """
-TINK_VERSION_LABEL = "1.4.0-rc2"
+TINK_VERSION_LABEL = "1.4.0"
diff --git a/tink_version.cmake b/tink_version.cmake
index b50e850..c81e0a0 100644
--- a/tink_version.cmake
+++ b/tink_version.cmake
@@ -1,2 +1,2 @@
 # Version of the current release of Tink.
-set(TINK_VERSION_LABEL 1.4.0-rc2)
+set(TINK_VERSION_LABEL 1.4.0)
diff --git a/tools/release_tinkey.sh b/tools/release_tinkey.sh
new file mode 100755
index 0000000..bba7236
--- /dev/null
+++ b/tools/release_tinkey.sh
@@ -0,0 +1,118 @@
+#!/bin/bash
+
+# This script creates a Tinkey distribution and uploads it to Google Cloud
+# Storage.
+# Prerequisites:
+#   - Google Cloud SDK: https://cloud.google.com/sdk/install
+#   - Write access to the "tinkey" GCS bucket. Ping tink-dev@.
+#   - bazel: https://bazel.build/
+
+usage() {
+  echo "Usage: $0 [-dh] <version>"
+  echo "  -d: Dry run. Only execute idempotent commands (default: FALSE)."
+  echo "  -h: Help. Print this usage information."
+  exit 1
+}
+
+# Process flags.
+
+DRY_RUN="false"
+
+while getopts "dh" opt; do
+  case "${opt}" in
+    d) DRY_RUN="true" ;;
+    h) usage ;;
+    *) usage ;;
+  esac
+done
+shift $((OPTIND - 1))
+
+readonly DRY_RUN
+
+# Process script arguments.
+
+VERSION="$1"
+shift 1
+
+if [ -z "${VERSION}" ]; then
+  VERSION="snapshot"
+fi
+
+if [[ "${VERSION}" =~ " " ]]; then
+  echo "Version name must not have any spaces"
+  exit 3
+fi
+
+# Set up parameters.
+
+readonly GCS_LOCATION="gs://tinkey/"
+
+readonly PLATFORM="$(uname -ms | tr '[:upper:]' '[:lower:]' | tr ' ' '-')"
+
+readonly TMP_DIR="$(mktemp -dt tinkey.XXXXXX)"
+
+do_command() {
+  if ! "$@"; then
+    echo "*** Failed executing command. ***"
+    echo "Failed command: $@"
+    exit 1
+  fi
+  return $?
+}
+
+print_command() {
+  printf '%q ' '+' "$@"
+  echo
+}
+
+print_and_do() {
+  print_command "$@"
+  do_command "$@"
+  return $?
+}
+
+do_if_not_dry_run() {
+  # $@ is an array containing a command to be executed and its arguments.
+  print_command "$@"
+  if [[ "${DRY_RUN}" == "true" ]]; then
+    echo "  *** Dry run, command not executed. ***"
+    return 0
+  fi
+  do_command "$@"
+  return $?
+}
+
+build_tinkey() {
+  print_and_do cd tools
+
+  print_and_do bazel build tinkey:tinkey_deploy.jar
+
+  print_and_do cp bazel-bin/tinkey/tinkey_deploy.jar "${TMP_DIR}"
+
+  print_and_do cd "${TMP_DIR}"
+
+  cat <<EOF > tinkey
+#!/usr/bin/env sh
+
+java -jar "\$(dirname \$0)/tinkey_deploy.jar" "\$@"
+EOF
+
+  chmod 755 tinkey
+
+  print_and_do tar -czvpf "tinkey-${PLATFORM}-${VERSION}.tar.gz" tinkey_deploy.jar tinkey
+}
+
+upload_to_gcs() {
+  print_and_do cd "${TMP_DIR}"
+
+  shasum -a 256 "tinkey-${PLATFORM}-${VERSION}.tar.gz"
+
+  do_if_not_dry_run gsutil cp "tinkey-${PLATFORM}-${VERSION}.tar.gz" "${GCS_LOCATION}"
+}
+
+main() {
+  build_tinkey
+  upload_to_gcs
+}
+
+main "$@"