| /* |
| * 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 <listfile> |
| * |
| * where the list file holds one file name per line. The list is passed |
| * this way because the corpus has more files in it than a test can be |
| * given arguments. |
| * |
| * 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 char **files; |
| static int num_files; |
| |
| static const char *corpus_file(int idx) |
| { |
| return files[idx]; |
| } |
| |
| /* Read the corpus file names, one per line. */ |
| static int read_file_list(const char *path) |
| { |
| BIO *bio = BIO_new_file(path, "r"); |
| char line[512]; |
| int n = 0, allocated = 0; |
| |
| if (bio == NULL) |
| return 0; |
| |
| while (BIO_gets(bio, line, sizeof(line)) > 0) { |
| size_t len = strlen(line); |
| |
| while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) |
| line[--len] = '\0'; |
| if (len == 0) |
| continue; |
| |
| if (n == allocated) { |
| char **tmp; |
| |
| allocated = allocated == 0 ? 64 : allocated * 2; |
| tmp = OPENSSL_realloc(files, allocated * sizeof(*files)); |
| if (tmp == NULL) |
| goto err; |
| files = tmp; |
| } |
| |
| if ((files[n] = OPENSSL_strdup(line)) == NULL) |
| goto err; |
| n++; |
| } |
| |
| BIO_free(bio); |
| num_files = n; |
| return n > 0; |
| |
| err: |
| num_files = n; |
| BIO_free(bio); |
| return 0; |
| } |
| |
| /* |
| * 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; |
| |
| if (!TEST_size_t_eq(test_get_argument_count(), 2)) { |
| TEST_error("usage: ecparam_test valid|noncanon|invalid <listfile>"); |
| 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; |
| |
| if (!TEST_true(read_file_list(test_get_argument(1)))) { |
| TEST_error("cannot read corpus list %s", test_get_argument(1)); |
| return 0; |
| } |
| |
| 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; |
| } |
| |
| void cleanup_tests(void) |
| { |
| int i; |
| |
| for (i = 0; i < num_files; i++) |
| OPENSSL_free(files[i]); |
| OPENSSL_free(files); |
| } |