From af30f20bd7691996db2d251bf16f27b4e4652d1e Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Fri, 28 Aug 2026 12:45:54 +0300 Subject: [PATCH 1/3] test: add short-write BIO regression for key encoders The key encoders write DER/PEM output with a single BIO_write and treat any positive return as success, so a short write silently truncates the encoding. Add a deterministic test that encodes an EC public key through a BIO that accepts one byte per write and confirms no bytes are lost. Fenrir 11560. --- test/test_ecc.c | 133 ++++++++++++++++++++++++++++++++++++++++++++++++ test/unit.c | 1 + test/unit.h | 1 + 3 files changed, 135 insertions(+) diff --git a/test/test_ecc.c b/test/test_ecc.c index 06337585..c3cd898a 100644 --- a/test/test_ecc.c +++ b/test/test_ecc.c @@ -1045,6 +1045,139 @@ int test_ecc_encode_epki(void *data) } #endif /* WP_HAVE_EPKI_TEST */ +/* Sink BIO that accepts one byte per write and keeps every byte it is given. + * It reproduces a short-writing BIO so a truncated encoding is detectable. */ +typedef struct { + unsigned char buf[4096]; + size_t len; +} ShortWriteSink; + +static int short_write_bio_write(BIO* b, const char* data, int len) +{ + ShortWriteSink* sink = (ShortWriteSink*)BIO_get_data(b); + + BIO_clear_retry_flags(b); + if ((sink == NULL) || (len <= 0) || (sink->len >= sizeof(sink->buf))) { + return 0; + } + /* Take a single byte so the writer must loop to make progress. */ + sink->buf[sink->len++] = (unsigned char)data[0]; + return 1; +} + +static long short_write_bio_ctrl(BIO* b, int cmd, long num, void* ptr) +{ + (void)b; + (void)num; + (void)ptr; + return (cmd == BIO_CTRL_FLUSH) ? 1 : 0; +} + +static int short_write_bio_create(BIO* b) +{ + BIO_set_init(b, 1); + return 1; +} + +/* Encode pkey twice with the same wolfProvider encoder: once via + * OSSL_ENCODER_to_data (a normal full-writing sink) for a reference, and once + * through a one-byte-at-a-time BIO. The two encodings must be identical. */ +static int test_ecc_encode_short_write(EVP_PKEY* pkey, int selection, + const char* format, const char* structure) +{ + int err = 0; + OSSL_ENCODER_CTX* ectx = NULL; + unsigned char* refData = NULL; + size_t refLen = 0; + BIO_METHOD* meth = NULL; + BIO* bio = NULL; + ShortWriteSink sink; + + memset(&sink, 0, sizeof(sink)); + + ectx = OSSL_ENCODER_CTX_new_for_pkey(pkey, selection, format, structure, + "provider=libwolfprov"); + err = (ectx == NULL); + if (err == 0) { + err = (OSSL_ENCODER_to_data(ectx, &refData, &refLen) != 1); + } + OSSL_ENCODER_CTX_free(ectx); + ectx = NULL; + if (err == 0) { + err = (refLen == 0); + } + + if (err == 0) { + meth = BIO_meth_new(BIO_get_new_index() | BIO_TYPE_SOURCE_SINK, + "short-write"); + err = (meth == NULL); + } + if (err == 0) { + BIO_meth_set_write(meth, short_write_bio_write); + BIO_meth_set_ctrl(meth, short_write_bio_ctrl); + BIO_meth_set_create(meth, short_write_bio_create); + bio = BIO_new(meth); + err = (bio == NULL); + } + if (err == 0) { + BIO_set_data(bio, &sink); + ectx = OSSL_ENCODER_CTX_new_for_pkey(pkey, selection, format, structure, + "provider=libwolfprov"); + err = (ectx == NULL); + } + if (err == 0) { + err = (OSSL_ENCODER_to_bio(ectx, bio) != 1); + } + if (err == 0) { + err = (sink.len != refLen); + if (err) { + PRINT_ERR_MSG("Short write truncated output: %lu of %lu bytes", + (unsigned long)sink.len, (unsigned long)refLen); + } + } + if (err == 0) { + err = (memcmp(sink.buf, refData, refLen) != 0); + if (err) { + PRINT_ERR_MSG("Short-write output does not match the reference"); + } + } + + OSSL_ENCODER_CTX_free(ectx); + BIO_free(bio); + BIO_meth_free(meth); + OPENSSL_free(refData); + + return err; +} + +int test_ecc_encode_short_write_bio(void *data) +{ + int err = 0; + const unsigned char* p = ecc_key_der_256; + EVP_PKEY* pkey = NULL; + + (void)data; + + pkey = d2i_PrivateKey_ex(EVP_PKEY_EC, NULL, &p, sizeof(ecc_key_der_256), + wpLibCtx, NULL); + err = (pkey == NULL); + + if (err == 0) { + PRINT_MSG("SubjectPublicKeyInfo DER survives a short-writing BIO"); + err = test_ecc_encode_short_write(pkey, EVP_PKEY_PUBLIC_KEY, "DER", + "SubjectPublicKeyInfo"); + } + if (err == 0) { + PRINT_MSG("SubjectPublicKeyInfo PEM survives a short-writing BIO"); + err = test_ecc_encode_short_write(pkey, EVP_PKEY_PUBLIC_KEY, "PEM", + "SubjectPublicKeyInfo"); + } + + EVP_PKEY_free(pkey); + + return err; +} + int test_ecdh_invalid_kdf_strings(void *data) { int err = 0; diff --git a/test/unit.c b/test/unit.c index cfb6e8c1..75a1c343 100644 --- a/test/unit.c +++ b/test/unit.c @@ -424,6 +424,7 @@ TEST_CASE test_case[] = { #endif #endif #ifdef WP_HAVE_EC_P256 + TEST_DECL(test_ecc_encode_short_write_bio, NULL), #ifdef WP_HAVE_EPKI_TEST TEST_DECL(test_ecc_encode_epki, NULL), #endif diff --git a/test/unit.h b/test/unit.h index 5a956043..b1c21589 100644 --- a/test/unit.h +++ b/test/unit.h @@ -470,6 +470,7 @@ int test_ecdh_p224(void *data); #endif /* WP_HAVE_EC_P224 */ #ifdef WP_HAVE_EC_P256 int test_ecdh_invalid_kdf_strings(void *data); +int test_ecc_encode_short_write_bio(void *data); #ifdef WP_HAVE_EPKI_TEST int test_ecc_encode_epki(void *data); #endif From 4d5515e7bdf2ad702feaacdcf0fea9f21dbda431 Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Fri, 28 Aug 2026 12:45:54 +0300 Subject: [PATCH 2/3] fix: write full encoder output on a short BIO_write BIO_write may write fewer bytes than requested. The key encoders checked only for a non-positive return, so a short write truncated the DER/PEM output while still reporting success. Add wp_write_bio, which loops until all bytes are written, and use it in the RSA, ECC, ECX, DH and ML-DSA encoders. Fenrir 11560. --- include/wolfprovider/internal.h | 1 + src/wp_dh_kmgmt.c | 5 +---- src/wp_ecc_kmgmt.c | 5 +---- src/wp_ecx_kmgmt.c | 5 +---- src/wp_internal.c | 40 +++++++++++++++++++++++++++++++++ src/wp_mldsa_kmgmt.c | 5 +---- src/wp_rsa_kmgmt.c | 5 +---- 7 files changed, 46 insertions(+), 20 deletions(-) diff --git a/include/wolfprovider/internal.h b/include/wolfprovider/internal.h index 6cf8cd0d..7e2eba47 100644 --- a/include/wolfprovider/internal.h +++ b/include/wolfprovider/internal.h @@ -266,6 +266,7 @@ int wp_decrypt_key_pkcs8(unsigned char* data, word32* len, int wp_read_der_bio(WOLFPROV_CTX* provCtx, OSSL_CORE_BIO *coreBio, unsigned char** data, word32* len); int wp_read_pem_bio(WOLFPROV_CTX *provctx, OSSL_CORE_BIO *coreBio, unsigned char** data, word32* len); +int wp_write_bio(BIO* bio, const unsigned char* data, size_t len); BIO* wp_corebio_get_bio(WOLFPROV_CTX* provCtx, OSSL_CORE_BIO *coreBio); #ifdef HAVE_FIPS diff --git a/src/wp_dh_kmgmt.c b/src/wp_dh_kmgmt.c index 442f1c40..4093a9c7 100644 --- a/src/wp_dh_kmgmt.c +++ b/src/wp_dh_kmgmt.c @@ -2965,10 +2965,7 @@ static int wp_dh_encode(wp_DhEncDecCtx* ctx, OSSL_CORE_BIO *cBio, } } if (ok) { - rc = BIO_write(out, keyData, (int)keyLen); - if (rc <= 0) { - ok = 0; - } + ok = wp_write_bio(out, keyData, keyLen); } if (private) { diff --git a/src/wp_ecc_kmgmt.c b/src/wp_ecc_kmgmt.c index f691060d..cfad9434 100644 --- a/src/wp_ecc_kmgmt.c +++ b/src/wp_ecc_kmgmt.c @@ -3090,10 +3090,7 @@ static int wp_ecc_encode(wp_EccEncDecCtx* ctx, OSSL_CORE_BIO *cBio, } } if (ok) { - rc = BIO_write(out, keyData, (int)keyLen); - if (rc <= 0) { - ok = 0; - } + ok = wp_write_bio(out, keyData, keyLen); } if (private) { diff --git a/src/wp_ecx_kmgmt.c b/src/wp_ecx_kmgmt.c index f1993af1..ec668be8 100644 --- a/src/wp_ecx_kmgmt.c +++ b/src/wp_ecx_kmgmt.c @@ -2314,10 +2314,7 @@ static int wp_ecx_encode(wp_EcxEncDecCtx* ctx, OSSL_CORE_BIO *cBio, } } if (ok) { - rc = BIO_write(out, keyData, (int)keyLen); - if (rc <= 0) { - ok = 0; - } + ok = wp_write_bio(out, keyData, keyLen); } /* derData holds the plaintext private key material. */ diff --git a/src/wp_internal.c b/src/wp_internal.c index 3aa0242e..ec6116f2 100644 --- a/src/wp_internal.c +++ b/src/wp_internal.c @@ -1447,6 +1447,46 @@ int wp_read_pem_bio(WOLFPROV_CTX *provctx, OSSL_CORE_BIO *coreBio, return ok; } +/** + * Write all data to a BIO. + * + * BIO_write may write fewer bytes than requested. Loop until all data is + * written so that a short write does not silently truncate the output. + * + * @param [in] bio BIO to write to. + * @param [in] data Data to write. + * @param [in] len Length of data in bytes. + * @return 1 on success. + * @return 0 on failure. + */ +int wp_write_bio(BIO* bio, const unsigned char* data, size_t len) +{ + int ok = 1; + size_t off = 0; + + WOLFPROV_ENTER(WP_LOG_COMP_PROVIDER, "wp_write_bio"); + + if ((bio == NULL) || (data == NULL)) { + ok = 0; + } + + while (ok && (off < len)) { + int rc = BIO_write(bio, data + off, (int)(len - off)); + if (rc > 0) { + off += (size_t)rc; + } + else { + WOLFPROV_MSG(WP_LOG_COMP_PROVIDER, "BIO_write error (%d) in %s:%d", + rc, __FILE__, __LINE__); + ok = 0; + } + } + + WOLFPROV_LEAVE(WP_LOG_COMP_PROVIDER, __FILE__ ":" WOLFPROV_STRINGIZE(__LINE__), + ok); + return ok; +} + /** * Get the underlying BIO object from the core BIO. * diff --git a/src/wp_mldsa_kmgmt.c b/src/wp_mldsa_kmgmt.c index 36c599fe..76854dad 100644 --- a/src/wp_mldsa_kmgmt.c +++ b/src/wp_mldsa_kmgmt.c @@ -1628,10 +1628,7 @@ static int wp_mldsa_encode(wp_MlDsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, } } if (ok) { - rc = BIO_write(out, keyData, (int)keyLen); - if (rc <= 0) { - ok = 0; - } + ok = wp_write_bio(out, keyData, keyLen); } if (private) { diff --git a/src/wp_rsa_kmgmt.c b/src/wp_rsa_kmgmt.c index a9fe71b1..decab81c 100644 --- a/src/wp_rsa_kmgmt.c +++ b/src/wp_rsa_kmgmt.c @@ -3683,10 +3683,7 @@ static int wp_rsa_encode(wp_RsaEncDecCtx* ctx, OSSL_CORE_BIO* cBio, } } if (ok) { - rc = BIO_write(out, keyData, (int)keyLen); - if (rc <= 0) { - ok = 0; - } + ok = wp_write_bio(out, keyData, keyLen); } if (private) { From e3f98f243a51c437a83a88eef6a19460863cddac Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Mon, 31 Aug 2026 11:39:51 +0300 Subject: [PATCH 3/3] test: gate short-write encoder test on WP_HAVE_EC_P256 only The regression test was registered under WP_HAVE_EC_P256 but defined and declared under WP_HAVE_ECDH && WP_HAVE_EC_P256, so a P-256 build without HAVE_ECC_DHE failed to compile. Move the test, its helper, and the sink BIO out of the WP_HAVE_ECDH blocks so all three sites match. --- test/test_ecc.c | 268 ++++++++++++++++++++++++------------------------ test/unit.h | 5 +- 2 files changed, 139 insertions(+), 134 deletions(-) diff --git a/test/test_ecc.c b/test/test_ecc.c index c3cd898a..a2b6e240 100644 --- a/test/test_ecc.c +++ b/test/test_ecc.c @@ -1045,139 +1045,6 @@ int test_ecc_encode_epki(void *data) } #endif /* WP_HAVE_EPKI_TEST */ -/* Sink BIO that accepts one byte per write and keeps every byte it is given. - * It reproduces a short-writing BIO so a truncated encoding is detectable. */ -typedef struct { - unsigned char buf[4096]; - size_t len; -} ShortWriteSink; - -static int short_write_bio_write(BIO* b, const char* data, int len) -{ - ShortWriteSink* sink = (ShortWriteSink*)BIO_get_data(b); - - BIO_clear_retry_flags(b); - if ((sink == NULL) || (len <= 0) || (sink->len >= sizeof(sink->buf))) { - return 0; - } - /* Take a single byte so the writer must loop to make progress. */ - sink->buf[sink->len++] = (unsigned char)data[0]; - return 1; -} - -static long short_write_bio_ctrl(BIO* b, int cmd, long num, void* ptr) -{ - (void)b; - (void)num; - (void)ptr; - return (cmd == BIO_CTRL_FLUSH) ? 1 : 0; -} - -static int short_write_bio_create(BIO* b) -{ - BIO_set_init(b, 1); - return 1; -} - -/* Encode pkey twice with the same wolfProvider encoder: once via - * OSSL_ENCODER_to_data (a normal full-writing sink) for a reference, and once - * through a one-byte-at-a-time BIO. The two encodings must be identical. */ -static int test_ecc_encode_short_write(EVP_PKEY* pkey, int selection, - const char* format, const char* structure) -{ - int err = 0; - OSSL_ENCODER_CTX* ectx = NULL; - unsigned char* refData = NULL; - size_t refLen = 0; - BIO_METHOD* meth = NULL; - BIO* bio = NULL; - ShortWriteSink sink; - - memset(&sink, 0, sizeof(sink)); - - ectx = OSSL_ENCODER_CTX_new_for_pkey(pkey, selection, format, structure, - "provider=libwolfprov"); - err = (ectx == NULL); - if (err == 0) { - err = (OSSL_ENCODER_to_data(ectx, &refData, &refLen) != 1); - } - OSSL_ENCODER_CTX_free(ectx); - ectx = NULL; - if (err == 0) { - err = (refLen == 0); - } - - if (err == 0) { - meth = BIO_meth_new(BIO_get_new_index() | BIO_TYPE_SOURCE_SINK, - "short-write"); - err = (meth == NULL); - } - if (err == 0) { - BIO_meth_set_write(meth, short_write_bio_write); - BIO_meth_set_ctrl(meth, short_write_bio_ctrl); - BIO_meth_set_create(meth, short_write_bio_create); - bio = BIO_new(meth); - err = (bio == NULL); - } - if (err == 0) { - BIO_set_data(bio, &sink); - ectx = OSSL_ENCODER_CTX_new_for_pkey(pkey, selection, format, structure, - "provider=libwolfprov"); - err = (ectx == NULL); - } - if (err == 0) { - err = (OSSL_ENCODER_to_bio(ectx, bio) != 1); - } - if (err == 0) { - err = (sink.len != refLen); - if (err) { - PRINT_ERR_MSG("Short write truncated output: %lu of %lu bytes", - (unsigned long)sink.len, (unsigned long)refLen); - } - } - if (err == 0) { - err = (memcmp(sink.buf, refData, refLen) != 0); - if (err) { - PRINT_ERR_MSG("Short-write output does not match the reference"); - } - } - - OSSL_ENCODER_CTX_free(ectx); - BIO_free(bio); - BIO_meth_free(meth); - OPENSSL_free(refData); - - return err; -} - -int test_ecc_encode_short_write_bio(void *data) -{ - int err = 0; - const unsigned char* p = ecc_key_der_256; - EVP_PKEY* pkey = NULL; - - (void)data; - - pkey = d2i_PrivateKey_ex(EVP_PKEY_EC, NULL, &p, sizeof(ecc_key_der_256), - wpLibCtx, NULL); - err = (pkey == NULL); - - if (err == 0) { - PRINT_MSG("SubjectPublicKeyInfo DER survives a short-writing BIO"); - err = test_ecc_encode_short_write(pkey, EVP_PKEY_PUBLIC_KEY, "DER", - "SubjectPublicKeyInfo"); - } - if (err == 0) { - PRINT_MSG("SubjectPublicKeyInfo PEM survives a short-writing BIO"); - err = test_ecc_encode_short_write(pkey, EVP_PKEY_PUBLIC_KEY, "PEM", - "SubjectPublicKeyInfo"); - } - - EVP_PKEY_free(pkey); - - return err; -} - int test_ecdh_invalid_kdf_strings(void *data) { int err = 0; @@ -1633,6 +1500,141 @@ int test_ecdh_x448_vector(void *data) #endif /* WP_HAVE_ECDH */ +#ifdef WP_HAVE_EC_P256 +/* Sink BIO that accepts one byte per write and keeps every byte it is given. + * It reproduces a short-writing BIO so a truncated encoding is detectable. */ +typedef struct { + unsigned char buf[4096]; + size_t len; +} ShortWriteSink; + +static int short_write_bio_write(BIO* b, const char* data, int len) +{ + ShortWriteSink* sink = (ShortWriteSink*)BIO_get_data(b); + + BIO_clear_retry_flags(b); + if ((sink == NULL) || (len <= 0) || (sink->len >= sizeof(sink->buf))) { + return 0; + } + /* Take a single byte so the writer must loop to make progress. */ + sink->buf[sink->len++] = (unsigned char)data[0]; + return 1; +} + +static long short_write_bio_ctrl(BIO* b, int cmd, long num, void* ptr) +{ + (void)b; + (void)num; + (void)ptr; + return (cmd == BIO_CTRL_FLUSH) ? 1 : 0; +} + +static int short_write_bio_create(BIO* b) +{ + BIO_set_init(b, 1); + return 1; +} + +/* Encode pkey twice with the same wolfProvider encoder: once via + * OSSL_ENCODER_to_data (a normal full-writing sink) for a reference, and once + * through a one-byte-at-a-time BIO. The two encodings must be identical. */ +static int test_ecc_encode_short_write(EVP_PKEY* pkey, int selection, + const char* format, const char* structure) +{ + int err = 0; + OSSL_ENCODER_CTX* ectx = NULL; + unsigned char* refData = NULL; + size_t refLen = 0; + BIO_METHOD* meth = NULL; + BIO* bio = NULL; + ShortWriteSink sink; + + memset(&sink, 0, sizeof(sink)); + + ectx = OSSL_ENCODER_CTX_new_for_pkey(pkey, selection, format, structure, + "provider=libwolfprov"); + err = (ectx == NULL); + if (err == 0) { + err = (OSSL_ENCODER_to_data(ectx, &refData, &refLen) != 1); + } + OSSL_ENCODER_CTX_free(ectx); + ectx = NULL; + if (err == 0) { + err = (refLen == 0); + } + + if (err == 0) { + meth = BIO_meth_new(BIO_get_new_index() | BIO_TYPE_SOURCE_SINK, + "short-write"); + err = (meth == NULL); + } + if (err == 0) { + BIO_meth_set_write(meth, short_write_bio_write); + BIO_meth_set_ctrl(meth, short_write_bio_ctrl); + BIO_meth_set_create(meth, short_write_bio_create); + bio = BIO_new(meth); + err = (bio == NULL); + } + if (err == 0) { + BIO_set_data(bio, &sink); + ectx = OSSL_ENCODER_CTX_new_for_pkey(pkey, selection, format, structure, + "provider=libwolfprov"); + err = (ectx == NULL); + } + if (err == 0) { + err = (OSSL_ENCODER_to_bio(ectx, bio) != 1); + } + if (err == 0) { + err = (sink.len != refLen); + if (err) { + PRINT_ERR_MSG("Short write truncated output: %lu of %lu bytes", + (unsigned long)sink.len, (unsigned long)refLen); + } + } + if (err == 0) { + err = (memcmp(sink.buf, refData, refLen) != 0); + if (err) { + PRINT_ERR_MSG("Short-write output does not match the reference"); + } + } + + OSSL_ENCODER_CTX_free(ectx); + BIO_free(bio); + BIO_meth_free(meth); + OPENSSL_free(refData); + + return err; +} + +int test_ecc_encode_short_write_bio(void *data) +{ + int err = 0; + const unsigned char* p = ecc_key_der_256; + EVP_PKEY* pkey = NULL; + + (void)data; + + pkey = d2i_PrivateKey_ex(EVP_PKEY_EC, NULL, &p, sizeof(ecc_key_der_256), + wpLibCtx, NULL); + err = (pkey == NULL); + + if (err == 0) { + PRINT_MSG("SubjectPublicKeyInfo DER survives a short-writing BIO"); + err = test_ecc_encode_short_write(pkey, EVP_PKEY_PUBLIC_KEY, "DER", + "SubjectPublicKeyInfo"); + } + if (err == 0) { + PRINT_MSG("SubjectPublicKeyInfo PEM survives a short-writing BIO"); + err = test_ecc_encode_short_write(pkey, EVP_PKEY_PUBLIC_KEY, "PEM", + "SubjectPublicKeyInfo"); + } + + EVP_PKEY_free(pkey); + + return err; +} +#endif /* WP_HAVE_EC_P256 */ + #ifdef WP_HAVE_ECDSA /* Convenience function for calling test_pkey_sign without RSA-specific diff --git a/test/unit.h b/test/unit.h index b1c21589..0c3ed6fe 100644 --- a/test/unit.h +++ b/test/unit.h @@ -427,6 +427,10 @@ int test_eckeygen_x448(void *data); #endif /* WP_HAVE_ECKEYGEN */ +#ifdef WP_HAVE_EC_P256 +int test_ecc_encode_short_write_bio(void *data); +#endif /* WP_HAVE_EC_P256 */ + #ifdef WP_HAVE_ECDH #ifdef WP_HAVE_ECKEYGEN @@ -470,7 +474,6 @@ int test_ecdh_p224(void *data); #endif /* WP_HAVE_EC_P224 */ #ifdef WP_HAVE_EC_P256 int test_ecdh_invalid_kdf_strings(void *data); -int test_ecc_encode_short_write_bio(void *data); #ifdef WP_HAVE_EPKI_TEST int test_ecc_encode_epki(void *data); #endif