]> git.ipfire.org Git - thirdparty/gnutls.git/commitdiff
nettle: support deriving ML-DSA public key from expanded secret key 2088/head
authorDaiki Ueno <ueno@gnu.org>
Thu, 9 Apr 2026 04:47:26 +0000 (13:47 +0900)
committerDaiki Ueno <ueno@gnu.org>
Sun, 12 Apr 2026 23:30:18 +0000 (08:30 +0900)
RFC 9881 defines 3 private key formats for ML-DSA: "seed",
"expandedKey" and both. When it is "expandedKey", a non-trivial
conversion is required to derive a public key, which is now
implemented in leancrypto through lc_dilithium_pk_from_sk. This patch
modifies the pk_fixup backend function to use it to derive a public
key when importing a private key.

Signed-off-by: Daiki Ueno <ueno@gnu.org>
configure.ac
lib/nettle/pk.c
tests/cert-tests/mldsa.sh

index c708d8f5e274ea352fa6152ceabb662b2de8f670..aea2f9860b4063a27a3835e24a4e5bcb26377bbe 100644 (file)
@@ -1315,6 +1315,13 @@ AM_COND_IF([ENABLE_LEANCRYPTO], [
     fi
 ])
 
+AM_COND_IF([ENABLE_LEANCRYPTO], [
+    save_LIBS=$LIBS
+    LIBS="$LIBS $LEANCRYPTO_LIBS"
+    AC_CHECK_FUNCS([lc_dilithium_pk_from_sk])
+    LIBS=$save_LIBS
+])
+
 AM_CONDITIONAL(NEED_LTLIBDL, test "$need_ltlibdl" = yes)
 
 # export for use in scripts
index 175a384ab10d721eb6cd50c89205bcccceb61f53..6a00924fc8886dadd6eed489c2b78d716323c136 100644 (file)
@@ -1824,7 +1824,67 @@ cleanup:
        zeroize_key(&sk, sizeof(sk));
        return ret;
 }
