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:
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,