Loading raw EC and RSA keys with OpenSSL 3

Jonathan Wernberg Jonathan.Wernberg at axis.com
Tue Aug 23 12:09:20 UTC 2022


TL;DR: With OpenSSL 3.x API, what is the recommended and safe way to read in an EC private key from raw format into an EVP_PKEY object ready to be used? What is the easiest way to convert an RSA public key from raw modulus and exponent components to proper DER encoded SubjectPublicKeyInfo data?

Hi openssl-users mailing list.

We are having some troubles converting some code from OpenSSL 1.x to OpenSSL 3.x APIs, to get rid of deprecation warnings, and hope someone may be able to give us some hints in the right direction.

One thing we want to do is to convert an EC private key from raw format into a EVP_PKEY. Today we do as below (error checking, freeing and secure memory context things removed for brevity, private key is in "privkey" and curve in "nid"):

BIGNUM *privkey_bn = BN_bin2bn(privkey, privkey_len, NULL);
EC_KEY *eckey = EC_KEY_new_by_curve_name(nid);
const EC_GROUP *group = EC_KEY_get0_group(eckey);
EC_POINT *pubkey_point = EC_POINT_new(group);
EC_POINT_mul(group, pubkey_point, privkey_bn, NULL, NULL, NULL);
EC_KEY_set_private_key(eckey, privkey_bn);
EC_KEY_set_public_key(eckey, pubkey_point);
EVP_PKEY *pkey = EVP_PKEY_new();
EVP_PKEY_assign_EC_KEY(pkey, eckey);

Basically we chained a lot of operations because we could not find any single function that did it for us. Some of these operations are now deprecated, such as the EC_KEY ones. We tried experimenting with the OSSL fromdata() function instead (omitted the mapping from "nid" to "sn" for brevity):

BIGNUM *privkey_bn = BN_bin2bn(privkey, privkey_len, NULL);
EC_GROUP *group = EC_GROUP_new_by_curve_name(nid);
EC_POINT *pubkey_point = EC_POINT_new(group);
EC_POINT_mul(group, pubkey_point, privkey_bn, NULL, NULL, NULL);
unsigned char pubkey_buf[65]; // size just an example
EC_POINT_point2oct(grp, pubkey_point, POINT_CONVERSION_UNCOMPRESSED, pubkey_buf, sizeof(pubkey_buf), NULL);
OSSL_PARAM_BLD *param_bld = OSSL_PARAM_BLD_new();
OSSL_PARAM_BLD_push_utf8_string(param_bld, OSSL_PKEY_PARAM_GROUP_NAME, sn, 0);
OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_PRIV_KEY, privkey_bn);
OSSL_PARAM_BLD_push_octet_string(param_bld, OSSL_PKEY_PARAM_PUB_KEY, pubkey_buf, sizeof(pubkey_buf));
OSSL_PARAM *params = OSSL_PARAM_BLD_to_param(param_bld);
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_from_name(NULL, "EC", NULL);
EVP_PKEY_fromdata_init(ctx);
EVP_PKEY *pkey = NULL;
EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_KEYPAIR, params);
EVP_PKEY_CTX_free(ctx);
ctx = EVP_PKEY_CTX_new(pkey, NULL);
EVP_PKEY_check(ctx);

Although it works, it does not feel right. We ended up chaining many more operations than before. Our understanding was that the new OpenSSL 3.x API was redesigned partially to remove low-level manipulations like these. We have looked though both the migration document and the reference API without finding anything that does our job better. OSSL_DECODERs as frequently suggested in the migration documentation do not seem to support raw EC key formats at all. The EVP_PKEY_new_raw_private_key() functions mentioned in the reference API does not appear to support NIST P curves, according to the documentation. The OSSL fromdata() way above does not calculate the public key from the private one itself, nor does it verify that the points are on the curve, and we are uncertain if there are anything else it does not do that we need to do to not compromise security. We could use d2i_PrivateKey() or d2i_AutoPrivateKey(), which both seem to read in the key data in a secure way and derive the public part automatically. But that way would require us to implement custom logic in our code to manually put together DER data from the raw key data, for multiple curve types.

What is the recommended and safe way to read in an EC private key from raw format into an EVP_PKEY object ready to be used?

Another thing we want to do is to convert an RSA public key from raw modulus and exponent components into proper DER encoded SubjectPublicKeyInfo data. Today we piggyback on OpenSSL to accomplish this like this:

BIGNUM *n = BN_bin2bn(modulus, (int)modulus_len, NULL);
BIGNUM *e = BN_bin2bn(exponent, (int)exponent_len, NULL);
RSA *rsa = RSA_new();
RSA_set0_key(rsa, n, e, NULL);
int data_len = i2d_RSA_PUBKEY(rsa, NULL);
uint8_t *data_buf = malloc((size_t)data_len);
uint8_t *pdata = data_buf;
data_len = i2d_RSA_PUBKEY(rsa, &pdata);

However, some of those functions are now deprecated. Unfortunately our best attempt with OpenSSL 3.x compatible APIs ended up being this comparably long sequence of operations:

BIGNUM *n = BN_bin2bn(modulus, (int)modulus_len, NULL);
BIGNUM *e = BN_bin2bn(exponent, (int)exponent_len, NULL);
EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new_from_name(NULL, "RSA", NULL);
OSSL_PARAM_BLD *param_bld = OSSL_PARAM_BLD_new();
OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_RSA_N, n);
OSSL_PARAM_BLD_push_BN(param_bld, OSSL_PKEY_PARAM_RSA_E, e);
OSSL_PARAM *params = OSSL_PARAM_BLD_to_param(param_bld);
EVP_PKEY_fromdata_init(ctx);
EVP_PKEY *pkey = NULL;
EVP_PKEY_fromdata(ctx, &pkey, EVP_PKEY_PUBLIC_KEY, params);
EVP_PKEY_CTX_free(ctx);
ctx = EVP_PKEY_CTX_new(pkey, NULL);
EVP_PKEY_public_check(ctx);
int data_len = i2d_PUBKEY(pkey, NULL);
uint8_t *data_buf = malloc((size_t)data_len);
uint8_t *pdata = data_buf;
data_len = i2d_PUBKEY(pkey, &pdata);

This also does not feel quite right. Especially the conversion from raw modulus and exponent ended up being much longer, and we failed to find an easier way to do it.

What is the easiest or most recommended way to convert an RSA public key from raw modulus and exponent components to proper DER encoded SubjectPublicKeyInfo data using non-deprecated OpenSSL 3.x APIs?


More information about the openssl-users mailing list