Repository: randombit/botan (cpp)

Issue:
Using Dilithium as PKI Signer Algo.
To generate a self signed root CA for PSQ PKI, I tried the following:

`botan keygen --algo=Dilithium --params=Dilithium-AES-r3/6x5 --passphrase=xxxx --pbe= --pbe-millis=300 --provider= > ~/tmp/rootca_key.pem`

Which generates correctly the key. Then I generate the root CA certificate with the following:

`botan gen_self_signed ~/tmp/rootca_key.pem "My Root CA" --country=CH --dns=cloud.xxxx.ch --organization=xxxx --email=info@xxxx.ch --path-limit=1 --days=7300  --key-pass=xxxx --ca  --hash=SHA-512 --emsa= > ~/tmp/rootca_crt.pem`

The certificate is not generated and an error is issued:
`Error: Unknown X.509 signing key type: Dilithium-AES-r3/6x5`

I checked the source code and find out that the algorithm is not yet supported.
When do you think this will be supported? Or do you recommend other approaches?




Pull request description:
FIX: Verify X.509 with Dilithium
### Pull Request Dependencies

* #3222 

### Discussion

This fixes an issue reported in #3208 that X.509 certificates signed by Dilithium could not be verified. A regression test is added in `test_cli.py` (`cli_cert_issuance_alternative_algos_tests`).

Dilithium -- just like Ed25519 -- hard-codes the hash function it uses for compressing the message to sign. Internally, Dilithium uses `SHAKE-256(512)`, so I added that as a special case in `X509_Object::hash_used_for_signature()` when Dilithium is used.

Though, that feels a bit dirty for two reasons:

* `::hash_used_for_signature()` now bears internal knowledge of Dilithium (just as it did for Ed25519).
* `SHAKE-256(512)` is not trusted in the path restriction's defaults. Adding it should be fine, but it makes me wonder what to do with the out-length parameter.

Currently, I just added `SHAKE-256(512)` as trusted by default in the `Path_Validation_Restrictions` as a proof of concept. I don't really like it, though.

I think we should find a more general approach to signature algorithms that do not require an EMSA. And find a solution for how things like `X509_Object::hash_used_for_signature()` should behave for such signatures.

I wouldn't be surprised if similar constructions are needed for other protocols that use signatures (e.g. the TLS implementation).

Closes #3208.

Patch:
diff --git a/src/cli/main.cpp b/src/cli/main.cpp
index 1f806a906..bed755491 100644
--- a/src/cli/main.cpp
+++ b/src/cli/main.cpp
@@ -28,7 +28,7 @@ int main(int argc, char* argv[])
 
    if(!cmd)
       {
-      std::cout << "Unknown command " << cmd_name << " (try --help)\n";
+      std::cerr << "Unknown command " << cmd_name << " (try --help)\n";
       return 1;
       }
 
diff --git a/src/lib/x509/x509_obj.cpp b/src/lib/x509/x509_obj.cpp
index 021ff4766..1bf9568c0 100644
--- a/src/lib/x509/x509_obj.cpp
+++ b/src/lib/x509/x509_obj.cpp
@@ -140,8 +140,13 @@ std::string X509_Object::hash_used_for_signature() const
    const OID& oid = m_sig_algo.get_oid();
    const std::vector<std::string> sig_info = split_on(oid.to_formatted_string(), '/');
 
-   if(sig_info.size() == 1 && sig_info[0] == "Ed25519")
-      return "SHA-512";
+   if(sig_info.size() == 1)
+      {
+      if(sig_info[0] == "Ed25519")
+         return "SHA-512";
+      else if(sig_info[0].starts_with("Dilithium-"))
+         return "SHAKE-256(512)";
+      }
    else if(sig_info.size() != 2)
       throw Internal_Error("Invalid name format found for " + oid.to_string());
 
