Commit 71d721d357 for openssl.org

commit 71d721d3574b9783cd0149026b4a52f67794506a
Author: Bob Beck <beck@openssl.org>
Date:   Sun Aug 16 14:59:52 2026 -0700

    Stop running the entire ecparam corpus through the app

    Checking 136 parameter files with ecparam and pkeyparam cost a process
    per file per check, 641 in all, which is process startup rather than
    coverage.  Test all of these in one process per set in C instead.

    The app keeps it's own test, covering the options and a few
    files through the output path, in 20-test_app_ecparam.t.

    Together the two now take 1.0 seconds where the one took 24.5.

    Reviewed-by: Andrew Dinh <andrewd@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Tue Sep  1 14:17:19 2026
    Merged-from: https://github.com/openssl/openssl/pull/32404

diff --git a/test/build.info b/test/build.info
index 260817fd2c..6be71ede1a 100644
--- a/test/build.info
+++ b/test/build.info
@@ -168,6 +168,11 @@ IF[{- !$disabled{tests} -}]
     SOURCE[genec_test]=genec_test.c
     INCLUDE[genec_test]=../include ../apps/include
     DEPEND[genec_test]=../libcrypto libtestutil.a
+
+    PROGRAMS{noinst}=ecparam_test
+    SOURCE[ecparam_test]=ecparam_test.c
+    INCLUDE[ecparam_test]=../include ../apps/include
+    DEPEND[ecparam_test]=../libcrypto libtestutil.a
   ENDIF

   SOURCE[gmdifftest]=gmdifftest.c
