Add SlhDsaPrivateKey::CreateFromSeed method.

PiperOrigin-RevId: 959035397
Change-Id: I96fa98f6d0dc52263f7967b87e397a0c777fa70f
diff --git a/tink/signature/BUILD.bazel b/tink/signature/BUILD.bazel
index fc937b2..8603097 100644
--- a/tink/signature/BUILD.bazel
+++ b/tink/signature/BUILD.bazel
@@ -1002,10 +1002,13 @@
         "//tink:key",
         "//tink:partial_key_access_token",
         "//tink:restricted_data",
+        "//tink:secret_data",
         "//tink/internal:call_with_core_dump_protection",
         "//tink/internal:dfsan_forwarders",
         "//tink/internal:fips_utils",
+        "//tink/internal:secret_buffer",
         "//tink/signature/internal:slh_dsa_parameter_set",
+        "//tink/util:secret_data",
         "@abseil-cpp//absl/status",
         "@abseil-cpp//absl/status:statusor",
         "@abseil-cpp//absl/strings:string_view",
@@ -2010,8 +2013,13 @@
         "//tink:insecure_secret_key_access",
         "//tink:key",
         "//tink:partial_key_access",
+        "//tink:public_key_sign",
+        "//tink:public_key_verify",
         "//tink:restricted_data",
         "//tink/internal:fips_utils",
+        "//tink/signature/internal:slh_dsa_parameter_set",
+        "//tink/signature/internal:slh_dsa_sign_boringssl",
+        "//tink/signature/internal:slh_dsa_verify_boringssl",
         "//tink/subtle:random",
         "@abseil-cpp//absl/status",
         "@abseil-cpp//absl/status:status_matchers",
diff --git a/tink/signature/CMakeLists.txt b/tink/signature/CMakeLists.txt
index 78ff21d..08a18e9 100644
--- a/tink/signature/CMakeLists.txt
+++ b/tink/signature/CMakeLists.txt
@@ -1896,10 +1896,13 @@
     tink::core::key
     tink::core::partial_key_access_token
     tink::core::restricted_data
+    tink::core::secret_data
     tink::internal::call_with_core_dump_protection
     tink::internal::dfsan_forwarders
     tink::internal::fips_utils
+    tink::internal::secret_buffer
     tink::signature::internal::slh_dsa_parameter_set