diff --git a/src/lib/x509/x509path.cpp b/src/lib/x509/x509path.cpp
index 612f86b11..26b12363c 100644
--- a/src/lib/x509/x509path.cpp
+++ b/src/lib/x509/x509path.cpp
@@ -1061,6 +1061,7 @@ Path_Validation_Restrictions::Path_Validation_Restrictions(bool require_rev,
    m_trusted_hashes.insert("SHA-256");
    m_trusted_hashes.insert("SHA-384");
    m_trusted_hashes.insert("SHA-512");
+   m_trusted_hashes.insert("SHAKE-256(512)");
    }
 
 namespace {


Test patch:
diff --git a/src/scripts/test_cli.py b/src/scripts/test_cli.py
index a47dfd40c..fd54dc017 100755
--- a/src/scripts/test_cli.py
+++ b/src/scripts/test_cli.py
@@ -773,7 +773,7 @@ def cli_cc_enc_tests(_tmp_dir):
     test_cli("cc_encrypt", ["8028028028028029", "pass"], "4308989841607208")
     test_cli("cc_decrypt", ["4308989841607208", "pass"], "8028028028028027")
 
-def cli_cert_issuance_tests(tmp_dir):
+def cli_cert_issuance_tests(tmp_dir, algos=None):
     root_key = os.path.join(tmp_dir, 'root.key')
     root_crt = os.path.join(tmp_dir, 'root.crt')
     int_key = os.path.join(tmp_dir, 'int.key')
@@ -783,9 +783,12 @@ def cli_cert_issuance_tests(tmp_dir):
     leaf_crt = os.path.join(tmp_dir, 'leaf.crt')
     leaf_csr = os.path.join(tmp_dir, 'leaf.csr')
 
-    test_cli("keygen", ["--params=2048", "--output=" + root_key], "")
-    test_cli("keygen", ["--params=2048", "--output=" + int_key], "")
-    test_cli("keygen", ["--params=2048", "--output=" + leaf_key], "")
+    if algos is None:
+        algos = [("RSA", "2048"),("RSA", "2048"),("RSA", "2048")]
+
+    test_cli("keygen", ["--algo=%s" % algos[0][0], "--params=%s" % algos[0][1], "--output=" + root_key], "")
+    test_cli("keygen", ["--algo=%s" % algos[1][0], "--params=%s" % algos[1][1], "--output=" + int_key], "")
+    test_cli("keygen", ["--algo=%s" % algos[2][0], "--params=%s" % algos[2][1], "--output=" + leaf_key], "")
 
     test_cli("gen_self_signed",
              [root_key, "Root", "--ca", "--path-limit=2", "--output="+root_crt], "")
@@ -796,7 +799,15 @@ def cli_cert_issuance_tests(tmp_dir):
     test_cli("gen_pkcs10", "%s Leaf --output=%s" % (leaf_key, leaf_csr))
     test_cli("sign_cert", "%s %s %s --output=%s" % (int_crt, int_key, leaf_csr, leaf_crt))
 
-    test_cli("cert_verify" "%s %s %s" % (leaf_crt, int_crt, root_crt), "Certificate passes validation checks")
+    test_cli("cert_verify", "%s %s %s" % (leaf_crt, int_crt, root_crt), "Certificate passes validation checks")
+
+def cli_cert_issuance_alternative_algos_tests(tmp_dir):
+    for i, algo in enumerate([[("Dilithium", "Dilithium-8x7-AES-r3"), ("Dilithium", "Dilithium-8x7-AES-r3"), ("Dilithium", "Dilithium-8x7-AES-r3")],
+                              [("ECDSA",     "secp256r1"),            ("ECDSA",     "secp256r1"),            ("ECDSA",     "secp256r1")],
+                              [("Dilithium", "Dilithium-6x5-r3"),     ("ECDSA",     "secp256r1"),            ("RSA",       "2048")]]):
+        sub_tmp_dir = os.path.join(tmp_dir, str(i))
+        os.mkdir(sub_tmp_dir)
+        cli_cert_issuance_tests(sub_tmp_dir, algo)
 
 def cli_timing_test_tests(_tmp_dir):
 
@@ -1423,6 +1434,7 @@ def main(args=None):
         cli_cc_enc_tests,
         cli_cycle_counter,
         cli_cert_issuance_tests,
+        cli_cert_issuance_alternative_algos_tests,
         cli_compress_tests,
         cli_config_tests,
         cli_cpuid_tests,