diff --git a/src/x509/clu_config.c b/src/x509/clu_config.c index 780e56db..4d1d17da 100644 --- a/src/x509/clu_config.c +++ b/src/x509/clu_config.c @@ -1061,6 +1061,17 @@ int wolfCLU_GetTypeFromPKEY(WOLFSSL_EVP_PKEY* key) case EVP_PKEY_DH: keyType = DHk; break; + +#ifdef HAVE_DILITHIUM + case EVP_PKEY_DILITHIUM: + /* wolfCLU's CA-signing path only uses this as an opaque marker + * (to route into the generic wolfSSL_X509_sign() path, which + * itself resolves the correct ML-DSA level/signature type from + * the WOLFSSL_EVP_PKEY object); the specific level chosen here + * doesn't matter. */ + keyType = ML_DSA_87k; + break; +#endif } return keyType; } diff --git a/src/x509/clu_x509_sign.c b/src/x509/clu_x509_sign.c index 717ad910..b71201c6 100644 --- a/src/x509/clu_x509_sign.c +++ b/src/x509/clu_x509_sign.c @@ -123,7 +123,11 @@ int wolfCLU_CertSignFree(WOLFCLU_CERT_SIGN* csign) } wolfSSL_BIO_free(csign->randFile); wolfSSL_X509_free(csign->ca); - if (csign->keyType == RSAk || csign->keyType == ECDSAk) { + if (csign->keyType == RSAk || csign->keyType == ECDSAk + #ifdef HAVE_DILITHIUM + || csign->keyType == ML_DSA_87k + #endif + ) { wolfSSL_EVP_PKEY_free(csign->caKey.pkey); } XFREE(csign, HEAP_HINT, DYNAMIC_TYPE_CERT); @@ -201,6 +205,9 @@ void wolfCLU_CertSignSetCA(WOLFCLU_CERT_SIGN* csign, WOLFSSL_X509* ca, switch (keyType) { case RSAk: case ECDSAk: + #ifdef HAVE_DILITHIUM + case ML_DSA_87k: + #endif wolfSSL_EVP_PKEY_free(csign->caKey.pkey); csign->caKey.pkey = (WOLFSSL_EVP_PKEY*)key; break; @@ -1394,9 +1401,20 @@ int wolfCLU_CertSign(WOLFCLU_CERT_SIGN* csign, WOLFSSL_X509* x509) wolfCLU_LogError("Error signing certificate"); ret = WOLFCLU_FATAL_ERROR; } - } /* @TODO else case here could get the tbs buffer or just the der of the - * x509 struct and use a different method for signing and creating the - * certificate */ + } +#ifdef HAVE_DILITHIUM + /* ML-DSA: wolfSSL_X509_sign() itself resolves the ML-DSA level/sig type + * from the WOLFSSL_EVP_PKEY and ignores `md`. */ + else if (ret == WOLFCLU_SUCCESS && csign->keyType == ML_DSA_87k) { + if (wolfSSL_X509_sign(x509, csign->caKey.pkey, md) <= 0) { + wolfCLU_LogError("Error signing certificate"); + ret = WOLFCLU_FATAL_ERROR; + } + } +#endif + /* @TODO else case here could get the tbs buffer or just the der of the + * x509 struct and use a different method for signing and creating the + * certificate */ /* check if unique subject name is required */ if (ret == WOLFCLU_SUCCESS && csign->unique == 1) { diff --git a/tests/x509/x509-ca-test.py b/tests/x509/x509-ca-test.py index 514d00d7..faca5ea3 100644 --- a/tests/x509/x509-ca-test.py +++ b/tests/x509/x509-ca-test.py @@ -809,5 +809,112 @@ def test_relative_out_path(self): "File not found at {}".format(expected)) +def _has_ml_dsa(): + """Check whether ML-DSA key generation is available in this build.""" + r = run_wolfssl("genkey", "-h") + return "ml-dsa" in (r.stdout + r.stderr) + + +@unittest.skipUnless(_has_ml_dsa(), "ml-dsa not available") +class TestCAMlDsa(unittest.TestCase): + """ML-DSA (post-quantum) CA-signing tests. + + Regression coverage for two bugs found while building a ML-KEM/ML-DSA + mutual-TLS demo, both of which let a broken certificate through with a + 0 exit code instead of failing loudly: + + 1. `req -new [-x509] -key ` failed to embed the + certificate's public key, because wolfSSL's ML-DSA private-key + decode does not recompute the public key from imported private-key + components. Fixed in wolfSSL by having + wc_MlDsaKey_PrivateKeyToDer() include the public key in the + RFC 5958 OneAsymmetricKey `publicKey [1]` field. + 2. `ca -in -keyfile ...` silently skipped the + signing step for any non-RSA/ECDSA CA key (wolfCLU_GetTypeFromPKEY, + wolfCLU_CertSignSetCA, and wolfCLU_CertSign only recognized + RSAk/ECDSAk), yet still wrote out a certificate file and exited 0; + the file was missing its issuer name and validity dates and failed + to parse. Fixed by recognizing ML-DSA keys in those three + functions. + """ + + def setUp(self): + _cleanup(_tmp("index.txt")) + _touch(_tmp("index.txt")) + + def tearDown(self): + _cleanup(_tmp("index.txt")) + + def _clean(self, *files): + for f in files: + self.addCleanup(lambda p=f: _cleanup(p)) + + def _genkey_mldsa(self, keybase): + priv = keybase + ".priv" + pub = keybase + ".pub" + self._clean(priv, pub) + r = run_wolfssl("genkey", "ml-dsa", "-level", "5", + "-out", keybase, "-output", "keypair", + "-outform", "PEM") + self.assertEqual(r.returncode, 0, + "genkey ml-dsa failed: {}".format(r.stderr)) + return priv, pub + + def test_mldsa_self_signed_ca(self): + """A self-signed ML-DSA-87 CA cert must embed its own public key.""" + ca_key, _ = self._genkey_mldsa(_tmp("tmp_mldsa_ca_key")) + ca_cert = _tmp("tmp_mldsa_ca_cert.pem") + self._clean(ca_cert) + + r = run_wolfssl("req", "-new", "-x509", "-key", ca_key, + "-subj", "O=wolfCLU-Test/C=US/CN=MLDSA-Test-CA", + "-out", ca_cert, "-outform", "PEM") + self.assertEqual(r.returncode, 0, r.stderr) + + r = run_wolfssl("x509", "-in", ca_cert, "-inform", "PEM", + "-issuer", "-noout") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("MLDSA-Test-CA", r.stdout) + + def test_mldsa_ca_sign_server_cert(self): + """A CSR signed by an ML-DSA CA key must produce a well-formed + certificate that still carries its issuer name and validity + dates, and verifies against the CA.""" + ca_key, _ = self._genkey_mldsa(_tmp("tmp_mldsa2_ca_key")) + server_key, _ = self._genkey_mldsa(_tmp("tmp_mldsa2_server_key")) + ca_cert = _tmp("tmp_mldsa2_ca_cert.pem") + server_csr = _tmp("tmp_mldsa2_server.csr") + server_cert = _tmp("tmp_mldsa2_server_cert.pem") + self._clean(ca_cert, server_csr, server_cert) + + r = run_wolfssl("req", "-new", "-x509", "-key", ca_key, + "-subj", "O=wolfCLU-Test/C=US/CN=MLDSA-Test-CA2", + "-out", ca_cert, "-outform", "PEM") + self.assertEqual(r.returncode, 0, r.stderr) + + r = run_wolfssl("req", "-new", "-key", server_key, + "-subj", "O=wolfCLU-Test/C=US/CN=mldsa-server", + "-out", server_csr, "-outform", "PEM") + self.assertEqual(r.returncode, 0, r.stderr) + + r = run_wolfssl("ca", "-in", server_csr, "-keyfile", ca_key, + "-cert", ca_cert, "-out", server_cert, + "-days", "30") + self.assertEqual(r.returncode, 0, r.stderr) + + r = run_wolfssl("x509", "-in", server_cert, "-inform", "PEM", + "-issuer", "-noout") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertIn("MLDSA-Test-CA2", r.stdout) + + r = run_wolfssl("x509", "-in", server_cert, "-inform", "PEM", + "-dates", "-noout") + self.assertEqual(r.returncode, 0, r.stderr) + self.assertTrue(r.stdout.strip(), "no validity dates printed") + + r = run_wolfssl("verify", "-CAfile", ca_cert, server_cert) + self.assertEqual(r.returncode, 0, r.stderr) + + if __name__ == "__main__": test_main()