From: Daiki Ueno Date: Thu, 9 Apr 2026 04:47:26 +0000 (+0900) Subject: nettle: support deriving ML-DSA public key from expanded secret key X-Git-Tag: 3.8.13~12^2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=46eeb76a6df1613f6afd3bfbdbab42ebc2c2eaaf;p=thirdparty%2Fgnutls.git nettle: support deriving ML-DSA public key from expanded secret key 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 --- diff --git a/configure.ac b/configure.ac index c708d8f5e2..aea2f9860b 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/lib/nettle/pk.c b/lib/nettle/pk.c index 175a384ab1..6a00924fc8 100644 --- a/lib/nettle/pk.c +++ b/lib/nettle/pk.c @@ -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, ¶ms->raw_priv, + ¶ms->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) { diff --git a/tests/cert-tests/mldsa.sh b/tests/cert-tests/mldsa.sh index 55e31ce5a7..c3e9fee42c 100644 --- a/tests/cert-tests/mldsa.sh +++ b/tests/cert-tests/mldsa.sh @@ -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"