]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
Per-key encoding formats for ML-KEM and ML-DSA
authorViktor Dukhovni <openssl-users@dukhovni.org>
Mon, 24 Nov 2025 13:37:49 +0000 (00:37 +1100)
committerPauli <paul.dale@oracle.com>
Thu, 4 Dec 2025 23:07:51 +0000 (10:07 +1100)
We support selection of ML-KEM and ML-DSA key formats on input and
output at the provider level, these are essentially global defaults,
in effect for the lifetime of the process.

Unfortunately, the JAVA interface in openssl-jostle needs to be able to
output a specific key in seed-only form.  To that end, this PR
introduces a new "output-formats" PKEY encoding parameter, that can be used
with OSSL_ENCODER_CTX_set_params(3) when encoding a key to PKCS#8, after
using OSSL_ENCODER_CTX_new_for_key(3), rather than i2d_PrivateKey(3),
i2d_PKCS8PrivateKey(3) or PEM equivalents.

Reviewed-by: Tomas Mraz <tomas@openssl.org>
Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Paul Dale <paul.dale@oracle.com>
(Merged from https://github.com/openssl/openssl/pull/29206)

13 files changed:
crypto/ml_dsa/ml_dsa_key.c
crypto/ml_kem/ml_kem.c
doc/man7/EVP_PKEY-ML-DSA.pod
doc/man7/EVP_PKEY-ML-KEM.pod
include/openssl/core_names.h.in
providers/implementations/encode_decode/encode_key2any.c
providers/implementations/encode_decode/encode_key2any.inc.in
providers/implementations/encode_decode/ml_dsa_codecs.c
providers/implementations/encode_decode/ml_kem_codecs.c
providers/implementations/include/prov/ml_dsa_codecs.h
providers/implementations/include/prov/ml_kem_codecs.h
test/evp_extra_test.c
util/perl/OpenSSL/paramnames.pm

index 94f906a9fc1b79b1139fdcde1eb525c50a2b40b1..c5ef15c60357df3792c9894f1462209440671738 100644 (file)
@@ -454,10 +454,6 @@ static int keygen_internal(ML_DSA_KEY *out)
         && ossl_ml_dsa_sk_encode(out);
 
 err:
-    if (out->seed != NULL && (out->prov_flags & ML_DSA_KEY_RETAIN_SEED) == 0) {
-        OPENSSL_clear_free(out->seed, ML_DSA_SEED_BYTES);
-        out->seed = NULL;
-    }
     EVP_MD_CTX_free(md_ctx);
     OPENSSL_cleanse(augmented_seed, sizeof(augmented_seed));
     OPENSSL_cleanse(expanded_seed, sizeof(expanded_seed));
index f316aaa8121c063c4115f6350da1e8923629aa20..ec76147331b943bc2ea1be16fcbbc2d7e385737e 100644 (file)
@@ -1418,14 +1418,9 @@ int genkey(const uint8_t seed[ML_KEM_SEED_BYTES],
     /* Save |z| portion of seed for "implicit rejection" on failure. */
     memcpy(key->z, seed + ML_KEM_RANDOM_BYTES, ML_KEM_RANDOM_BYTES);
 
-    /* Optionally save the |d| portion of the seed */
+    /* Save the |d| portion of the seed */
     key->d = key->z + ML_KEM_RANDOM_BYTES;
-    if (key->prov_flags & ML_KEM_KEY_RETAIN_SEED) {
-        memcpy(key->d, seed, ML_KEM_RANDOM_BYTES);
-    } else {
-        OPENSSL_cleanse(key->d, ML_KEM_RANDOM_BYTES);
-        key->d = NULL;
-    }
+    memcpy(key->d, seed, ML_KEM_RANDOM_BYTES);
 
     ret = 1;
  end:
index e87053b0a1832016041d84b8469f6d0b10dc0d4b..11498c161f7555e74502fdaa09d02d0ac2c234d0 100644 (file)
@@ -53,6 +53,25 @@ ML-DSA hashing operations.
 
 Use L<EVP_PKEY_CTX_set_params(3)> after calling L<EVP_PKEY_keygen_init(3)>.
 
+=head2 Encoder Parameters
+
+=over 4
+
+=item "output_formats" (B<OSSL_PKEY_PARAM_OUTPUT_FORMATS>) <UTF8 string>
+
+This parameter can be used with L<OSSL_ENCODER_CTX_new_for_pkey(3)>
+and L<OSSL_ENCODER_CTX_set_params(3)> to select the preferred B<PKCS#8> output
+formats of a private key.
+When this parameter is either not specified or empty, the behaviour is
+determined by the provider configuration parameters described below.
+Otherwise, it has the same syntax and behaviour as the provider configuration
+B<ml-dsa.output_formats> parameter, except that the B<ml-dsa.retain-seed>
+parameter is then ignored, i.e. the C<seed-only> and C<seed-priv> formats
+remain available so long as the seed was initially known, regardless of any
+setting of B<ml-dsa.retain-seed>.
+
+=back
+
 =head2 Common ML-DSA parameters
 
 In addition to the common parameters that all keytypes should support (see
@@ -91,10 +110,10 @@ configuration options programmatically.
 =item C<ml-dsa.retain_seed> (B<OSSL_PKEY_PARAM_ML_DSA_RETAIN_SEED>) <UTF8 string>
 
 When set to a string representing a false boolean value (see
-L<OSSL_PROVIDER_conf_get_bool(3)>), the seed will not be retained after key
-generation or key import from a seed value.
-If the resulting key is then written to a PKCS#8 object, it will contain
-only the FIPS 204 C<sk> key.
+L<OSSL_PROVIDER_conf_get_bool(3)>), the seed will not be included when
+keys are encoded to B<PKCS#8> form, unless the C<output_formats> encoder
+parameter is set to a nonempty value, as described above.
+The output B<PKCS#8> object will contain only the FIPS 204 C<sk> key.
 
 =item C<ml-dsa.prefer_seed> (B<OSSL_PKEY_PARAM_ML_DSA_PREFER_SEED>) <UTF8 string>
 
@@ -285,6 +304,8 @@ L<provider-keymgmt(7)>,
 L<EVP_PKEY_get_raw_private_key(3)>,
 L<EVP_PKEY_get_raw_public_key(3)>,
 L<EVP_PKEY_get1_encoded_public_key(3)>,
+L<OSSL_ENCODER_CTX_new_for_pkey(3)>,
+L<OSSL_ENCODER_CTX_set_params(3)>,
 L<OSSL_PROVIDER_add_conf_parameter(3)>,
 L<provider-keymgmt(7)>,
 L<EVP_SIGNATURE-ML-DSA(7)>
@@ -292,6 +313,7 @@ L<EVP_SIGNATURE-ML-DSA(7)>
 =head1 HISTORY
 
 This functionality was added in OpenSSL 3.5.
+The C<output_formats> B<OSSL_ENCODER_CTX> parameter was added in OpenSSL 4.0.
 
 =head1 COPYRIGHT
 
index be12a50ccff86634de41bf9c71c30389cc0824a2..d8bc67022db1403d245fce1b1a07f05f4aff0ac3 100644 (file)
@@ -53,11 +53,30 @@ Use L<EVP_PKEY_CTX_set_params(3)> after calling L<EVP_PKEY_keygen_init(3)>.
 
 =back
 
+=head2 Encoder Parameters
+
+=over 4
+
+=item "output_formats" (B<OSSL_PKEY_PARAM_OUTPUT_FORMATS>) <UTF8 string>
+
+This parameter can be used with L<OSSL_ENCODER_CTX_new_for_pkey(3)>
+and L<OSSL_ENCODER_CTX_set_params(3)> to select the preferred B<PKCS#8> output
+formats of a private key.
+When this parameter is either not specified or empty, the behaviour is
+determined by the provider configuration parameters described below.
+Otherwise, it has the same syntax and behaviour as the provider configuration
+B<ml-kem.output_formats> parameter, except that the B<ml-kem.retain-seed>
+parameter is then ignored, i.e. the C<seed-only> and C<seed-priv> formats
+remain available so long as the seed was initially known, regardless of any
+setting of B<ml-kem.retain-seed>.
+
+=back
+
 =head2 Common parameters
 
 In addition to the common parameters that all keytypes should support (see
 L<provider-keymgmt(7)/Common Information Parameters>), B<ML-KEM> keys
-keys support the parameters listed below.
+support the parameters listed below.
 These are gettable using
 L<EVP_PKEY_get_octet_string_param(3)> or L<EVP_PKEY_get_params(3)>.
 They can be initialised via L<EVP_PKEY_fromdata(3)>, and are returned by
@@ -122,10 +141,10 @@ Specifying any other value of the parameter, e.g. C<none>, skips the test.
 =item C<ml-kem.retain_seed> (B<OSSL_PKEY_PARAM_ML_KEM_RETAIN_SEED>) <UTF8 string>
 
 When set to a string representing a false boolean value (see
-L<OSSL_PROVIDER_conf_get_bool(3)>), the seed will not be retained after key
-generation or key import from a seed value.
-If the resulting key is then written to a PKCS#8 object, it will contain
-only the FIPS 203 C<dk> key.
+L<OSSL_PROVIDER_conf_get_bool(3)>), the seed will not be included when
+keys are encoded to B<PKCS#8> form, unless the C<output_formats> encoder
+parameter is set to a nonempty value, as described above.
+The output B<PKCS#8> object will contain only the FIPS 203 C<dk> key.
 
 =item C<ml-kem.prefer_seed> (B<OSSL_PKEY_PARAM_ML_KEM_PREFER_SEED>) <UTF8 string>
 
@@ -193,14 +212,14 @@ recognised on input.
 =item C<bare-seed>:
 
 This format represents B<PKCS#8> objects in which the private key contains
-the 64-byte FIPS 204 seed B<(d, z)> without any ASN.1 encapsulation.
+the 64-byte FIPS 203 seed B<(d, z)> without any ASN.1 encapsulation.
 If the C<bare-seed> format is not included in the list, this format will not be
 recognised on input.
 
 =item C<bare-priv>:
 
 This format represents B<PKCS#8> objects in which the private key contains
-the FIPS 204 decapsulation key B<dk> without any ASN.1 encapsulation.
+the FIPS 203 decapsulation key B<dk> without any ASN.1 encapsulation.
 If the C<bare-priv> format is not included in the list, this format will not be
 recognised on input.
 
@@ -305,6 +324,8 @@ L<EVP_PKEY(3)>,
 L<EVP_PKEY_get_raw_private_key(3)>,
 L<EVP_PKEY_get_raw_public_key(3)>,
 L<EVP_PKEY_get1_encoded_public_key(3)>,
+L<OSSL_ENCODER_CTX_new_for_pkey(3)>,
+L<OSSL_ENCODER_CTX_set_params(3)>,
 L<OSSL_PROVIDER_add_conf_parameter(3)>,
 L<provider-keymgmt(7)>,
 L<EVP_KEM-ML-KEM(7)>
@@ -312,6 +333,7 @@ L<EVP_KEM-ML-KEM(7)>
 =head1 HISTORY
 
 This functionality was added in OpenSSL 3.5.
+The C<output_formats> B<OSSL_ENCODER_CTX> parameter was added in OpenSSL 4.0.
 
 =head1 COPYRIGHT
 
index 66f796c03a28954277c152d896321713419183e0..77e0e3f59a4a602ca5113907e9e3e234f7f3abd1 100644 (file)
@@ -116,9 +116,6 @@ extern "C" {
 #define OSSL_KEM_PARAM_OPERATION_RSASVE     "RSASVE"
 #define OSSL_KEM_PARAM_OPERATION_DHKEM      "DHKEM"
 
-/* Provider configuration variables */
-#define OSSL_PKEY_RETAIN_SEED   "pkey_retain_seed"
-
 /* Parameter name definitions - generated by util/perl/OpenSSL/paramnames.pm */
 {- generate_public_macros(); -}
 
index c9945a548290f663defc820c8e1038735b00debb..c9a718694d8d9becf3531e5afbbbdb104ac248ea 100644 (file)
@@ -59,6 +59,9 @@ typedef struct key2any_ctx_st {
     EVP_CIPHER *cipher;
 
     struct ossl_passphrase_data_st pwdata;
+
+    /* Just-in-time ML-KEM and ML-DSA output format override */
+    char *output_formats;
 } KEY2ANY_CTX;
 
 typedef int check_key_type_fn(const void *key, int nid);
@@ -864,7 +867,8 @@ static int ml_dsa_pki_priv_to_der(const void *vkey, unsigned char **pder,
 {
     KEY2ANY_CTX *ctx = vctx;
 
-    return ossl_ml_dsa_i2d_prvkey(vkey, pder, ctx->provctx);
+    return ossl_ml_dsa_i2d_prvkey(vkey, pder,
+                                  ctx->provctx, ctx->output_formats);
 }
 
 # define ml_dsa_epki_priv_to_der ml_dsa_pki_priv_to_der
@@ -894,7 +898,8 @@ static int ml_kem_pki_priv_to_der(const void *vkey, unsigned char **pder,
 {
     KEY2ANY_CTX *ctx = vctx;
 
-    return ossl_ml_kem_i2d_prvkey(vkey, pder, ctx->provctx);
+    return ossl_ml_kem_i2d_prvkey(vkey, pder,
+                                  ctx->provctx, ctx->output_formats);
 }
 
 # define ml_kem_epki_priv_to_der ml_kem_pki_priv_to_der
@@ -1130,6 +1135,7 @@ static void key2any_freectx(void *vctx)
 
     ossl_pw_clear_passphrase_data(&ctx->pwdata);
     EVP_CIPHER_free(ctx->cipher);
+    OPENSSL_free(ctx->output_formats);
     OPENSSL_free(ctx);
 }
 
@@ -1169,6 +1175,15 @@ static int key2any_set_ctx_params(void *vctx, const OSSL_PARAM params[])
     if (p.svprm != NULL && !OSSL_PARAM_get_int(p.svprm, &ctx->save_parameters))
         return 0;
 
+    if (p.output_formats != NULL) {
+        char *val = NULL;
+
+        if (!OSSL_PARAM_get_utf8_string(p.output_formats, &val, 0))
+            return 0;
+        OPENSSL_free(ctx->output_formats);
+        ctx->output_formats = *val != '\0' ? val : NULL;
+    }
+
     return 1;
 }
 
index 6fe9688d4ed8f9f3bd3c76594a7fa77b63e0ecf6..fe59f3072ecb813374dd72aff97665dbce71b6c6 100644 (file)
@@ -12,7 +12,8 @@ use OpenSSL::paramnames qw(produce_param_decoder);
 -}
 
 {- produce_param_decoder('key2any_set_ctx_params',
-                         (['OSSL_ENCODER_PARAM_CIPHER',          'cipher', 'utf8_string'],
-                          ['OSSL_ENCODER_PARAM_PROPERTIES',      'propq',  'utf8_string'],
-                          ['OSSL_ENCODER_PARAM_SAVE_PARAMETERS', 'svprm',  'int'],
+                         (['OSSL_ENCODER_PARAM_CIPHER',          'cipher',         'utf8_string'],
+                          ['OSSL_ENCODER_PARAM_PROPERTIES',      'propq',          'utf8_string'],
+                          ['OSSL_ENCODER_PARAM_SAVE_PARAMETERS', 'svprm',          'int'],
+                          ['OSSL_PKEY_PARAM_OUTPUT_FORMATS',     'output_formats', 'utf8_string'],
                          )); -}
index 7850c94e3bab2a87b955c4a0a74e4ea7236234f0..14474528b253b7f3feb8bbd9f3655181df1ed451 100644 (file)
@@ -283,14 +283,13 @@ int ossl_ml_dsa_i2d_pubkey(const ML_DSA_KEY *key, unsigned char **out)
 
 /* Allocate and encode PKCS#8 private key payload. */
 int ossl_ml_dsa_i2d_prvkey(const ML_DSA_KEY *key, uint8_t **out,
-                           PROV_CTX *provctx)
+                           PROV_CTX *provctx, const char *formats)
 {
     const ML_DSA_PARAMS *params = ossl_ml_dsa_key_params(key);
     const ML_COMMON_CODEC *codec;
     ML_COMMON_PKCS8_FMT_PREF *fmt_slots, *slot;
     const ML_COMMON_PKCS8_FMT *p8fmt;
     uint8_t *buf = NULL, *pos;
-    const char *formats;
     int len = ML_DSA_SEED_BYTES;
     int ret = 0;
     const uint8_t *seed = ossl_ml_dsa_key_get_seed(key);
@@ -307,8 +306,13 @@ int ossl_ml_dsa_i2d_prvkey(const ML_DSA_KEY *key, uint8_t **out,
         return 0;
     }
 
-    formats = ossl_prov_ctx_get_param(
-        provctx, OSSL_PKEY_PARAM_ML_DSA_OUTPUT_FORMATS, NULL);
+    if (formats == NULL) {
+        if ((ossl_ml_dsa_key_get_prov_flags(key) & ML_DSA_KEY_RETAIN_SEED) == 0)
+            seed = NULL;
+        formats = ossl_prov_ctx_get_param(provctx,
+                                          OSSL_PKEY_PARAM_ML_DSA_OUTPUT_FORMATS,
+                                          NULL);
+    }
     fmt_slots = ossl_ml_common_pkcs8_fmt_order(params->alg, codec->p8fmt,
                                                "output", formats);
     if (fmt_slots == NULL)
index bbc52aa10ca812dddc0198fcbd4448557aa8dd02..6e007b44cfb849b4357c025d7626670deb6341c5 100644 (file)
@@ -302,15 +302,15 @@ int ossl_ml_kem_i2d_pubkey(const ML_KEM_KEY *key, unsigned char **out)
 
 /* Allocate and encode PKCS#8 private key payload. */
 int ossl_ml_kem_i2d_prvkey(const ML_KEM_KEY *key, uint8_t **out,
-                           PROV_CTX *provctx)
+                           PROV_CTX *provctx, const char *formats)
 {
     const ML_KEM_VINFO *v = key->vinfo;
     const ML_COMMON_CODEC *codec;
     ML_COMMON_PKCS8_FMT_PREF *fmt_slots, *slot;
     const ML_COMMON_PKCS8_FMT *p8fmt;
     uint8_t *buf = NULL, *pos;
-    const char *formats;
     int len = ML_KEM_SEED_BYTES;
+    int have_seed = ossl_ml_kem_have_seed(key);
     int ret = 0;
 
     /* Not ours to handle */
@@ -324,8 +324,13 @@ int ossl_ml_kem_i2d_prvkey(const ML_KEM_KEY *key, uint8_t **out,
         return 0;
     }
 
-    formats = ossl_prov_ctx_get_param(
-        provctx, OSSL_PKEY_PARAM_ML_KEM_OUTPUT_FORMATS, NULL);
+    if (formats == NULL) {
+        if ((key->prov_flags & ML_KEM_KEY_RETAIN_SEED) == 0)
+            have_seed = 0;
+        formats = ossl_prov_ctx_get_param(provctx,
+                                          OSSL_PKEY_PARAM_ML_KEM_OUTPUT_FORMATS,
+                                          NULL);
+    }
     fmt_slots = ossl_ml_common_pkcs8_fmt_order(v->algorithm_name, codec->p8fmt,
                                                "output", formats);
     if (fmt_slots == NULL)
@@ -333,7 +338,7 @@ int ossl_ml_kem_i2d_prvkey(const ML_KEM_KEY *key, uint8_t **out,
 
     /* If we don't have a seed, skip seedful entries */
     for (slot = fmt_slots; (p8fmt = slot->fmt) != NULL; ++slot)
-        if (ossl_ml_kem_have_seed(key) || p8fmt->seed_length == 0)
+        if (have_seed || p8fmt->seed_length == 0)
             break;
     /* No matching table entries, give up */
     if (p8fmt == NULL
index 722434baaeeda354a0bc8fe64360ecd897b99690..34bde63fe946dd87251551cf63bbec79deace0c4 100644 (file)
@@ -33,7 +33,7 @@ int ossl_ml_dsa_i2d_pubkey(const ML_DSA_KEY *key, unsigned char **out);
 __owur
 __owur
 int ossl_ml_dsa_i2d_prvkey(const ML_DSA_KEY *key, unsigned char **out,
-                           PROV_CTX *provctx);
+                           PROV_CTX *provctx, const char *formats);
 
 # endif /* OPENSSL_NO_ML_DSA */
 #endif  /* PROV_ML_DSA_CODECS_H */
index 1a48c25ce39f10b9ada38d8570b76b1ce7b16b19..406a46601b34b9d7c55c16b9c5bf537ba84167ff 100644 (file)
@@ -33,7 +33,7 @@ int ossl_ml_kem_i2d_pubkey(const ML_KEM_KEY *key, unsigned char **out);
 __owur
 __owur
 int ossl_ml_kem_i2d_prvkey(const ML_KEM_KEY *key, unsigned char **out,
-                           PROV_CTX *provctx);
+                           PROV_CTX *provctx, const char *formats);
 
 # endif  /* OPENSSL_NO_ML_KEM */
 #endif  /* PROV_ML_KEM_CODECS_H */
index ec8179d7730cd253dd15263500358e0beab6320c..9ec90fc308f5131a18a7740d103d2d255c78fe55 100644 (file)
@@ -29,6 +29,7 @@
 #include <openssl/dh.h>
 #include <openssl/aes.h>
 #include <openssl/decoder.h>
+#include <openssl/encoder.h>
 #include <openssl/rsa.h>
 #include <openssl/proverr.h>
 #include <openssl/rand.h>
@@ -737,6 +738,39 @@ static const unsigned char kExampleDHKeyDER[] = {
 # endif
 #endif
 
+#ifndef OPENSSL_NO_ML_KEM
+/*
+ * openssl genpkey -provparam ml-kem.output_formats=seed-only \
+ *   -algorithm ml-kem-512 -outform DER |
+ *   xxd -i
+ */
+static const unsigned char kMLKEMSeedOnlyDER[] = {
+    0x30, 0x54, 0x02, 0x01, 0x00, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48,
+    0x01, 0x65, 0x03, 0x04, 0x04, 0x01, 0x04, 0x42, 0x80, 0x40, 0x21, 0xe4,
+    0x30, 0x56, 0x08, 0x64, 0xd3, 0xd4, 0x37, 0x33, 0x1c, 0xe5, 0xc9, 0xd8,
+    0x26, 0x10, 0x9b, 0x4d, 0x58, 0xb4, 0xe2, 0x57, 0x70, 0x0f, 0x28, 0xe2,
+    0xd2, 0xa8, 0x6b, 0xb6, 0x2b, 0x85, 0x1b, 0x25, 0x39, 0x29, 0xca, 0x6d,
+    0x9a, 0xf0, 0x11, 0x5d, 0xca, 0xf4, 0xf7, 0x9a, 0x50, 0x39, 0xa7, 0x52,
+    0x39, 0x5d, 0x84, 0xa9, 0xb9, 0x84, 0x4c, 0xa2, 0xe5, 0x49, 0xd7, 0x81,
+    0xd1, 0x6d
+};
+#endif
+
+#ifndef OPENSSL_NO_ML_DSA
+/*
+ * openssl genpkey -provparam ml-dsa.output_formats=seed-only \
+ *   -algorithm ml-dsa-44 -outform DER |
+ *   xxd -i
+ */
+static const unsigned char kMLDSASeedOnlyDER[] = {
+    0x30, 0x34, 0x02, 0x01, 0x00, 0x30, 0x0b, 0x06, 0x09, 0x60, 0x86, 0x48,
+    0x01, 0x65, 0x03, 0x04, 0x03, 0x11, 0x04, 0x22, 0x80, 0x20, 0xc5, 0xbb,
+    0x12, 0x9c, 0x76, 0xac, 0x6f, 0x03, 0x6c, 0x56, 0x32, 0xf4, 0x66, 0xb8,
+    0x8c, 0x77, 0x4b, 0xe0, 0xaa, 0x4a, 0xd6, 0xa0, 0x96, 0x12, 0xc3, 0x8c,
+    0xe7, 0x71, 0xbe, 0xf8, 0xba, 0xbe
+};
+#endif
+
 static const unsigned char kCFBDefaultKey[] = {
     0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, 0xAB, 0xF7, 0x15, 0x88,
     0x09, 0xCF, 0x4F, 0x3C
@@ -926,6 +960,140 @@ static EVP_PKEY *load_example_ec_key(void)
 }
 #endif
 
+#if !defined(OPENSSL_NO_ML_KEM) || !defined(OPENSSL_NO_ML_DSA)
+static EVP_PKEY *load_ml_key(const char *keytype, const char *input_type,
+                             const unsigned char *data, size_t data_len)
+{
+    const unsigned char **pdata = &data;
+    EVP_PKEY *pkey = NULL;
+    OSSL_DECODER_CTX *dctx =
+        OSSL_DECODER_CTX_new_for_pkey(&pkey, input_type, "PrivateKeyInfo",
+                                      keytype, 0, testctx, testpropq);
+
+    if (!TEST_ptr(dctx))
+        return NULL;
+    /* |pkey| will be NULL on error */
+    (void)OSSL_DECODER_from_data(dctx, pdata, &data_len);
+    OSSL_DECODER_CTX_free(dctx);
+    return pkey;
+}
+
+static int
+store_ml_key(EVP_PKEY *pkey, const char *input_type, const char *fmts,
+             const unsigned char *expect, size_t expectlen)
+{
+    OSSL_PARAM params[2] = { OSSL_PARAM_END, OSSL_PARAM_END };
+    OSSL_ENCODER_CTX *ectx = NULL;
+    unsigned char *buf = NULL, *der = NULL;
+    size_t len = 0;
+    long derlen;
+    int ret = 0;
+
+    /* Just-in-time encoding format selection */
+    params[0] = OSSL_PARAM_construct_utf8_string(OSSL_PKEY_PARAM_OUTPUT_FORMATS,
+                                                 (char *)fmts, 0);
+
+    ectx = OSSL_ENCODER_CTX_new_for_pkey(pkey, EVP_PKEY_KEYPAIR, input_type,
+                                         "PrivateKeyInfo", testpropq);
+    if (!TEST_ptr(ectx))
+        return 0;
+    if (!TEST_true(OSSL_ENCODER_CTX_set_params(ectx, params))
+        || !TEST_true(OSSL_ENCODER_to_data(ectx, &buf, &len)))
+        goto end;
+
+    if (strcmp(input_type, "PEM") == 0) {
+        BIO *pembio = BIO_new_mem_buf(buf, len);
+        char *name = NULL, *header = NULL;
+
+        if (!TEST_ptr(pembio))
+            goto end;
+        ret = PEM_read_bio(pembio, &name, &header, &der, &derlen);
+        BIO_free(pembio);
+        if (TEST_true(ret)) {
+            if (!TEST_int_eq(strcmp(name, PEM_STRING_PKCS8INF), 0)
+                || !TEST_true(header == NULL || *header == '\0'))
+                ret = 0;
+            OPENSSL_free(name);
+            OPENSSL_free(header);
+            if (!ret)
+                goto end;
+        }
+    } else {
+        der = buf;
+        derlen = len;
+    }
+    ret = expect != NULL ?
+        TEST_mem_eq(der, (size_t) derlen, expect, expectlen) :
+        TEST_size_t_eq((size_t) derlen, expectlen);
+
+ end:
+    OSSL_ENCODER_CTX_free(ectx);
+    if (der != buf)
+        OPENSSL_free(der);
+    OPENSSL_free(buf);
+    return ret;
+}
+
+static int test_ml_seed_only(int idx)
+{
+    const char *alg;
+    const unsigned char *seedonly;
+    EVP_PKEY *pkey = NULL;
+    size_t seedonlysz, privonlysz;
+    const char *outform = (idx & 1) ? "DER" : "PEM";
+    int ret = 0;
+
+    if (idx & 2) {
+# ifndef OPENSSL_NO_ML_DSA
+        alg = "ML-DSA-44";
+        seedonly = kMLDSASeedOnlyDER;
+        seedonlysz = sizeof(kMLDSASeedOnlyDER);
+        privonlysz = 2588;
+# else
+        return 0;
+# endif
+    } else {
+# ifndef OPENSSL_NO_ML_KEM
+        alg = "ML-KEM-512";
+        seedonly = kMLKEMSeedOnlyDER;
+        seedonlysz = sizeof(kMLKEMSeedOnlyDER);
+        privonlysz = 1660;
+# else
+        return 0;
+# endif
+    }
+
+    pkey = load_ml_key(alg, "DER", seedonly, seedonlysz);
+    if (!TEST_ptr(pkey))
+        return 0;
+
+    /*
+     * Check that the "output_formats" parameter is behaving as expected.
+     * With "seed-only" check full payload, otherwise just the DER length.
+     */
+    if (store_ml_key(pkey, outform, "seed-only", seedonly, seedonlysz)
+        && store_ml_key(pkey, outform, "bare-seed", NULL, seedonlysz - 2)
+        && store_ml_key(pkey, outform, "priv-only", NULL, privonlysz))
+        ret = 1;
+
+    EVP_PKEY_free(pkey);
+    return ret;
+}
+#endif
+
+#ifndef OPENSSL_NO_ML_KEM
+static int test_ml_kem_seed_only(int idx)
+{
+    return test_ml_seed_only(idx);
+}
+#endif
+#ifndef OPENSSL_NO_ML_DSA
+static int test_ml_dsa_seed_only(int idx)
+{
+    return test_ml_seed_only(idx + 2);
+}
+#endif
+
 #ifndef OPENSSL_NO_DEPRECATED_3_0
 # ifndef OPENSSL_NO_DH
 static EVP_PKEY *load_example_dh_key(void)
@@ -6910,6 +7078,13 @@ int setup_tests(void)
 
     ADD_TEST(test_evp_cipher_pipeline);
 
+#ifndef OPENSSL_NO_ML_KEM
+    ADD_ALL_TESTS(test_ml_kem_seed_only, 2);
+#endif
+#ifndef OPENSSL_NO_ML_DSA
+    ADD_ALL_TESTS(test_ml_dsa_seed_only, 2);
+#endif
+
     return 1;
 }
 
index b73863b98557f0d88ee7951f4a063d6c44d07fb6..744edb10e36d432a06a14d098244d1267dd8ba1e 100644 (file)
@@ -307,6 +307,7 @@ my %params = (
     'OSSL_PKEY_PARAM_DIST_ID' =>             "distid",
     'OSSL_PKEY_PARAM_PUB_KEY' =>             "pub",
     'OSSL_PKEY_PARAM_PRIV_KEY' =>            "priv",
+    'OSSL_PKEY_PARAM_OUTPUT_FORMATS' =>      "output_formats",
     # PKEY_PARAM_IMPLICIT_REJECTION isn't actually used, or meaningful.  We keep
     # it for API stability, but please use ASYM_CIPHER_PARAM_IMPLICIT_REJECTION
     # instead.