diff --git a/test/ecparam_test.c b/test/ecparam_test.c
new file mode 100644
index 0000000000..462b2d2ce9
--- /dev/null
+++ b/test/ecparam_test.c
@@ -0,0 +1,264 @@
+/*
+ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <string.h>
+
+#include <openssl/bio.h>
+#include <openssl/core_names.h>
+#include <openssl/decoder.h>
+#include <openssl/encoder.h>
+#include <openssl/evp.h>
+#include <openssl/pem.h>
+
+#include "testutil.h"
+
+/*-
+ * Sweep a corpus of EC parameter files, exercising what the ecparam and
+ * pkeyparam applications do to each one.  The applications are covered
+ * separately in 20-test_app_ecparam.t; running the whole corpus through
+ * them costs a process per file per check, which is startup time rather
+ * than test coverage.
+ *
+ * Invoked as:
+ *
+ *     ecparam_test valid|noncanon|invalid <file>...
+ *
+ * Valid and non-canonically encoded parameters must load and check.
+ * Invalid ones must not.  Only the canonically encoded valid files are
+ * expected to re-encode to exactly the bytes they were read from.
+ */
+typedef enum {
+    CORPUS_VALID,
+    CORPUS_NONCANON,
+    CORPUS_INVALID
+} corpus_kind;
+
+static corpus_kind corpus;
+static int expect_check; /* Whether loading and checking should succeed */
+static int num_files;
+
+/* The files start at argument 1; argument 0 names the corpus. */
+static const char *corpus_file(int idx)
+{
+    return test_get_argument(idx + 1);
+}
+
+/*
+ * Load domain parameters the way the applications do.  ecparam insists the
+ * result be an EC or SM2 key, pkeyparam takes whatever it is given.
+ */
+static EVP_PKEY *load_params(const char *file, int ec_only)
+{
+    EVP_PKEY *pkey = NULL;
+    OSSL_DECODER_CTX *dctx = NULL;
+    BIO *bio = BIO_new_file(file, "rb");
+
+    if (bio == NULL)
+        return NULL;
+
+    dctx = OSSL_DECODER_CTX_new_for_pkey(&pkey, "PEM", NULL, NULL,
+        OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS, NULL, NULL);
+    if (dctx == NULL) {
+        BIO_free(bio);
+        return NULL;
+    }
+
+    if (!OSSL_DECODER_from_bio(dctx, bio)) {
+        EVP_PKEY_free(pkey);
+        pkey = NULL;
+    }
+
+    OSSL_DECODER_CTX_free(dctx);
+    BIO_free(bio);
+
+    if (pkey != NULL && ec_only
+        && !EVP_PKEY_is_a(pkey, "EC") && !EVP_PKEY_is_a(pkey, "SM2")) {
+        EVP_PKEY_free(pkey);
+        pkey = NULL;
+    }
+
+    return pkey;
+}
+
+/* Run EVP_PKEY_param_check() as the applications do after loading. */
+static int check_params(EVP_PKEY *pkey)
+{
+    EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_from_pkey(NULL, pkey, NULL);
+    int ret;
+
+    if (ctx == NULL)
+        return 0;
+
+    ret = EVP_PKEY_param_check(ctx) > 0;
+    EVP_PKEY_CTX_free(ctx);
+    return ret;
+}
+
+/*
+ * Load and check, optionally restricting the check to named curves as
+ * "ecparam -check_named" does.  Returns 1 if the outcome matched what the
+ * corpus expects.
+ */
+static int load_and_check(int idx, int ec_only, int named)
+{
+    const char *file = corpus_file(idx);
+    EVP_PKEY *pkey = load_params(file, ec_only);
+    int ok;
+
+    if (pkey == NULL)
+        return TEST_int_eq(expect_check, 0);
+
+    if (named
+        && !TEST_true(EVP_PKEY_set_utf8_string_param(pkey,
+            OSSL_PKEY_PARAM_EC_GROUP_CHECK_TYPE,
+            OSSL_PKEY_EC_GROUP_CHECK_NAMED))) {
+        EVP_PKEY_free(pkey);
+        return 0;
+    }
+
+    ok = check_params(pkey);
+    EVP_PKEY_free(pkey);
+
+    if (!TEST_int_eq(ok, expect_check)) {
+        TEST_info("%s", file);
+        return 0;
+    }
+    return 1;
+}
+
+static int test_ecparam_check(int idx)
+{
+    return load_and_check(idx, 1, 0);
+}
+
+static int test_ecparam_check_named(int idx)
+{
+    return load_and_check(idx, 1, 1);
+}
+
+static int test_pkeyparam_check(int idx)
+{
+    return load_and_check(idx, 0, 0);
+}
+
+/* Read a whole file, so the re-encoded form can be compared against it. */
+static int read_file(const char *file, unsigned char **out, long *out_len)
+{
+    BIO *bio = BIO_new_file(file, "rb");
+    unsigned char *buf = NULL;
+    long len = 0, n, i, j;
+
+    if (bio == NULL)
+        return 0;
+
+    for (;;) {
+        unsigned char *tmp = OPENSSL_realloc(buf, (size_t)len + 4096);
+
+        if (tmp == NULL) {
+            OPENSSL_free(buf);
+            BIO_free(bio);
+            return 0;
+        }
+        buf = tmp;
+        n = BIO_read(bio, buf + len, 4096);
+        if (n <= 0)
+            break;
+        len += n;
+    }
+
+    BIO_free(bio);
+
+    for (i = 0, j = 0; i < len; i++)
+        if (buf[i] != '\r')
+            buf[j++] = buf[i];
+
+    *out = buf;
+    *out_len = j;
+    return 1;
+}
+
+/*
+ * Canonically encoded parameters must survive a decode and re-encode
+ * unchanged, which is what "ecparam -in x -out y" is checked to do.
+ */
+static int test_reencode(int idx)
+{
+    const char *file = corpus_file(idx);
+    EVP_PKEY *pkey = load_params(file, 1);
+    OSSL_ENCODER_CTX *ectx = NULL;
+    BIO *mem = NULL;
+    unsigned char *orig = NULL;
+    char *enc = NULL;
+    long orig_len = 0;
+    long enc_len;
+    int ret = 0;
+
+    if (!TEST_ptr(pkey))
+        goto err;
+
+    if (!TEST_ptr(mem = BIO_new(BIO_s_mem())))
+        goto err;
+
+    ectx = OSSL_ENCODER_CTX_new_for_pkey(pkey,
+        OSSL_KEYMGMT_SELECT_DOMAIN_PARAMETERS, "PEM", NULL, NULL);
+    if (!TEST_ptr(ectx) || !TEST_true(OSSL_ENCODER_to_bio(ectx, mem)))
+        goto err;
+
+    if (!TEST_true(read_file(file, &orig, &orig_len)))
+        goto err;
+
+    enc_len = BIO_get_mem_data(mem, &enc);
+    if (!TEST_mem_eq(enc, (size_t)enc_len, orig, (size_t)orig_len)) {
+        TEST_info("%s", file);
+        goto err;
+    }
+
+    ret = 1;
+err:
+    OPENSSL_free(orig);
+    OSSL_ENCODER_CTX_free(ectx);
+    BIO_free(mem);
+    EVP_PKEY_free(pkey);
+    return ret;
+}
+
+int setup_tests(void)
+{
+    const char *kind;
+    size_t argc = test_get_argument_count();
+
+    if (!TEST_size_t_gt(argc, 1)) {
+        TEST_error("usage: ecparam_test valid|noncanon|invalid <file>...");
+        return 0;
+    }
+
+    kind = test_get_argument(0);
+    if (strcmp(kind, "valid") == 0) {
+        corpus = CORPUS_VALID;
+    } else if (strcmp(kind, "noncanon") == 0) {
+        corpus = CORPUS_NONCANON;
+    } else if (strcmp(kind, "invalid") == 0) {
+        corpus = CORPUS_INVALID;
+    } else {
+        TEST_error("unknown corpus \"%s\"", kind);
+        return 0;
+    }
+
+    expect_check = corpus != CORPUS_INVALID;
+    num_files = (int)argc - 1;
+
+    ADD_ALL_TESTS(test_ecparam_check, num_files);
+    ADD_ALL_TESTS(test_ecparam_check_named, num_files);
+    ADD_ALL_TESTS(test_pkeyparam_check, num_files);
+    /* Only the canonical encodings are expected to be byte stable. */
+    if (corpus == CORPUS_VALID)
+        ADD_ALL_TESTS(test_reencode, num_files);
+
+    return 1;
+}
diff --git a/test/recipes/15-test_ecparam.t b/test/recipes/15-test_ecparam.t
index 6ff1df815b..20e664873d 100644
--- a/test/recipes/15-test_ecparam.t
+++ b/test/recipes/15-test_ecparam.t
@@ -1,5 +1,5 @@
 #! /usr/bin/env perl