+    tink::util::secret_data
 )
 
 tink_cc_test(
@@ -1919,8 +1922,13 @@
     tink::core::insecure_secret_key_access
     tink::core::key
     tink::core::partial_key_access
+    tink::core::public_key_sign
+    tink::core::public_key_verify
     tink::core::restricted_data
     tink::internal::fips_utils
+    tink::signature::internal::slh_dsa_parameter_set
+    tink::signature::internal::slh_dsa_sign_boringssl
+    tink::signature::internal::slh_dsa_verify_boringssl
     tink::subtle::random
   TAGS
     exclude_if_openssl
diff --git a/tink/signature/internal/slh_dsa_parameter_set.h b/tink/signature/internal/slh_dsa_parameter_set.h
index 9d90404..00b3d64 100644
--- a/tink/signature/internal/slh_dsa_parameter_set.h
+++ b/tink/signature/internal/slh_dsa_parameter_set.h
@@ -30,6 +30,9 @@
   static SlhDsaParameterSet Shake_256f();
 
   SlhDsaParameters::HashType GetHashType() const { return hash_type_; }
+  int GetPrivateSeedSizeInBytes() const {
+    return private_key_size_in_bytes_ * 3 / 4;
+  }
   int GetPrivateKeySizeInBytes() const { return private_key_size_in_bytes_; }
   int GetPublicKeySizeInBytes() const { return private_key_size_in_bytes_ / 2; }
   SlhDsaParameters::SignatureType GetSignatureType() const {
diff --git a/tink/signature/internal/slh_dsa_parameter_set_test.cc b/tink/signature/internal/slh_dsa_parameter_set_test.cc
index a9876c2..8e8c59c 100644
--- a/tink/signature/internal/slh_dsa_parameter_set_test.cc
+++ b/tink/signature/internal/slh_dsa_parameter_set_test.cc
@@ -35,6 +35,7 @@
 struct TestCase {
   SlhDsaParameterSet parameter_set;
   SlhDsaParameters::HashType hash_type;
+  int private_seed_size_in_bytes;
   int private_key_size_in_bytes;
   int public_key_size_in_bytes;
   SlhDsaParameters::SignatureType signature_type;
@@ -47,16 +48,18 @@
     ::testing::Values(
         // SLH-DSA-SHA2-128s
         TestCase{SlhDsaParameterSet::Sha2_128s(),
-                 SlhDsaParameters::HashType::kSha2, 64, 32,
+                 SlhDsaParameters::HashType::kSha2, 48, 64, 32,
                  SlhDsaParameters::SignatureType::kSmallSignature},
         // SLH-DSA-SHAKE-256f
         TestCase{SlhDsaParameterSet::Shake_256f(),
-                 SlhDsaParameters::HashType::kShake, 128, 64,
+                 SlhDsaParameters::HashType::kShake, 96, 128, 64,
                  SlhDsaParameters::SignatureType::kFastSigning}));
 
 TEST_P(SlhDsaParameterSetTest, Getters) {
   const TestCase& test_case = GetParam();
   EXPECT_THAT(test_case.parameter_set.GetHashType(), Eq(test_case.hash_type));
+  EXPECT_THAT(test_case.parameter_set.GetPrivateSeedSizeInBytes(),
+              Eq(test_case.private_seed_size_in_bytes));
   EXPECT_THAT(test_case.parameter_set.GetPrivateKeySizeInBytes(),
               Eq(test_case.private_key_size_in_bytes));
   EXPECT_THAT(test_case.parameter_set.GetPublicKeySizeInBytes(),
diff --git a/tink/signature/internal/slh_dsa_sign_boringssl.cc b/tink/signature/internal/slh_dsa_sign_boringssl.cc
index 7a2ff74..0e6f81b 100644
--- a/tink/signature/internal/slh_dsa_sign_boringssl.cc
+++ b/tink/signature/internal/slh_dsa_sign_boringssl.cc
@@ -29,13 +29,13 @@
 // OPENSSL_IS_BORINGSSL. So we include this common header upfront here to
 // "force" the definition of OPENSSL_IS_BORINGSSL in case BoringSSL is used.
 #include "openssl/crypto.h"
-#ifdef OPENSSL_IS_BORINGSSL
+#include "tink/internal/fips_utils.h"  // IWYU pragma: keep
+#if defined(OPENSSL_IS_BORINGSSL) && !defined(TINK_USE_ONLY_FIPS)
 #include "openssl/slhdsa.h"
-#endif  // OPENSSL_IS_BORINGSSL
+#endif
 #include "tink/insecure_secret_key_access.h"
 #include "tink/internal/call_with_core_dump_protection.h"
 #include "tink/internal/dfsan_forwarders.h"
-#include "tink/internal/fips_utils.h"
 #include "tink/partial_key_access.h"
 #include "tink/public_key_sign.h"
 #include "tink/signature/internal/slh_dsa_parameter_set.h"
@@ -48,7 +48,7 @@
 
 namespace {
 
-#ifdef OPENSSL_IS_BORINGSSL
+#if defined(OPENSSL_IS_BORINGSSL) && !defined(TINK_USE_ONLY_FIPS)
 using SignFunc = int (*)(uint8_t*, const uint8_t*, const uint8_t*, size_t,
                          const uint8_t*, size_t);
 
@@ -133,15 +133,15 @@
 
   return signature;
 }
-#endif  // OPENSSL_IS_BORINGSSL
+#endif
 
 }  // namespace
 
 absl::StatusOr<std::unique_ptr<PublicKeySign>> NewSlhDsaSignBoringSsl(
     const SlhDsaPrivateKey& private_key) {
-#ifndef OPENSSL_IS_BORINGSSL
+#if !defined(OPENSSL_IS_BORINGSSL) || defined(TINK_USE_ONLY_FIPS)
   return absl::UnimplementedError(
-      "SLH-DSA is only supported in BoringSSL builds.");
+      "SLH-DSA is only supported in non-FIPS BoringSSL builds.");
 #else
   auto status = internal::CheckFipsCompatibility<SlhDsaSignBoringSsl>();
   if (!status.ok()) {
@@ -154,7 +154,7 @@
   }
 
   return {std::make_unique<SlhDsaSignBoringSsl>(std::move(private_key))};
-#endif  // OPENSSL_IS_BORINGSSL
+#endif
 }
 
 }  // namespace internal
diff --git a/tink/signature/internal/slh_dsa_verify_boringssl.cc b/tink/signature/internal/slh_dsa_verify_boringssl.cc
index eae3783..88aebac 100644
--- a/tink/signature/internal/slh_dsa_verify_boringssl.cc
+++ b/tink/signature/internal/slh_dsa_verify_boringssl.cc
@@ -30,10 +30,10 @@
 // OPENSSL_IS_BORINGSSL. So we include this common header upfront here to
 // "force" the definition of OPENSSL_IS_BORINGSSL in case BoringSSL is used.
 #include "openssl/crypto.h"
-#ifdef OPENSSL_IS_BORINGSSL
+#include "tink/internal/fips_utils.h"  // IWYU pragma: keep
+#if defined(OPENSSL_IS_BORINGSSL) && !defined(TINK_USE_ONLY_FIPS)
 #include "openssl/slhdsa.h"
-#endif  // OPENSSL_IS_BORINGSSL
-#include "tink/internal/fips_utils.h"
+#endif  // defined(OPENSSL_IS_BORINGSSL) && !defined(TINK_USE_ONLY_FIPS)
 #include "tink/partial_key_access.h"
 #include "tink/public_key_verify.h"
 #include "tink/signature/internal/slh_dsa_parameter_set.h"
@@ -45,7 +45,7 @@
 
 namespace {
 
-#ifdef OPENSSL_IS_BORINGSSL
+#if defined(OPENSSL_IS_BORINGSSL) && !defined(TINK_USE_ONLY_FIPS)
 // Public Key Verification using SLH-DSA-SHA2-128s implementation from
 // BoringSSL.
 namespace {
@@ -143,15 +143,15 @@
  private:
   SlhDsaPublicKey public_key_;
 };
-#endif  // OPENSSL_IS_BORINGSSL
+#endif
 
 }  // namespace
 
 absl::StatusOr<std::unique_ptr<PublicKeyVerify>> NewSlhDsaVerifyBoringSsl(
     const SlhDsaPublicKey& public_key) {
-#ifndef OPENSSL_IS_BORINGSSL
+#if !defined(OPENSSL_IS_BORINGSSL) || defined(TINK_USE_ONLY_FIPS)
   return absl::UnimplementedError(
-      "SLH-DSA is only supported in BoringSSL builds.");
+      "SLH-DSA is only supported in non-FIPS BoringSSL builds.");
 #else
   auto status = internal::CheckFipsCompatibility<SlhDsaVerifyBoringSsl>();
   if (!status.ok()) {
diff --git a/tink/signature/slh_dsa_private_key.cc b/tink/signature/slh_dsa_private_key.cc
index 4052a1f..9efc627 100644
--- a/tink/signature/slh_dsa_private_key.cc
+++ b/tink/signature/slh_dsa_private_key.cc
@@ -28,6 +28,10 @@
 // "force" the definition of OPENSSL_IS_BORINGSSL in case BoringSSL is used.
 #include "openssl/crypto.h"
 #include "tink/internal/fips_utils.h"  // IWYU pragma: keep
+#include "tink/internal/secret_buffer.h"
+#include "tink/secret_data.h"
+#include "tink/signature/slh_dsa_parameters.h"
+#include "tink/util/secret_data.h"
 #if defined(OPENSSL_IS_BORINGSSL) && !defined(TINK_USE_ONLY_FIPS)
 #include "openssl/mem.h"
 #include "openssl/slhdsa.h"
@@ -50,15 +54,41 @@
 namespace {
 
 #if defined(OPENSSL_IS_BORINGSSL) && !defined(TINK_USE_ONLY_FIPS)
-using PublicFromPrivateFunc = void (*)(uint8_t*, const uint8_t*);
-
-absl::StatusOr<PublicFromPrivateFunc> GetPublicFromPrivateFunc(
-    const internal::SlhDsaParameterSet& parameter_set) {
+absl::Status GetPublicFromPrivate(
+    const internal::SlhDsaParameterSet& parameter_set,
+    std::string& public_key_bytes, const SecretData& private_key_bytes) {
   if (parameter_set == internal::SlhDsaParameterSet::Sha2_128s()) {
-    return SLHDSA_SHA2_128S_public_from_private;
+    SLHDSA_SHA2_128S_public_from_private(
+        reinterpret_cast<uint8_t*>(&public_key_bytes[0]),
+        private_key_bytes.data());
+    return absl::OkStatus();
   }
   if (parameter_set == internal::SlhDsaParameterSet::Shake_256f()) {
-    return SLHDSA_SHAKE_256F_public_from_private;
+    SLHDSA_SHAKE_256F_public_from_private(
+        reinterpret_cast<uint8_t*>(&public_key_bytes[0]),
+        private_key_bytes.data());
+    return absl::OkStatus();
+  }
+  return absl::Status(
+      absl::StatusCode::kInvalidArgument,
+      "SLH-DSA parameter combination is not supported by BoringSSL.");
+}
+
+absl::Status GetKeyPairFromSeed(
+    const internal::SlhDsaParameterSet& parameter_set,
+    std::string& public_key_bytes, internal::SecretBuffer& private_key_bytes,
+    const SecretData& private_seed_bytes) {
+  if (parameter_set == internal::SlhDsaParameterSet::Sha2_128s()) {
+    SLHDSA_SHA2_128S_generate_key_from_seed(
+        reinterpret_cast<uint8_t*>(&public_key_bytes[0]),
+        private_key_bytes.data(), private_seed_bytes.data());
+    return absl::OkStatus();
+  }
+  if (parameter_set == internal::SlhDsaParameterSet::Shake_256f()) {
+    SLHDSA_SHAKE_256F_generate_key_from_seed(
+        reinterpret_cast<uint8_t*>(&public_key_bytes[0]),
+        private_key_bytes.data(), private_seed_bytes.data());
+    return absl::OkStatus();
   }
   return absl::Status(
       absl::StatusCode::kInvalidArgument,
@@ -97,29 +127,23 @@
 
   int public_key_size = parameter_set->GetPublicKeySizeInBytes();
 
-  absl::StatusOr<PublicFromPrivateFunc> public_from_private_func =
-      GetPublicFromPrivateFunc(*parameter_set);
-  if (!public_from_private_func.ok()) {
-    return public_from_private_func.status();
-  }
-  PublicFromPrivateFunc public_from_private_function =
-      *public_from_private_func;
-
   // Confirm that the private key and public key are a valid SLH-DSA key pair.
   std::string public_key_bytes_regen;
   public_key_bytes_regen.resize(public_key_size);
 
+  absl::Status status = absl::OkStatus();
   internal::CallWithCoreDumpProtection([&]() {
     internal::ScopedAssumeRegionCoreDumpSafe scope(&public_key_bytes_regen[0],
                                                    public_key_size);
 
-    public_from_private_function(
-        reinterpret_cast<uint8_t*>(&public_key_bytes_regen[0]),
-        reinterpret_cast<const uint8_t*>(
-            private_key_bytes.GetSecret(InsecureSecretKeyAccess::Get())
-                .data()));
+    status = GetPublicFromPrivate(
+        *parameter_set, public_key_bytes_regen,
+        private_key_bytes.Get(InsecureSecretKeyAccess::Get()));
     internal::DfsanClearLabel(&public_key_bytes_regen[0], public_key_size);
   });
+  if (!status.ok()) {
+    return status;
+  }
 
   absl::string_view expected_public_key_bytes =
       public_key.GetPublicKeyBytes(token);
@@ -135,8 +159,7 @@
 }
 
 absl::StatusOr<SlhDsaPrivateKey> SlhDsaPrivateKey::Create(
-    const SlhDsaParameters& parameters,
-    const RestrictedData& private_key_bytes,
+    const SlhDsaParameters& parameters, const RestrictedData& private_key_bytes,
     absl::optional<int> id_requirement, PartialKeyAccessToken token) {
 #if !defined(OPENSSL_IS_BORINGSSL) || defined(TINK_USE_ONLY_FIPS)
   return absl::UnimplementedError(
@@ -163,28 +186,22 @@
 
   int public_key_size = parameter_set->GetPublicKeySizeInBytes();
 
-  absl::StatusOr<PublicFromPrivateFunc> public_from_private_func =
-      GetPublicFromPrivateFunc(*parameter_set);
-  if (!public_from_private_func.ok()) {
-    return public_from_private_func.status();
-  }
-  PublicFromPrivateFunc public_from_private_function =
-      *public_from_private_func;
-
   std::string public_key_bytes;
   public_key_bytes.resize(public_key_size);
 
+  absl::Status status = absl::OkStatus();
   internal::CallWithCoreDumpProtection([&]() {
     internal::ScopedAssumeRegionCoreDumpSafe scope(&public_key_bytes[0],
                                                    public_key_size);
 
-    public_from_private_function(
-        reinterpret_cast<uint8_t*>(&public_key_bytes[0]),
-        reinterpret_cast<const uint8_t*>(
-            private_key_bytes.GetSecret(InsecureSecretKeyAccess::Get())
-                .data()));
+    absl::Status status = GetPublicFromPrivate(
+        *parameter_set, public_key_bytes,
+        private_key_bytes.Get(InsecureSecretKeyAccess::Get()));
     internal::DfsanClearLabel(&public_key_bytes[0], public_key_size);
   });
+  if (!status.ok()) {
+    return status;
+  }
 
   absl::StatusOr<SlhDsaPublicKey> public_key = SlhDsaPublicKey::Create(
       parameters, public_key_bytes, id_requirement, token);
@@ -196,6 +213,67 @@
 #endif
 }
 
+absl::StatusOr<SlhDsaPrivateKey> SlhDsaPrivateKey::CreateFromSeed(
+    const SlhDsaParameters& parameters,
+    const RestrictedData& private_seed_bytes,
+    absl::optional<int> id_requirement, PartialKeyAccessToken token) {
+#if !defined(OPENSSL_IS_BORINGSSL) || defined(TINK_USE_ONLY_FIPS)
+  return absl::UnimplementedError(
+      "SLH-DSA is only supported in non-FIPS BoringSSL builds.");
+#else
+  // Only 48-byte, 72-byte and 96-byte private keys are supported.
+  if (private_seed_bytes.size() != 48 && private_seed_bytes.size() != 72 &&
+      private_seed_bytes.size() != 96) {
+    return absl::Status(
+        absl::StatusCode::kInvalidArgument,
+        "SLH-DSA private seed length must be 48, 72, or 96 bytes.");
+  }
+
+  absl::StatusOr<internal::SlhDsaParameterSet> parameter_set =
+      internal::GetSlhDsaParameterSet(parameters);
+  if (!parameter_set.ok()) {
+    return parameter_set.status();
+  }
+
+  if (parameter_set->GetPrivateSeedSizeInBytes() != private_seed_bytes.size()) {
+    return absl::Status(absl::StatusCode::kInvalidArgument,
+                        "Private key size does not match parameters");
+  }
+
+  int public_key_size = parameter_set->GetPublicKeySizeInBytes();
+  int private_key_size = parameter_set->GetPrivateKeySizeInBytes();
+
+  std::string public_key_bytes;
+  public_key_bytes.resize(public_key_size);
+  internal::SecretBuffer private_key_bytes(private_key_size);
+
+  absl::Status status = absl::OkStatus();
+  internal::CallWithCoreDumpProtection([&]() {
+    internal::ScopedAssumeRegionCoreDumpSafe scope(&public_key_bytes[0],
+                                                   public_key_size);
+
+    status = GetKeyPairFromSeed(
+        *parameter_set, public_key_bytes, private_key_bytes,
+        private_seed_bytes.Get(InsecureSecretKeyAccess::Get()));
+    internal::DfsanClearLabel(&public_key_bytes[0], public_key_size);
+  });
+  if (!status.ok()) {
+    return status;
+  }
+
+  absl::StatusOr<SlhDsaPublicKey> public_key = SlhDsaPublicKey::Create(
+      parameters, public_key_bytes, id_requirement, token);
+  if (!public_key.ok()) {
+    return public_key.status();
+  }
+
+  return SlhDsaPrivateKey(
+      *public_key,
+      RestrictedData(util::internal::AsSecretData(private_key_bytes),
+                     InsecureSecretKeyAccess::Get()));
+#endif
+}
+
 bool SlhDsaPrivateKey::operator==(const Key& other) const {
   const SlhDsaPrivateKey* that = dynamic_cast<const SlhDsaPrivateKey*>(&other);
   if (that == nullptr) {
diff --git a/tink/signature/slh_dsa_private_key.h b/tink/signature/slh_dsa_private_key.h
index dd64a88..259fdbb 100644
--- a/tink/signature/slh_dsa_private_key.h
+++ b/tink/signature/slh_dsa_private_key.h
@@ -52,11 +52,24 @@
   // `parameters`.
   //
   // This function unconditionally returns an error in non-BoringSSL builds.
+  //
+  // Warning: This function assumes that `private_key_bytes` is a valid private
+  // key. In particular, it cannot be filled with random bytes. If you need to
+  // create a private key from a random seed, use `CreateFromSeed` instead.
   static absl::StatusOr<SlhDsaPrivateKey> Create(
       const SlhDsaParameters& parameters,
       const RestrictedData& private_key_bytes,
       absl::optional<int> id_requirement, PartialKeyAccessToken token);
 
+  // Creates a new SLH-DSA private key from `private_seed_bytes` and
+  // `parameters`.
+  //
+  // This function unconditionally returns an error in non-BoringSSL builds.
+  static absl::StatusOr<SlhDsaPrivateKey> CreateFromSeed(
+      const SlhDsaParameters& parameters,
+      const RestrictedData& private_seed_bytes,
+      absl::optional<int> id_requirement, PartialKeyAccessToken token);
+
   const RestrictedData& GetPrivateKeyBytes(PartialKeyAccessToken token) const {
     return private_key_bytes_;
   }
diff --git a/tink/signature/slh_dsa_private_key_test.cc b/tink/signature/slh_dsa_private_key_test.cc
index 600d450..f677666 100644
--- a/tink/signature/slh_dsa_private_key_test.cc
+++ b/tink/signature/slh_dsa_private_key_test.cc
@@ -34,7 +34,12 @@
 #include "tink/insecure_secret_key_access.h"
 #include "tink/key.h"
 #include "tink/partial_key_access.h"
+#include "tink/public_key_sign.h"
+#include "tink/public_key_verify.h"
 #include "tink/restricted_data.h"
+#include "tink/signature/internal/slh_dsa_parameter_set.h"
+#include "tink/signature/internal/slh_dsa_sign_boringssl.h"
+#include "tink/signature/internal/slh_dsa_verify_boringssl.h"
 #include "tink/signature/slh_dsa_parameters.h"
 #include "tink/signature/slh_dsa_public_key.h"
 #include "tink/subtle/random.h"
@@ -95,6 +100,40 @@
 #endif
 }
 
+absl::StatusOr<KeyPair> GenerateKeyPairFromSeed(
+    SlhDsaParameters::HashType hash_type, RestrictedData seed,
+    int private_key_size_in_bytes, int public_key_size_in_bytes) {
+#ifdef TINK_USE_ONLY_FIPS
+  return absl::UnimplementedError(
+      "SLH-DSA is only supported in non-FIPS BoringSSL builds.");
+#else
+  std::string public_key_bytes;
+  public_key_bytes.resize(public_key_size_in_bytes);
+  std::string private_key_bytes;
+  private_key_bytes.resize(private_key_size_in_bytes);
+
+  if (hash_type == SlhDsaParameters::HashType::kSha2) {
+    SLHDSA_SHA2_128S_generate_key_from_seed(
+        reinterpret_cast<uint8_t*>(&public_key_bytes[0]),
+        reinterpret_cast<uint8_t*>(&private_key_bytes[0]),
+        reinterpret_cast<const uint8_t*>(
+            seed.GetSecret(InsecureSecretKeyAccess::Get()).data()));
+  } else {
+    SLHDSA_SHAKE_256F_generate_key_from_seed(
+        reinterpret_cast<uint8_t*>(&public_key_bytes[0]),
+        reinterpret_cast<uint8_t*>(&private_key_bytes[0]),
+        reinterpret_cast<const uint8_t*>(
+            seed.GetSecret(InsecureSecretKeyAccess::Get()).data()));
+  }
+
+  RestrictedData restricted_private_key_bytes =
+      RestrictedData(private_key_bytes, InsecureSecretKeyAccess::Get());
+
+  KeyPair key_pair = {public_key_bytes, restricted_private_key_bytes};
+  return key_pair;
+#endif
+}
+
 using SlhDsaPrivateKeyTest = TestWithParam<TestCase>;
 
 static constexpr int kSlhDsa128sPrivateKeyBytes = 64;
@@ -146,6 +185,28 @@
                HasSubstr(
                    "SLH-DSA is only supported in non-FIPS BoringSSL builds.")));
 }
+
+TEST_P(SlhDsaPrivateKeyTest, CreateFromSeedFipsFails) {
+  TestCase test_case = GetParam();
+
+  absl::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      test_case.hash_type, test_case.private_key_size_in_bytes,
+      test_case.signature_type, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  absl::StatusOr<internal::SlhDsaParameterSet> parameter_set =
+      internal::GetSlhDsaParameterSet(*parameters);
+  ASSERT_THAT(parameter_set, IsOk());
+
+  RestrictedData seed(parameter_set->GetPrivateSeedSizeInBytes());
+
+  EXPECT_THAT(
+      SlhDsaPrivateKey::CreateFromSeed(
+          *parameters, seed, test_case.id_requirement, GetPartialKeyAccess()),
+      StatusIs(absl::StatusCode::kUnimplemented,
+               HasSubstr("SLH-DSA is only supported in non-FIPS "
+                         "BoringSSL builds.")));
+}
 #else
 TEST_P(SlhDsaPrivateKeyTest, CreateSucceeds) {
   TestCase test_case = GetParam();
@@ -208,6 +269,137 @@
               Eq(key_pair->private_key_bytes));
 }
 
+TEST_P(SlhDsaPrivateKeyTest, CreateFromSeedSucceeds) {
+  TestCase test_case = GetParam();
+
+  absl::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      test_case.hash_type, test_case.private_key_size_in_bytes,
+      test_case.signature_type, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  absl::StatusOr<internal::SlhDsaParameterSet> parameter_set =
+      internal::GetSlhDsaParameterSet(*parameters);
+  ASSERT_THAT(parameter_set, IsOk());
+
+  RestrictedData seed(parameter_set->GetPrivateSeedSizeInBytes());
+
+  absl::StatusOr<KeyPair> expected_key_pair = GenerateKeyPairFromSeed(
+      test_case.hash_type, seed, test_case.private_key_size_in_bytes,
+      test_case.public_key_size_in_bytes);
+  ASSERT_THAT(expected_key_pair, IsOk());
+
+  absl::StatusOr<SlhDsaPublicKey> expected_public_key =
+      SlhDsaPublicKey::Create(*parameters, expected_key_pair->public_key_bytes,
+                              test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(expected_public_key, IsOk());
+
+  absl::StatusOr<SlhDsaPrivateKey> private_key =
+      SlhDsaPrivateKey::CreateFromSeed(
+          *parameters, seed, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(private_key, IsOk());
+
+  EXPECT_THAT(private_key->GetParameters(), Eq(*parameters));
+  EXPECT_THAT(private_key->GetIdRequirement(), Eq(test_case.id_requirement));
+  EXPECT_THAT(private_key->GetPublicKey(), Eq(*expected_public_key));
+  EXPECT_THAT(private_key->GetOutputPrefix(), Eq(test_case.output_prefix));
+  EXPECT_THAT(private_key->GetPrivateKeyBytes(GetPartialKeyAccess()),
+              Eq(expected_key_pair->private_key_bytes));
+}
+
+TEST_P(SlhDsaPrivateKeyTest, CreateFromSeedGeneratesConsistentKeyPair) {
+  TestCase test_case = GetParam();
+
+  absl::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      test_case.hash_type, test_case.private_key_size_in_bytes,
+      test_case.signature_type, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  absl::StatusOr<internal::SlhDsaParameterSet> parameter_set =
+      internal::GetSlhDsaParameterSet(*parameters);
+  ASSERT_THAT(parameter_set, IsOk());
+
+  RestrictedData seed(parameter_set->GetPrivateSeedSizeInBytes());
+
+  absl::StatusOr<SlhDsaPrivateKey> private_key =
+      SlhDsaPrivateKey::CreateFromSeed(
+          *parameters, seed, test_case.id_requirement, GetPartialKeyAccess());
+  ASSERT_THAT(private_key, IsOk());
+
+  absl::StatusOr<std::unique_ptr<PublicKeySign> > signer =
+      internal::NewSlhDsaSignBoringSsl(*private_key);
+  ASSERT_THAT(signer, IsOk());
+
+  absl::StatusOr<std::unique_ptr<PublicKeyVerify> > verifier =
+      internal::NewSlhDsaVerifyBoringSsl(private_key->GetPublicKey());
+  ASSERT_THAT(verifier, IsOk());
+
+  std::string message = "Self-verify test message";
+  absl::StatusOr<std::string> signature = (*signer)->Sign(message);
+  ASSERT_THAT(signature, IsOk());
+
+  EXPECT_THAT((*verifier)->Verify(*signature, message), IsOk());
+}
+
+TEST(SlhDsaPrivateKeyTest, CreateFromSeedWithInvalidSeedLengthFails) {
+  absl::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      SlhDsaParameters::HashType::kSha2,
+      /*private_key_size_in_bytes=*/SLHDSA_SHA2_128S_PRIVATE_KEY_BYTES,
+      SlhDsaParameters::SignatureType::kSmallSignature,
+      SlhDsaParameters::Variant::kTink);
+  ASSERT_THAT(parameters, IsOk());
+
+  // General invalid seed length (47 bytes, not 48, 72, or 96 bytes)
+  RestrictedData invalid_seed(47);
+  EXPECT_THAT(
+      SlhDsaPrivateKey::CreateFromSeed(*parameters, invalid_seed,
+                                       /*id_requirement=*/123,
+                                       GetPartialKeyAccess())
+          .status(),
+      StatusIs(
+          absl::StatusCode::kInvalidArgument,
+          HasSubstr(
+              "SLH-DSA private seed length must be 48, 72, or 96 bytes.")));
+
+  // Mismatched seed size (96 bytes, but parameters expect 48 bytes)
+  RestrictedData mismatched_seed(96);
+  EXPECT_THAT(
+      SlhDsaPrivateKey::CreateFromSeed(*parameters, mismatched_seed,
+                                       /*id_requirement=*/123,
+                                       GetPartialKeyAccess())
+          .status(),
+      StatusIs(absl::StatusCode::kInvalidArgument,
+               HasSubstr("Private key size does not match parameters")));
+}
+
+TEST_P(SlhDsaPrivateKeyTest, CreateFromSeedWithMismatchedIdRequirementFails) {
+  TestCase test_case = GetParam();
+
+  absl::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
+      test_case.hash_type, test_case.private_key_size_in_bytes,
+      test_case.signature_type, test_case.variant);
+  ASSERT_THAT(parameters, IsOk());
+
+  absl::StatusOr<internal::SlhDsaParameterSet> parameter_set =
+      internal::GetSlhDsaParameterSet(*parameters);
+  ASSERT_THAT(parameter_set, IsOk());
+
+  RestrictedData seed(parameter_set->GetPrivateSeedSizeInBytes());
+
+  if (parameters->HasIdRequirement()) {
+    EXPECT_THAT(SlhDsaPrivateKey::CreateFromSeed(
+                    *parameters, seed,
+                    /*id_requirement=*/std::nullopt, GetPartialKeyAccess())
+                    .status(),
+                StatusIs(absl::StatusCode::kInvalidArgument));
+  } else {
+    EXPECT_THAT(SlhDsaPrivateKey::CreateFromSeed(*parameters, seed,
+                                                 /*id_requirement=*/123,
+                                                 GetPartialKeyAccess())
+                    .status(),
+                StatusIs(absl::StatusCode::kInvalidArgument));
+  }
+}
+
 TEST(SlhDsaPrivateKeyTest,
      CreateFromParametersWithInvalidPrivateKeyLengthFails) {
   absl::StatusOr<SlhDsaParameters> parameters = SlhDsaParameters::Create(
@@ -217,8 +409,7 @@
       SlhDsaParameters::Variant::kTink);
   ASSERT_THAT(parameters, IsOk());
 
-  RestrictedData restricted_private_key_bytes = RestrictedData(
-      subtle::Random::GetRandomBytes(63), InsecureSecretKeyAccess::Get());
+  RestrictedData restricted_private_key_bytes(63);
   EXPECT_THAT(
       SlhDsaPrivateKey::Create(*parameters, restricted_private_key_bytes,
                                /*id_requirement=*/123, GetPartialKeyAccess())
@@ -271,8 +462,7 @@
       /*id_requirement=*/123, GetPartialKeyAccess());
   ASSERT_THAT(public_key, IsOk());
 
-  RestrictedData restricted_private_key_bytes = RestrictedData(
-      subtle::Random::GetRandomBytes(63), InsecureSecretKeyAccess::Get());
+  RestrictedData restricted_private_key_bytes(63);
   EXPECT_THAT(
       SlhDsaPrivateKey::Create(*public_key, restricted_private_key_bytes,
                                GetPartialKeyAccess())