From 233e2c016cd9f4b1dc052bae5c9290e958a8e811 Mon Sep 17 00:00:00 2001 From: Colton Willey Date: Mon, 5 Jan 2026 08:51:58 -0800 Subject: [PATCH 1/2] Add text encoder for ECC --- include/wolfprovider/alg_funcs.h | 1 + src/wp_ecc_kmgmt.c | 287 +++++++++++++++++++++++++++++++ src/wp_wolfprov.c | 3 + test/test_ecc.c | 89 ++++++++++ test/unit.c | 1 + test/unit.h | 3 + 6 files changed, 384 insertions(+) diff --git a/include/wolfprovider/alg_funcs.h b/include/wolfprovider/alg_funcs.h index b9271fef..f3f9ab91 100644 --- a/include/wolfprovider/alg_funcs.h +++ b/include/wolfprovider/alg_funcs.h @@ -408,6 +408,7 @@ extern const OSSL_DISPATCH wp_ecc_epki_der_encoder_functions[]; extern const OSSL_DISPATCH wp_ecc_epki_pem_encoder_functions[]; extern const OSSL_DISPATCH wp_ecc_x9_62_der_encoder_functions[]; extern const OSSL_DISPATCH wp_ecc_x9_62_pem_encoder_functions[]; +extern const OSSL_DISPATCH wp_ecc_text_encoder_functions[]; extern const OSSL_DISPATCH wp_x25519_spki_der_encoder_functions[]; extern const OSSL_DISPATCH wp_x25519_spki_pem_encoder_functions[]; extern const OSSL_DISPATCH wp_x25519_pki_der_encoder_functions[]; diff --git a/src/wp_ecc_kmgmt.c b/src/wp_ecc_kmgmt.c index ca616222..42c73e95 100644 --- a/src/wp_ecc_kmgmt.c +++ b/src/wp_ecc_kmgmt.c @@ -2934,6 +2934,7 @@ static int wp_ecc_encode(wp_EccEncDecCtx* ctx, OSSL_CORE_BIO *cBio, OPENSSL_free(pemData); } OPENSSL_free(cipherInfo); + BIO_free(out); WOLFPROV_LEAVE(WP_LOG_COMP_ECC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), ok); return ok; } @@ -3478,4 +3479,290 @@ const OSSL_DISPATCH wp_ecc_x9_62_pem_encoder_functions[] = { { 0, NULL } }; +/* + * ECC Text Encoder + */ + +/** + * Create a new ECC text encoder context. + * + * @param [in] provCtx Provider context. + * @return New ECC encoder/decoder context object on success. + * @return NULL on failure. + */ +static wp_EccEncDecCtx* wp_ecc_text_enc_new(WOLFPROV_CTX* provCtx) +{ + return wp_ecc_enc_dec_new(provCtx, WP_ENC_FORMAT_TEXT, WP_FORMAT_TEXT); +} + +/** + * Return whether the ECC text encoder handles this part of the key. + * + * @param [in] provCtx Provider context. Unused. + * @param [in] selection Parts of key to handle. + * @return 1 when supported. + * @return 0 when not supported. + */ +static int wp_ecc_text_enc_does_selection(WOLFPROV_CTX* provCtx, int selection) +{ + int ok; + + (void)provCtx; + + /* Text encoder supports both public and private key parts */ + if (selection == 0) { + ok = 1; + } + else { + ok = ((selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0) || + ((selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0); + } + + return ok; +} + +/** + * Get the NIST curve name for the given curve ID. + * + * @param [in] curveId wolfSSL curve identifier. + * @return NIST curve name string on success. + * @return NULL if not a NIST curve. + */ +static const char* wp_ecc_get_nist_curve_name(int curveId) +{ + const char* name = NULL; + + switch (curveId) { + case ECC_SECP192R1: + name = "P-192"; + break; + case ECC_SECP224R1: + name = "P-224"; + break; + case ECC_SECP256R1: + name = "P-256"; + break; + case ECC_SECP384R1: + name = "P-384"; + break; + case ECC_SECP521R1: + name = "P-521"; + break; + default: + break; + } + + return name; +} + +/** Number of bytes per line when printing labeled hex data (matches OpenSSL) */ +#define WP_ECC_TEXT_PRINT_WIDTH 15 + +/** + * Print a labeled buffer in hex format matching OpenSSL's output. + * + * Format: + * label: + * xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx:xx: + * xx:xx:xx:xx:xx + * + * @param [in] out BIO to write output to. + * @param [in] label Label string to print before the hex data. + * @param [in] buf Buffer containing data to print. + * @param [in] bufLen Length of buffer in bytes. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_ecc_encode_text_labeled_buf(BIO* out, const char* label, + const unsigned char* buf, size_t bufLen) +{ + int ok = 1; + size_t i; + + if (BIO_printf(out, "%s\n", label) <= 0) { + ok = 0; + } + + for (i = 0; ok && (i < bufLen); i++) { + if ((i % WP_ECC_TEXT_PRINT_WIDTH) == 0) { + if (i > 0 && BIO_printf(out, "\n") <= 0) { + ok = 0; + break; + } + if (BIO_printf(out, " ") <= 0) { + ok = 0; + break; + } + } + + if (BIO_printf(out, "%02x%s", buf[i], + (i == bufLen - 1) ? "" : ":") <= 0) { + ok = 0; + break; + } + } + + if (ok && BIO_printf(out, "\n") <= 0) { + ok = 0; + } + + return ok; +} + +/** + * Encode the ECC key in text format. + * + * @param [in] ctx ECC encoder/decoder context object. + * @param [in, out] cBio Core BIO to write data to. + * @param [in] key ECC key object. + * @param [in] params Key parameters. Unused. + * @param [in] selection Parts of key to encode. + * @param [in] pwCb Password callback. Unused. + * @param [in] pwCbArg Argument to pass to password callback. Unused. + * @return 1 on success. + * @return 0 on failure. + */ +static int wp_ecc_encode_text(wp_EccEncDecCtx* ctx, OSSL_CORE_BIO* cBio, + const wp_Ecc* key, const OSSL_PARAM* params, int selection, + OSSL_PASSPHRASE_CALLBACK* pwCb, void* pwCbArg) +{ + int ok = 1; + BIO* out = wp_corebio_get_bio(ctx->provCtx, cBio); + int hasPriv = (selection & OSSL_KEYMGMT_SELECT_PRIVATE_KEY) != 0; + int hasPub = (selection & OSSL_KEYMGMT_SELECT_PUBLIC_KEY) != 0; + unsigned char* privBuf = NULL; + word32 privLen = 0; + unsigned char* pubBuf = NULL; + word32 pubLen = 0; + const char* curveName = NULL; + const char* nistName = NULL; + int rc; + + WOLFPROV_ENTER(WP_LOG_COMP_ECC, "wp_ecc_encode_text"); + + (void)params; + (void)pwCb; + (void)pwCbArg; + + if (!wolfssl_prov_is_running()) { + ok = 0; + } + + if (ok && (out == NULL)) { + ok = 0; + } + + /* Print header line */ + if (ok) { + if (hasPriv) { + if (BIO_printf(out, "Private-Key: (%d bit)\n", key->bits) <= 0) { + ok = 0; + } + } + else if (hasPub) { + if (BIO_printf(out, "Public-Key: (%d bit)\n", key->bits) <= 0) { + ok = 0; + } + } + } + + /* Export and print private key if present */ + if (ok && hasPriv && key->hasPriv) { + mp_int* priv; +#if (!defined(HAVE_FIPS) || FIPS_VERSION_GE(5,3)) && \ + LIBWOLFSSL_VERSION_HEX >= 0x05006002 + priv = wc_ecc_key_get_priv((ecc_key*)&key->key); +#else + priv = (mp_int*)&key->key.k; +#endif + privLen = (word32)mp_unsigned_bin_size(priv); + if (privLen > 0) { + privBuf = (unsigned char*)OPENSSL_malloc(privLen); + if (privBuf == NULL) { + ok = 0; + } + else if (mp_to_unsigned_bin(priv, privBuf) != MP_OKAY) { + ok = 0; + } + else { + ok = wp_ecc_encode_text_labeled_buf(out, "priv:", privBuf, + privLen); + } + } + } + + /* Export and print public key if present */ + if (ok && (hasPriv || hasPub) && key->hasPub) { + /* Get size first */ + PRIVATE_KEY_UNLOCK(); + rc = wc_ecc_export_x963_ex((ecc_key*)&key->key, NULL, &pubLen, 0); + PRIVATE_KEY_LOCK(); + if (rc != LENGTH_ONLY_E) { + ok = 0; + } + else { + pubBuf = (unsigned char*)OPENSSL_malloc(pubLen); + if (pubBuf == NULL) { + ok = 0; + } + else { + PRIVATE_KEY_UNLOCK(); + rc = wc_ecc_export_x963_ex((ecc_key*)&key->key, pubBuf, + &pubLen, 0); + PRIVATE_KEY_LOCK(); + if (rc != 0) { + ok = 0; + } + else { + ok = wp_ecc_encode_text_labeled_buf(out, "pub:", pubBuf, + pubLen); + } + } + } + } + + /* Print ASN1 OID (curve short name) */ + if (ok) { + curveName = wp_ecc_get_group_name((wp_Ecc*)key); + if (curveName != NULL) { + if (BIO_printf(out, "ASN1 OID: %s\n", curveName) <= 0) { + ok = 0; + } + } + } + + /* Print NIST CURVE name if applicable */ + if (ok) { + nistName = wp_ecc_get_nist_curve_name(key->curveId); + if (nistName != NULL) { + if (BIO_printf(out, "NIST CURVE: %s\n", nistName) <= 0) { + ok = 0; + } + } + } + + /* Clean up */ + if (privBuf != NULL) { + OPENSSL_clear_free(privBuf, privLen); + } + OPENSSL_free(pubBuf); + BIO_free(out); + + WOLFPROV_LEAVE(WP_LOG_COMP_ECC, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + ok); + return ok; +} + +/** + * Dispatch table for ECC text encoder. + */ +const OSSL_DISPATCH wp_ecc_text_encoder_functions[] = { + { OSSL_FUNC_ENCODER_NEWCTX, (DFUNC)wp_ecc_text_enc_new }, + { OSSL_FUNC_ENCODER_FREECTX, (DFUNC)wp_ecc_enc_dec_free }, + { OSSL_FUNC_ENCODER_DOES_SELECTION, + (DFUNC)wp_ecc_text_enc_does_selection }, + { OSSL_FUNC_ENCODER_ENCODE, (DFUNC)wp_ecc_encode_text }, + { 0, NULL } +}; + #endif /* WP_HAVE_ECC */ diff --git a/src/wp_wolfprov.c b/src/wp_wolfprov.c index daab28bf..b3f6520a 100644 --- a/src/wp_wolfprov.c +++ b/src/wp_wolfprov.c @@ -828,6 +828,9 @@ static const OSSL_ALGORITHM wolfprov_encoder[] = { { WP_NAMES_EC, WP_ENCODER_PROPERTIES(X9_62, pem), wp_ecc_x9_62_pem_encoder_functions, "" }, + { WP_NAMES_EC, WP_ENCODER_PROPERTIES(type-specific, text), + wp_ecc_text_encoder_functions, + "" }, #endif #ifdef WP_HAVE_X25519 diff --git a/test/test_ecc.c b/test/test_ecc.c index f2254769..08a9de74 100644 --- a/test/test_ecc.c +++ b/test/test_ecc.c @@ -2206,4 +2206,93 @@ int test_ec_auto_derive_pubkey(void* data) return err; } +#ifdef WP_HAVE_EC_P256 +int test_ec_print_public(void* data) +{ + int err = 0; + EVP_PKEY *osslPkey = NULL; + EVP_PKEY *wpPkey = NULL; + BIO *osslBio = NULL; + BIO *wpBio = NULL; + const unsigned char *p; + char *osslBuf = NULL; + char *wpBuf = NULL; + long osslLen = 0; + long wpLen = 0; + + (void)data; + + /* First, load key and print with OpenSSL default provider */ + PRINT_MSG("Load ECC P-256 key with OpenSSL default provider"); + p = ecc_key_der_256; + osslPkey = d2i_PrivateKey_ex(EVP_PKEY_EC, NULL, &p, sizeof(ecc_key_der_256), + osslLibCtx, NULL); + err = osslPkey == NULL; + if (err == 0) { + PRINT_MSG("Create BIO for OpenSSL output"); + err = (osslBio = BIO_new(BIO_s_mem())) == NULL; + } + if (err == 0) { + PRINT_MSG("Print public key with OpenSSL default provider"); + err = EVP_PKEY_print_public(osslBio, osslPkey, 0, NULL) != 1; + if (err != 0) { + PRINT_ERR_MSG("EVP_PKEY_print_public failed for OpenSSL key"); + } + } + if (err == 0) { + osslLen = BIO_get_mem_data(osslBio, &osslBuf); + err = osslLen <= 0; + } + + /* Now load key and print with wolfProvider */ + if (err == 0) { + PRINT_MSG("Load ECC P-256 key with wolfProvider"); + p = ecc_key_der_256; + wpPkey = d2i_PrivateKey_ex(EVP_PKEY_EC, NULL, &p, + sizeof(ecc_key_der_256), wpLibCtx, NULL); + err = wpPkey == NULL; + } + if (err == 0) { + PRINT_MSG("Create BIO for wolfProvider output"); + err = (wpBio = BIO_new(BIO_s_mem())) == NULL; + } + if (err == 0) { + PRINT_MSG("Print public key with wolfProvider"); + err = EVP_PKEY_print_public(wpBio, wpPkey, 0, NULL) != 1; + if (err != 0) { + PRINT_ERR_MSG("EVP_PKEY_print_public failed for wolfProvider key"); + } + } + if (err == 0) { + wpLen = BIO_get_mem_data(wpBio, &wpBuf); + err = wpLen <= 0; + } + + /* Compare outputs */ + if (err == 0) { + PRINT_MSG("Compare OpenSSL and wolfProvider outputs"); + if (osslLen != wpLen) { + PRINT_ERR_MSG("Output lengths differ: OpenSSL=%ld, wolfProvider=%ld", + osslLen, wpLen); + PRINT_BUFFER("OpenSSL output", (unsigned char*)osslBuf, osslLen); + PRINT_BUFFER("wolfProvider output", (unsigned char*)wpBuf, wpLen); + err = 1; + } + else if (memcmp(osslBuf, wpBuf, osslLen) != 0) { + PRINT_ERR_MSG("Output contents differ"); + PRINT_BUFFER("OpenSSL output", (unsigned char*)osslBuf, osslLen); + PRINT_BUFFER("wolfProvider output", (unsigned char*)wpBuf, wpLen); + err = 1; + } + } + + BIO_free(wpBio); + BIO_free(osslBio); + EVP_PKEY_free(wpPkey); + EVP_PKEY_free(osslPkey); + + return err; +} +#endif /* WP_HAVE_EC_P256 */ + #endif /* WP_HAVE_ECC */ diff --git a/test/unit.c b/test/unit.c index a42a4b42..bac908ac 100644 --- a/test/unit.c +++ b/test/unit.c @@ -357,6 +357,7 @@ TEST_CASE test_case[] = { TEST_DECL(test_ec_import, NULL), TEST_DECL(test_ec_auto_derive_pubkey, NULL), TEST_DECL(test_ec_null_init, NULL), + TEST_DECL(test_ec_print_public, NULL), #endif #ifdef WP_HAVE_EC_P384 #ifdef WP_HAVE_ECKEYGEN diff --git a/test/unit.h b/test/unit.h index b0fdadfd..20f66af4 100644 --- a/test/unit.h +++ b/test/unit.h @@ -398,6 +398,9 @@ int test_ec_decode(void* data); int test_ec_import(void* data); int test_ec_auto_derive_pubkey(void* data); int test_ec_null_init(void* data); +#ifdef WP_HAVE_EC_P256 +int test_ec_print_public(void* data); +#endif #endif /* WP_HAVE_ECC */ From c74d729176f532b17e28306474a0d10d0da7257f Mon Sep 17 00:00:00 2001 From: Colton Willey Date: Mon, 5 Jan 2026 09:41:17 -0800 Subject: [PATCH 2/2] Use whitespace insensitive check for public key print --- src/wp_ecc_kmgmt.c | 2 +- test/test_ecc.c | 55 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/src/wp_ecc_kmgmt.c b/src/wp_ecc_kmgmt.c index 42c73e95..c7ddf5d6 100644 --- a/src/wp_ecc_kmgmt.c +++ b/src/wp_ecc_kmgmt.c @@ -3556,7 +3556,7 @@ static const char* wp_ecc_get_nist_curve_name(int curveId) } /** Number of bytes per line when printing labeled hex data (matches OpenSSL) */ -#define WP_ECC_TEXT_PRINT_WIDTH 15 +#define WP_ECC_TEXT_PRINT_WIDTH 16 /** * Print a labeled buffer in hex format matching OpenSSL's output. diff --git a/test/test_ecc.c b/test/test_ecc.c index 08a9de74..85850053 100644 --- a/test/test_ecc.c +++ b/test/test_ecc.c @@ -2206,6 +2206,48 @@ int test_ec_auto_derive_pubkey(void* data) return err; } +/* Helper function to compare strings ignoring whitespace */ +static int strcmp_ignore_whitespace(const char *s1, long len1, + const char *s2, long len2) +{ + long i1 = 0, i2 = 0; + + while (i1 < len1 && i2 < len2) { + /* Skip whitespace in s1 */ + while (i1 < len1 && (s1[i1] == ' ' || s1[i1] == '\n' || + s1[i1] == '\t' || s1[i1] == '\r')) { + i1++; + } + /* Skip whitespace in s2 */ + while (i2 < len2 && (s2[i2] == ' ' || s2[i2] == '\n' || + s2[i2] == '\t' || s2[i2] == '\r')) { + i2++; + } + /* Compare non-whitespace characters */ + if (i1 < len1 && i2 < len2) { + if (s1[i1] != s2[i2]) { + return 1; /* Different */ + } + i1++; + i2++; + } + } + /* Skip trailing whitespace */ + while (i1 < len1 && (s1[i1] == ' ' || s1[i1] == '\n' || + s1[i1] == '\t' || s1[i1] == '\r')) { + i1++; + } + while (i2 < len2 && (s2[i2] == ' ' || s2[i2] == '\n' || + s2[i2] == '\t' || s2[i2] == '\r')) { + i2++; + } + /* Both should be at end */ + if (i1 != len1 || i2 != len2) { + return 1; /* Different */ + } + return 0; /* Same */ +} + #ifdef WP_HAVE_EC_P256 int test_ec_print_public(void* data) { @@ -2268,18 +2310,11 @@ int test_ec_print_public(void* data) err = wpLen <= 0; } - /* Compare outputs */ + /* Compare outputs ignoring whitespace differences */ if (err == 0) { PRINT_MSG("Compare OpenSSL and wolfProvider outputs"); - if (osslLen != wpLen) { - PRINT_ERR_MSG("Output lengths differ: OpenSSL=%ld, wolfProvider=%ld", - osslLen, wpLen); - PRINT_BUFFER("OpenSSL output", (unsigned char*)osslBuf, osslLen); - PRINT_BUFFER("wolfProvider output", (unsigned char*)wpBuf, wpLen); - err = 1; - } - else if (memcmp(osslBuf, wpBuf, osslLen) != 0) { - PRINT_ERR_MSG("Output contents differ"); + if (strcmp_ignore_whitespace(osslBuf, osslLen, wpBuf, wpLen) != 0) { + PRINT_ERR_MSG("Output contents differ (ignoring whitespace)"); PRINT_BUFFER("OpenSSL output", (unsigned char*)osslBuf, osslLen); PRINT_BUFFER("wolfProvider output", (unsigned char*)wpBuf, wpLen); err = 1;