-# Copyright 2017-2025 The OpenSSL Project Authors. All Rights Reserved.
+# Copyright 2017-2026 The OpenSSL Project Authors. All Rights Reserved.
 #
 # Licensed under the Apache License 2.0 (the "License").  You may not use
 # this file except in compliance with the License.  You can obtain a copy
@@ -10,11 +10,8 @@
 use strict;
 use warnings;

-use File::Spec;
-use File::Copy;
-use File::Compare qw/compare_text compare/;
 use OpenSSL::Glob;
-use OpenSSL::Test qw/:DEFAULT data_file srctop_file bldtop_dir/;
+use OpenSSL::Test qw/:DEFAULT data_file/;
 use OpenSSL::Test::Utils;

 setup("test_ecparam");
@@ -30,286 +27,15 @@ if (disabled("sm2")) {
     @valid = grep { !/sm2-.*\.pem/} @valid;
 }

-plan tests => 18;
+# The corpus is swept in a single process per set; the ecparam and
+# pkeyparam applications themselves are covered by 20-test_app_ecparam.t.
+plan tests => 3;

-sub checkload {
-    my $files = shift; # List of files
-    my $valid = shift; # Check should pass or fail?
-    my $app = shift;   # Which application
-    my $opt = shift;   # Additional option
+ok(run(test(["ecparam_test", "valid", @valid])),
+   "Load and check valid parameters");

-    foreach (@$files) {
-        if ($valid) {
-            ok(run(app(['openssl', $app, '-noout', $opt, '-in', $_])));
-        } else {
-            ok(!run(app(['openssl', $app, '-noout', $opt, '-in', $_])));
-        }
-    }
-}
-
-sub checkcompare {
-    my $files = shift; # List of files
-    my $app = shift;   # Which application
-
-    foreach (@$files) {
-        my $testout = "$app.tst";
-
-        ok(run(app(['openssl', $app, '-out', $testout, '-in', $_])));
-        ok(!compare_text($_, $testout, sub {
-            my $in1 = $_[0];
-            my $in2 = $_[1];
-            $in1 =~ s/\r\n/\n/g;
-            $in2 =~ s/\r\n/\n/g;
-            $in1 ne $in2}), "Original file $_ is the same as new one");
-    }
-}
-
-sub check_identical {
-    my $apps = shift; # List of applications
-
-    foreach (@$apps) {
-        my $inout = "$_.tst";
-        my $backup = "backup.tst";
-
-        copy($inout, $backup);
-        ok(run(app(['openssl', $_, '-in', $inout, '-out', $inout])));
-        ok(!compare($inout, $backup), "converted file $inout did not change");
-    }
-}
-
-my $no_fips = disabled('fips') || ($ENV{NO_FIPS} // 0);
-
-subtest "Check loading valid parameters by ecparam with -check" => sub {
-    plan tests => scalar(@valid);
-    checkload(\@valid, 1, "ecparam", "-check");
-};
-
-subtest "Check loading valid parameters by ecparam with -check_named" => sub {
-    plan tests => scalar(@valid);
-    checkload(\@valid, 1, "ecparam", "-check_named");
-};
-
-subtest "Check loading valid parameters by pkeyparam with -check" => sub {
-    plan tests => scalar(@valid);
-    checkload(\@valid, 1, "pkeyparam", "-check");
-};
-
-subtest "Check loading non-canonically encoded parameters by ecparam with -check" => sub {
-    plan tests => scalar(@noncanon);
-    checkload(\@noncanon, 1, "ecparam", "-check");
-};
-
-subtest "Check loading non-canonically encoded parameters by ecparam with -check_named" => sub {
-    plan tests => scalar(@noncanon);
-    checkload(\@noncanon, 1, "ecparam", "-check_named");
-};
-
-subtest "Check loading non-canonically encoded parameters by pkeyparam with -check" => sub {
-    plan tests => scalar(@noncanon);
-    checkload(\@noncanon, 1, "pkeyparam", "-check");
-};
-
-subtest "Check loading invalid parameters by ecparam with -check" => sub {
-    plan tests => scalar(@invalid);
-    checkload(\@invalid, 0, "ecparam", "-check");
-};
-
-subtest "Check loading invalid parameters by ecparam with -check_named" => sub {
-    plan tests => scalar(@invalid);
-    checkload(\@invalid, 0, "ecparam", "-check_named");
-};
-
-subtest "Check loading invalid parameters by pkeyparam with -check" => sub {
-    plan tests => scalar(@invalid);
-    checkload(\@invalid, 0, "pkeyparam", "-check");
-};
-
-subtest "Check ecparam does not change the parameter file on output" => sub {
-    plan tests => 2 * scalar(@valid);
-    checkcompare(\@valid, "ecparam");
-};
-
-subtest "Check pkeyparam does not change the parameter file on output" => sub {
-    plan tests => 2 * scalar(@valid);
-    checkcompare(\@valid, "pkeyparam");
-};
-
-my @apps = ("ecparam", "pkeyparam");
-subtest "Check param apps do not garble infile identical to outfile" => sub {
-    plan tests => 2 * scalar(@apps);
-    check_identical(\@apps);
-};
-
-subtest "Check loading of fips and non-fips params" => sub {
-    plan skip_all => "FIPS is disabled"
-        if $no_fips;
-    plan tests => 8;
-
-    my $fipsconf = srctop_file("test", "fips-and-base.cnf");
-    my $defaultconf = srctop_file("test", "default.cnf");
-
-    $ENV{OPENSSL_CONF} = $fipsconf;
-
-    ok(run(app(['openssl', 'ecparam',
-                '-in', data_file('valid', 'secp384r1-explicit.pem'),
-                '-check'])),
-       "Loading explicitly encoded valid curve");
-
-    ok(run(app(['openssl', 'ecparam',
-                '-in', data_file('valid', 'secp384r1-named.pem'),
-                '-check'])),
-       "Loading named valid curve");
-
-    ok(!run(app(['openssl', 'ecparam',
-                '-in', data_file('valid', 'secp112r1-named.pem'),
-                '-check'])),
-       "Fail loading named non-fips curve");
-
-    ok(!run(app(['openssl', 'pkeyparam',
-                '-in', data_file('valid', 'secp112r1-named.pem'),
-                '-check'])),
-       "Fail loading named non-fips curve using pkeyparam");
-
-    ok(run(app(['openssl', 'ecparam',
-                '-provider', 'default',
-                '-propquery', '?fips!=yes',
-                '-in', data_file('valid', 'secp112r1-named.pem'),
-                '-check'])),
-       "Loading named non-fips curve in FIPS mode with non-FIPS property".
-       " query");
-
-    ok(run(app(['openssl', 'pkeyparam',
-                '-provider', 'default',
-                '-propquery', '?fips!=yes',
-                '-in', data_file('valid', 'secp112r1-named.pem'),
-                '-check'])),
-       "Loading named non-fips curve in FIPS mode with non-FIPS property".
-       " query using pkeyparam");
-
-    ok(!run(app(['openssl', 'ecparam',
-                '-genkey', '-name', 'secp112r1'])),
-       "Fail generating key for named non-fips curve");
-
-    ok(run(app(['openssl', 'ecparam',
-                '-provider', 'default',
-                '-propquery', '?fips!=yes',
-                '-genkey', '-name', 'secp112r1'])),
-       "Generating key for named non-fips curve with non-FIPS property query");
-
-    $ENV{OPENSSL_CONF} = $defaultconf;
-};
-
-subtest "Check ecparam -param_enc converts between named and explicit" => sub {
-    plan tests => 3;
-
-    my $named = data_file('valid', 'secp384r1-named.pem');
-    my $explicit = data_file('valid', 'secp384r1-explicit.pem');
-
-    # The encodings are canonical, so re-encoding a named curve as explicit
-    # (and vice versa) must reproduce the matching reference file byte for byte.
-    my $to_explicit = 'param-explicit.tst';
-    ok(run(app(['openssl', 'ecparam', '-in', $named, '-param_enc', 'explicit',
-                '-out', $to_explicit]))
-       && !compare($to_explicit, $explicit),
-       "named_curve params re-encoded as explicit match the reference file");
-
-    my $to_named = 'param-named.tst';
-    ok(run(app(['openssl', 'ecparam', '-in', $explicit, '-param_enc',
-                'named_curve', '-out', $to_named]))
-       && !compare($to_named, $named),
-       "explicit params re-encoded as named_curve match the reference file");
-
-    ok(!run(app(['openssl', 'ecparam', '-in', $named, '-noout',
-                 '-param_enc', 'bogus'])),
-       "an invalid parameter encoding is rejected");
-};
-
-subtest "Check ecparam -inform and -outform handling" => sub {
-    plan tests => 4;
-
-    my $named = data_file('valid', 'secp384r1-named.pem');
-
-    my $der = 'param.der';
-    ok(run(app(['openssl', 'ecparam', '-in', $named, '-outform', 'DER',
-                '-out', $der])),
-       "write DER-encoded parameters");
-    my $pem = 'param-der.pem';
-    ok(run(app(['openssl', 'ecparam', '-inform', 'DER', '-in', $der,
-                '-out', $pem]))
-       && !compare($pem, $named),
-       "parameters survive a PEM -> DER -> PEM roundtrip");
-
-    ok(!run(app(['openssl', 'ecparam', '-in', $der, '-noout'])),
-       "DER input without -inform is rejected as the default is PEM");
-
-    ok(!run(app(['openssl', 'ecparam', '-in', $named, '-outform', 'MSBLOB',
-                 '-out', 'param.tmp'])),
-       "-outform is limited to PEM and DER");
-};
-
-subtest "Check ecparam -conv_form selects the generator point encoding" => sub {
-    plan tests => 5;
-
-    my $named = data_file('valid', 'secp384r1-named.pem');
-    my $explicit = data_file('valid', 'secp384r1-explicit.pem');
-
-    # Only explicit parameters encode the generator point; the reference file
-    # uses the default uncompressed form.
-    my $comp = 'param-comp.pem';
-    ok(run(app(['openssl', 'ecparam', '-in', $explicit, '-conv_form',
-                'compressed', '-out', $comp])),
-       "write explicit parameters with a compressed generator");
-    ok((-s $comp) < (-s $explicit),
-       "compressed generator encoding is smaller than uncompressed");
-
-    my $back = 'param-unc.pem';
-    ok(run(app(['openssl', 'ecparam', '-in', $comp, '-conv_form',
-                'uncompressed', '-out', $back]))
-       && !compare($back, $explicit),
-       "converting back to uncompressed matches the reference file");
-
-    my $namedout = 'param-named-conv.pem';
-    ok(run(app(['openssl', 'ecparam', '-in', $named, '-conv_form',
-                'compressed', '-out', $namedout]))
-       && !compare($namedout, $named),
-       "-conv_form does not change named curve parameters");
-
-    ok(!run(app(['openssl', 'ecparam', '-in', $named, '-noout',
-                 '-conv_form', 'bogus'])),
-       "an invalid conversion form is rejected");
-};
-
-subtest "Check ecparam -text prints the parameters in text form" => sub {
-    plan tests => 6;
-
-    my $named = data_file('valid', 'secp384r1-named.pem');
-    my $explicit = data_file('valid', 'secp384r1-explicit.pem');
-
-    # Named parameters print the curve identification.
-    my @named = run(app(['openssl', 'ecparam', '-text', '-noout', '-in', $named],
-                        stderr => undef),
-                    capture => 1);
-    chomp @named;
-    ok(grep(/^EC-Parameters: \(384 bit field, 192 bit security level\)$/, @named),
-       "named parameters print the EC-Parameters header");
-    ok(grep(/^ASN1 OID: secp384r1$/, @named),
-       "named parameters print the expected curve OID");
-    ok(grep(/^NIST CURVE: P-384$/, @named),
-       "named parameters print the expected NIST curve name");
-
-    # Explicit parameters print the field parameters instead of the curve name.
-    my @explicit = run(app(['openssl', 'ecparam', '-text', '-noout',
-                            '-in', $explicit],
-                           stderr => undef),
-                       capture => 1);
-    chomp @explicit;
-    ok(grep(/^EC-Parameters: \(384 bit field, 192 bit security level\)$/, @explicit),
-       "explicit parameters print the EC-Parameters header");
-    ok(grep(/^Field Type: prime-field$/, @explicit)
-       && grep(/^Cofactor:/, @explicit),
-       "explicit parameters print the field parameters");
-    ok(!grep(/^ASN1 OID:/, @explicit),
-       "explicit parameters do not print a curve OID");
-};
+ok(run(test(["ecparam_test", "noncanon", @noncanon])),
+   "Load and check non-canonically encoded parameters");

-ok(run(app(['openssl', 'ecparam', '-list_curves'])), "Test -list_curves");
+ok(run(test(["ecparam_test", "invalid", @invalid])),
+   "Reject invalid parameters");
diff --git a/test/recipes/20-test_app_ecparam.t b/test/recipes/20-test_app_ecparam.t
new file mode 100644
index 0000000000..1c03efff1d
--- /dev/null
+++ b/test/recipes/20-test_app_ecparam.t
@@ -0,0 +1,251 @@
+#! /usr/bin/env perl
+# Copyright 2017-2026 The OpenSSL Project Authors. All Rights Reserved.
+#
+# Licensed under the Apache License 2.0 (the "License").  You may not use
+# this file except in compliance with the License.  You can obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+
+use strict;
+use warnings;
+
+use File::Copy;
+use File::Compare qw/compare_text compare/;
+use OpenSSL::Test qw/:DEFAULT srctop_file/;
+use OpenSSL::Test::Utils;
+
+setup("test_app_ecparam");
+
+plan skip_all => "EC or EC2M isn't supported in this build"
+    if disabled("ec") || disabled("ec2m");
+
+# The parameter corpus belongs to 15-test_ecparam.t, so it has to be named
+# by path rather than through data_file(), which resolves against the data
+# directory of the recipe that calls it.
+sub param_file {
+    return srctop_file("test", "recipes", "15-test_ecparam_data", @_);
+}
+
+# The parameter corpus is swept in process by 15-test_ecparam.t.  What is
+# tested here is the applications: their options, and that they write what
+# they read.  A representative curve is enough for that; running the whole
+# corpus through them buys process startup rather than coverage.
+my $named = param_file('valid', 'secp384r1-named.pem');
+my $explicit = param_file('valid', 'secp384r1-explicit.pem');
+my $prime = param_file('valid', 'prime256v1-named.pem');
+
+my $no_fips = disabled('fips') || ($ENV{NO_FIPS} // 0);
+
+plan tests => 8;
+
+sub checkcompare {
+    my $files = shift; # List of files
+    my $app = shift;   # Which application
+
+    foreach (@$files) {
+        my $testout = "$app.tst";
+
+        ok(run(app(['openssl', $app, '-out', $testout, '-in', $_])));
+        ok(!compare_text($_, $testout, sub {
+            my $in1 = $_[0];
+            my $in2 = $_[1];
+            $in1 =~ s/\r\n/\n/g;
+            $in2 =~ s/\r\n/\n/g;
+            $in1 ne $in2}), "Original file $_ is the same as new one");
+    }
+}
+
+sub check_identical {
+    my $apps = shift; # List of applications
+
+    foreach (@$apps) {
+        my $inout = "$_.tst";
+        my $backup = "backup.tst";
+
+        copy($inout, $backup);
+        ok(run(app(['openssl', $_, '-in', $inout, '-out', $inout])));
+        ok(!compare($inout, $backup), "converted file $inout did not change");
+    }
+}
+
+my @representative = ($named, $explicit, $prime);
+
+subtest "Check ecparam does not change the parameter file on output" => sub {
+    plan tests => 2 * scalar(@representative);
+    checkcompare(\@representative, "ecparam");
+};
+
+subtest "Check pkeyparam does not change the parameter file on output" => sub {
+    plan tests => 2 * scalar(@representative);
+    checkcompare(\@representative, "pkeyparam");
+};
+
+my @apps = ("ecparam", "pkeyparam");
+subtest "Check param apps do not garble infile identical to outfile" => sub {
+    plan tests => 2 * scalar(@apps);
+    check_identical(\@apps);
+};
+
+subtest "Check loading of fips and non-fips params" => sub {
+    plan skip_all => "FIPS is disabled"
+        if $no_fips;
+    plan tests => 8;
+
+    my $fipsconf = srctop_file("test", "fips-and-base.cnf");
+    my $defaultconf = srctop_file("test", "default.cnf");
+
+    $ENV{OPENSSL_CONF} = $fipsconf;
+
+    ok(run(app(['openssl', 'ecparam',
+                '-in', param_file('valid', 'secp384r1-explicit.pem'),
+                '-check'])),
+       "Loading explicitly encoded valid curve");
+
+    ok(run(app(['openssl', 'ecparam',
+                '-in', param_file('valid', 'secp384r1-named.pem'),
+                '-check'])),
+       "Loading named valid curve");
+
+    ok(!run(app(['openssl', 'ecparam',
+                '-in', param_file('valid', 'secp112r1-named.pem'),
+                '-check'])),
+       "Fail loading named non-fips curve");
+
+    ok(!run(app(['openssl', 'pkeyparam',
+                '-in', param_file('valid', 'secp112r1-named.pem'),
+                '-check'])),
+       "Fail loading named non-fips curve using pkeyparam");
+
+    ok(run(app(['openssl', 'ecparam',
+                '-provider', 'default',
+                '-propquery', '?fips!=yes',
+                '-in', param_file('valid', 'secp112r1-named.pem'),
+                '-check'])),
+       "Loading named non-fips curve in FIPS mode with non-FIPS property".
+       " query");
+
+    ok(run(app(['openssl', 'pkeyparam',
+                '-provider', 'default',
+                '-propquery', '?fips!=yes',
+                '-in', param_file('valid', 'secp112r1-named.pem'),
+                '-check'])),
+       "Loading named non-fips curve in FIPS mode with non-FIPS property".
+       " query using pkeyparam");
+
+    ok(!run(app(['openssl', 'ecparam',
+                '-genkey', '-name', 'secp112r1'])),
+       "Fail generating key for named non-fips curve");
+
+    ok(run(app(['openssl', 'ecparam',
+                '-provider', 'default',
+                '-propquery', '?fips!=yes',
+                '-genkey', '-name', 'secp112r1'])),
+       "Generating key for named non-fips curve with non-FIPS property query");
+
+    $ENV{OPENSSL_CONF} = $defaultconf;
+};
+
+subtest "Check ecparam -param_enc converts between named and explicit" => sub {
+    plan tests => 3;
+
+    # The encodings are canonical, so re-encoding a named curve as explicit
+    # (and vice versa) must reproduce the matching reference file byte for byte.
+    my $to_explicit = 'param-explicit.tst';
+    ok(run(app(['openssl', 'ecparam', '-in', $named, '-param_enc', 'explicit',
+                '-out', $to_explicit]))
+       && !compare($to_explicit, $explicit),
+       "named_curve params re-encoded as explicit match the reference file");
+
+    my $to_named = 'param-named.tst';
+    ok(run(app(['openssl', 'ecparam', '-in', $explicit, '-param_enc',
+                'named_curve', '-out', $to_named]))
+       && !compare($to_named, $named),
+       "explicit params re-encoded as named_curve match the reference file");
+
+    ok(!run(app(['openssl', 'ecparam', '-in', $named, '-noout',
+                 '-param_enc', 'bogus'])),
+       "an invalid parameter encoding is rejected");
+};
+
+subtest "Check ecparam -inform and -outform handling" => sub {
+    plan tests => 4;
+
+    my $der = 'param.der';
+    ok(run(app(['openssl', 'ecparam', '-in', $named, '-outform', 'DER',
+                '-out', $der])),
+       "write DER-encoded parameters");
+    my $pem = 'param-der.pem';
+    ok(run(app(['openssl', 'ecparam', '-inform', 'DER', '-in', $der,
+                '-out', $pem]))
+       && !compare($pem, $named),
+       "parameters survive a PEM -> DER -> PEM roundtrip");
+
+    ok(!run(app(['openssl', 'ecparam', '-in', $der, '-noout'])),
+       "DER input without -inform is rejected as the default is PEM");
+
+    ok(!run(app(['openssl', 'ecparam', '-in', $named, '-outform', 'MSBLOB',
+                 '-out', 'param.tmp'])),
+       "-outform is limited to PEM and DER");
+};
+
+subtest "Check ecparam -conv_form selects the generator point encoding" => sub {
+    plan tests => 5;
+
+    # Only explicit parameters encode the generator point; the reference file
+    # uses the default uncompressed form.
+    my $comp = 'param-comp.pem';
+    ok(run(app(['openssl', 'ecparam', '-in', $explicit, '-conv_form',
+                'compressed', '-out', $comp])),
+       "write explicit parameters with a compressed generator");
+    ok((-s $comp) < (-s $explicit),
+       "compressed generator encoding is smaller than uncompressed");
+
+    my $back = 'param-unc.pem';
+    ok(run(app(['openssl', 'ecparam', '-in', $comp, '-conv_form',
+                'uncompressed', '-out', $back]))
+       && !compare($back, $explicit),
+       "converting back to uncompressed matches the reference file");
+
+    my $namedout = 'param-named-conv.pem';
+    ok(run(app(['openssl', 'ecparam', '-in', $named, '-conv_form',
+                'compressed', '-out', $namedout]))
+       && !compare($namedout, $named),
+       "-conv_form does not change named curve parameters");
+
+    ok(!run(app(['openssl', 'ecparam', '-in', $named, '-noout',
+                 '-conv_form', 'bogus'])),
+       "an invalid conversion form is rejected");
+};
+
+subtest "Check ecparam -text and -list_curves" => sub {
+    plan tests => 7;
+
+    # Named parameters print the curve identification.
+    my @named = run(app(['openssl', 'ecparam', '-text', '-noout', '-in', $named],
+                        stderr => undef),
+                    capture => 1);
+    chomp @named;
+    ok(grep(/^EC-Parameters: \(384 bit field, 192 bit security level\)$/, @named),
+       "named parameters print the EC-Parameters header");
+    ok(grep(/^ASN1 OID: secp384r1$/, @named),
+       "named parameters print the expected curve OID");
+    ok(grep(/^NIST CURVE: P-384$/, @named),
+       "named parameters print the expected NIST curve name");
+
+    # Explicit parameters print the field parameters instead of the curve name.
+    my @explicit = run(app(['openssl', 'ecparam', '-text', '-noout',
+                            '-in', $explicit],
+                           stderr => undef),
+                       capture => 1);
+    chomp @explicit;
+    ok(grep(/^EC-Parameters: \(384 bit field, 192 bit security level\)$/, @explicit),
+       "explicit parameters print the EC-Parameters header");
+    ok(grep(/^Field Type: prime-field$/, @explicit)
+       && grep(/^Cofactor:/, @explicit),
+       "explicit parameters print the field parameters");
+    ok(!grep(/^ASN1 OID:/, @explicit),
+       "explicit parameters do not print a curve OID");
+
+    ok(run(app(['openssl', 'ecparam', '-list_curves'])), "Test -list_curves");
+};