-#else
+
+#ifdef HAVE_LC_DILITHIUM_PK_FROM_SK
+static int ml_dsa_privkey_to_pubkey(gnutls_pk_algorithm_t algo,
+                                   const gnutls_datum_t *raw_priv,
+                                   gnutls_datum_t *raw_pub)
+{
+       int ret;
+       enum lc_dilithium_type type;
+       struct lc_dilithium_sk sk;
+       struct lc_dilithium_pk pk;
+       gnutls_datum_t tmp_raw_pub = { NULL, 0 };
+       uint8_t *ptr;
+       size_t len;
+
+       type = ml_dsa_pk_to_lc_dilithium_type(algo);
+       if (type == LC_DILITHIUM_UNKNOWN)
+               return gnutls_assert_val(GNUTLS_E_UNKNOWN_PK_ALGORITHM);
+
+       ret = lc_dilithium_sk_load(&sk, raw_priv->data, raw_priv->size);
+       if (ret < 0 || lc_dilithium_sk_type(&sk) != type) {
+               ret = gnutls_assert_val(GNUTLS_E_INVALID_REQUEST);
+               goto cleanup;
+       }
+
+       ret = lc_dilithium_pk_from_sk(&pk, &sk);
+       if (ret < 0) {
+               ret = gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
+               goto cleanup;
+       }
+
+       ret = lc_dilithium_pk_ptr(&ptr, &len, &pk);
+       if (ret < 0) {
+               ret = gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
+               goto cleanup;
+       }
+
+       ret = _gnutls_set_datum(&tmp_raw_pub, ptr, len);
+       if (ret < 0) {
+               ret = gnutls_assert_val(GNUTLS_E_INTERNAL_ERROR);
+               goto cleanup;
+       }
+
+       *raw_pub = _gnutls_take_datum(&tmp_raw_pub);
+
+       ret = 0;
+
+cleanup:
+       _gnutls_free_key_datum(&tmp_raw_pub);
+       zeroize_key(&pk, sizeof(pk));
+       zeroize_key(&sk, sizeof(sk));
+       return ret;
+}
+#else /* !HAVE_LC_DILITHIUM_PK_FROM_SK */
+static int ml_dsa_privkey_to_pubkey(gnutls_pk_algorithm_t algo MAYBE_UNUSED,
+                                   const gnutls_datum_t *raw_priv MAYBE_UNUSED,
+                                   gnutls_datum_t *raw_pub MAYBE_UNUSED)
+{
+       return gnutls_assert_val(GNUTLS_E_UNIMPLEMENTED_FEATURE);
+}
+#endif
+#else /* !HAVE_LEANCRYPTO */
 static int ml_dsa_exists(gnutls_pk_algorithm_t algo MAYBE_UNUSED)
 {
        return 0;
@@ -1853,6 +1913,13 @@ static int ml_dsa_generate_keypair(gnutls_pk_algorithm_t algo MAYBE_UNUSED,
 {
        return gnutls_assert_val(GNUTLS_E_UNSUPPORTED_SIGNATURE_ALGORITHM);
 }
+
+static int ml_dsa_privkey_to_pubkey(gnutls_pk_algorithm_t algo MAYBE_UNUSED,
+                                   const gnutls_datum_t *raw_priv MAYBE_UNUSED,
+                                   gnutls_datum_t *raw_pub MAYBE_UNUSED)
+{
+       return gnutls_assert_val(GNUTLS_E_UNSUPPORTED_SIGNATURE_ALGORITHM);
+}
 #endif
 
 /* This is the lower-level part of privkey_sign_raw_data().
@@ -4968,6 +5035,23 @@ static int wrap_nettle_pk_fixup(gnutls_pk_algorithm_t algo,
                }
                break;
 
+       case GNUTLS_PK_MLDSA44:
+       case GNUTLS_PK_MLDSA65:
+       case GNUTLS_PK_MLDSA87:
+               if (params->raw_pub.data == NULL) {
+                       ret = ml_dsa_privkey_to_pubkey(algo, &params->raw_priv,
+                                                      &params->raw_pub);
+                       if (ret < 0) {
+                               if (ret == GNUTLS_E_UNIMPLEMENTED_FEATURE) {
+                                       _gnutls_debug_log(
+                                               "Deriving public key from an ML-DSA private key is not implemented; ignoring the request\n");
+                                       return 0;
+                               }
+                               return gnutls_assert_val(ret);
+                       }
+               }
+               break;
+
 #ifdef ENABLE_DSA
        case GNUTLS_PK_DSA:
                if (params->params[DSA_Y] == NULL) {
index 55e31ce5a7f9b009b6f24c598fbc1ce0dbdd10d8..c3e9fee42cd7b2cbddce445e0dbf3e793cd23257 100644 (file)
@@ -127,21 +127,46 @@ for variant in 44 65 87; do
     for format in seed expanded both; do
        echo "Testing ML-DSA-$variant ($format)"
 
-       # Check default
-       TMPKEYDEFAULT=$testdir/key-$algo-$format-default
        TMPKEY=$testdir/key-$algo-$format
-       ${VALGRIND} "${CERTTOOL}" -k --no-text --infile "$srcdir/data/key-$algo-$format.pem" >"$TMPKEYDEFAULT"
-       if [ $? != 0 ]; then
-           cat "$TMPKEYDEFAULT"
-           exit 1
-       fi
 
-       # The "expandedKey" format doesn't have public key part
-       if [ "$format" = seed ] || [ "$format" = both ]; then
-           if ! "${DIFF}" "$TMPKEYDEFAULT" "$srcdir/data/key-$algo-both.pem"; then
-               exit 1
-           fi
-       fi
+       # Check certtool --key-info would result in the same output as
+       # "both" for "seed and "both" formats.
+       #
+       # As for "expandedKey" format, it is not possible to recover a
+       # seed, so compare the textual information about public key.
+       case "$format" in
+           seed | both)
+               TMPKEYBODY=$testdir/key-$algo-$format-default
+               ${VALGRIND} "${CERTTOOL}" -k --no-text --infile "$srcdir/data/key-$algo-$format.pem" >"$TMPKEYBODY"
+               if [ $? != 0 ]; then
+                   cat "$TMPKEYBODY"
+                   exit 1
+               fi
+
+               if ! "${DIFF}" "$TMPKEYBODY" "$srcdir/data/key-$algo-both.pem"; then
+                   exit 1
+               fi
+               ;;
+           expandedKey)
+               TMPKEYTEXT=$testdir/key-$algo-$format-text
+               ${VALGRIND} "${CERTTOOL}" -k --infile "$srcdir/data/key-$algo-$format.pem" | sed -n '1,/^-----BEGIN/p' | head -n-1 >"$TMPKEYTEXT"
+               if [ $? != 0 ]; then
+                   cat "$TMPKEYTEXT"
+                   exit 1
+               fi
+
+               TMPKEYTEXT2=$testdir/key-$algo-both-text
+               ${VALGRIND} "${CERTTOOL}" -k --infile "$srcdir/data/key-$algo-both.pem" | sed -n '1,/^-----BEGIN/p' | head -n-1 >"$TMPKEYTEXT2"
+               if [ $? != 0 ]; then
+                   cat "$TMPKEYTEXT2"
+                   exit 1
+               fi
+
+               if ! "${DIFF}" "$TMPKEYTEXT" "$TMPKEYTEXT2"; then
+                   exit 1
+               fi
+               ;;
+       esac
 
        # Check roundtrip with --key-format
        ${VALGRIND} "${CERTTOOL}" -k --no-text --key-format "$format" --infile "$srcdir/data/key-$algo-$format.pem" >"$TMPKEY"