Repository: randombit/botan
Pull request #2051 (2019-07-30 14:47:44)

Add Private_Key::stateful_operation and use it in cli signer
If you generated an XMSS key and used it in the CLI, the key would not be updated which is bad news for stateful signatures.

@mgierlings did I get this right? Is there anything else that is needed to do for updating the key?


Patch:
diff --git a/src/cli/pubkey.cpp b/src/cli/pubkey.cpp
index 51224cd1b..74ffde906 100644
--- a/src/cli/pubkey.cpp
+++ b/src/cli/pubkey.cpp
@@ -1,5 +1,6 @@
 /*
-* (C) 2010,2014,2015 Jack Lloyd
+* (C) 2010,2014,2015,2019 Jack Lloyd
+* (C) 2019 Matthias Gierlings
 * (C) 2015 René Korthaus
 *
 * Botan is released under the Simplified BSD License (see license.txt)
@@ -21,6 +22,8 @@
 #include <botan/workfactor.h>
 #include <botan/data_src.h>
 
+#include <fstream>
+
 #if defined(BOTAN_HAS_DL_GROUP)
    #include <botan/dl_group.h>
 #endif
@@ -214,7 +217,18 @@ class PK_Sign final : public Command
             };
          this->read_file(get_arg("file"), onData);
 
-         output() << Botan::base64_encode(signer.signature(rng())) << "\n";
+         std::vector<uint8_t> sig { signer.signature(rng()) };
+
+         if(key->stateful_operation())
+            {
+            std::ofstream updated_key(key_file);
+            if(passphrase.empty())
+               updated_key << Botan::PKCS8::PEM_encode(*key);
+            else
+               updated_key << Botan::PKCS8::PEM_encode(*key, rng(), passphrase);
+            }
+
+         output() << Botan::base64_encode(sig) << "\n";
          }
    };
 
diff --git a/src/lib/pubkey/pk_keys.h b/src/lib/pubkey/pk_keys.h
index 0aa9caf54..b149be37f 100644
--- a/src/lib/pubkey/pk_keys.h
+++ b/src/lib/pubkey/pk_keys.h
@@ -176,6 +176,8 @@ class BOTAN_PUBLIC_API(2,0) Private_Key : public virtual Public_Key
       Private_Key& operator=(const Private_Key& other) = default;
       virtual ~Private_Key() = default;
 
+      virtual bool stateful_operation() const { return false; }
+
       /**
       * @return BER encoded private key bits
       */
diff --git a/src/lib/pubkey/xmss/xmss_privatekey.h b/src/lib/pubkey/xmss/xmss_privatekey.h
index d362f99b5..2bfcbc82e 100644
--- a/src/lib/pubkey/xmss/xmss_privatekey.h
+++ b/src/lib/pubkey/xmss/xmss_privatekey.h
@@ -91,6 +91,8 @@ class BOTAN_PUBLIC_API(2,0) XMSS_PrivateKey final : public virtual XMSS_PublicKe
          set_unused_leaf_index(idx_leaf);
          }
 
+      bool stateful_operation() const override { return true; }
+
       /**
        * Retrieves the last unused leaf index of the private key. Reusing a leaf
        * by utilizing leaf indices lower than the last unused leaf index will


Test patch:
diff --git a/src/scripts/test_cli.py b/src/scripts/test_cli.py
index da7587194..eaff50a16 100755
--- a/src/scripts/test_cli.py
+++ b/src/scripts/test_cli.py
@@ -318,6 +318,40 @@ mlLtJ5JvZ0/p6zP3x+Y9yPIrAR8L/acG5ItSrAKXzzuqQQZMv4aN
     test_cli("cert_verify", user_cert,
              "Certificate did not validate - Certificate issuer not found")
 
+def cli_xmss_sign_tests(tmp_dir):
+    priv_key = os.path.join(tmp_dir, 'priv.pem')
+    pub_key = os.path.join(tmp_dir, 'pub.pem')
+    pub_key2 = os.path.join(tmp_dir, 'pub2.pem')
+    msg = os.path.join(tmp_dir, 'input')
+    sig1 = os.path.join(tmp_dir, 'sig1')
+    sig2 = os.path.join(tmp_dir, 'sig2')
+
+    test_cli("rng", ['--output=%s' % (msg)], "")
+    test_cli("hash", ["--no-fsname", msg], "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855")
+
+    test_cli("keygen", ["--algo=XMSS", "--output=%s" % (priv_key)], "")
+    test_cli("hash", ["--no-fsname", priv_key], "32397312E3FAC9D6396C55FEEFFF11EE195E2D2D5B34279D2544AF27763B0946")
+
+    test_cli("pkcs8", "--pub-out --output=%s %s" % (pub_key, priv_key), "")
+    test_cli("fingerprint", ['--no-fsname', pub_key],
+             "E2:BE:C8:6D:CF:4B:5D:67:AB:A1:C1:F8:36:79:D5:3B:D8:17:D5:E3:5B:BE:29:08:03:7E:6E:07:27:4E:16:46")
+
+    # verify the key is updated after each signature:
+    test_cli("sign", [priv_key, msg, "--output=%s" % (sig1)], "")
+    test_cli("verify", [pub_key, msg, sig1], "Signature is valid")
+    test_cli("hash", ["--no-fsname", sig1], "04AF45451C7A9AF2D828E1AD6EC262E012436F4087C5DA6F32C689D781E597D0")
+    test_cli("hash", ["--no-fsname", priv_key], "649E54D334F78A6AAAE34CFABF62121C74909D80E4DC2FA240A6EE1848526094")
+
+    test_cli("sign", [priv_key, msg, "--output=%s" % (sig2)], "")
+    test_cli("verify", [pub_key, msg, sig2], "Signature is valid")
+    test_cli("hash", ["--no-fsname", sig2], "0785A6AD54CC7D01F2BE2BC6463A3EAA1159792E52210ED754992C5068E8F24F")
+    test_cli("hash", ["--no-fsname", priv_key], "04483FA5367A7340F4BF6160FABD5742258009E05F9584E8D9732660B132608E")
+
+    # private key updates, public key is unchanged:
+    test_cli("pkcs8", "--pub-out --output=%s %s" % (pub_key2, priv_key), "")
+    test_cli("fingerprint", ['--no-fsname', pub_key2],
+             "E2:BE:C8:6D:CF:4B:5D:67:AB:A1:C1:F8:36:79:D5:3B:D8:17:D5:E3:5B:BE:29:08:03:7E:6E:07:27:4E:16:46")
+
 def cli_pbkdf_tune_tests(_tmp_dir):
     if not check_for_command("pbkdf_tune"):
         return
@@ -1032,6 +1066,7 @@ def main(args=None):
         cli_hmac_tests,
         cli_is_prime_tests,
         cli_key_tests,
+        cli_xmss_sign_tests,
         cli_mod_inverse_tests,
         cli_pbkdf_tune_tests,
         cli_pk_encrypt_tests,