crypto-util: zero-pad ECC point coordinates to curve width

ecc_pkey_to_curve_x_y() marshalled the affine point coordinates with
BN_num_bytes() + BN_bn2bin(), which produce the minimal big-endian
encoding and strip any leading zero bytes. The affine coordinates are
elements of the curve's underlying field, though, and consumers expect
them at the fixed field width.

About 1 in 256 keys per coordinate has a most significant zero byte on
the curves whose field width is a whole number of bytes, and about 1 in 2
on P-521, where only the low bit of the leading byte is ever set. The
coordinate then comes out one or more bytes short of the field width. When
that happens tpm2_tpm2b_public_from_openssl_pkey() builds a TPM2B_PUBLIC
whose unique.ecc.x or .y is undersized. The TPM 2.0 reference
implementation requires each coordinate size to equal the key size in
bytes, so TPM2_LoadExternal() rejects the public area with TPM_RC_KEY.
The name a TPM derives for the object also stops matching the name
systemd computes in software, because the name hashes the marshalled
public area.

For a signed PCR policy the key is only round-tripped through the TPM at
unlock time, so an enrolled key with a leading-zero coordinate is
accepted by systemd-cryptenroll and systemd-measure and only fails when
the volume is first unlocked, with:

    Esys_LoadExternal() Esys Finish ErrorCode (0x000002dc)
    Failed to unseal secret using TPM2: State not recoverable

Derive the curve's field width from the EC group degree and zero-pad both
coordinates to it with BN_bn2binpad(). Padding a full-width coordinate is
a no-op, so only keys that a reference-conformant TPM rejects today change
their marshalling; no enrollment that currently unlocks changes behavior.
Every caller of this converter inherits the fix, including the
systemd-creds public-key binding.

Because the width now comes from the group rather than from the BIGNUM
being written, BN_bn2binpad() can fail where BN_bn2bin() could not, so
both calls return -EIO rather than asserting. It reports that through its
return value without touching the OpenSSL error queue, so these use
log_debug_errno() rather than log_openssl_errors(). EC_GROUP_get_degree
is file-local, matching the other libcrypto symbols used only here.

The tests pin the full expected coordinates for a P-384 key and a P-521
key that each have a leading zero byte. test-tpm2 gains a case pinning
the TPM object name for the P-384 key, because the name is derived from
the marshalled public area and this change alters it for exactly those
keys a reference-conformant TPM rejects today.
This commit is contained in:
Andrew Dunn
2026-08-20 04:06:04 +09:00
committed by Yu Watanabe
parent dc4108c43c
commit 8e3d6dc535
3 changed files with 84 additions and 8 deletions
+27 -6
View File
@@ -110,6 +110,7 @@ DLSYM_PROTOTYPE(EC_GROUP_get0_generator) = NULL;
DLSYM_PROTOTYPE(EC_GROUP_get0_order) = NULL;
DLSYM_PROTOTYPE(EC_GROUP_get_curve) = NULL;
DLSYM_PROTOTYPE(EC_GROUP_get_curve_name) = NULL;
static DLSYM_PROTOTYPE(EC_GROUP_get_degree) = NULL;
DLSYM_PROTOTYPE(EC_GROUP_get_field_type) = NULL;
DLSYM_PROTOTYPE(EC_GROUP_new_by_curve_name) = NULL;
DLSYM_PROTOTYPE(EC_POINT_free) = NULL;
@@ -435,6 +436,7 @@ int dlopen_libcrypto(int log_level) {
DLSYM_ARG(EC_GROUP_get0_order),
DLSYM_ARG(EC_GROUP_get_curve),
DLSYM_ARG(EC_GROUP_get_curve_name),
DLSYM_ARG(EC_GROUP_get_degree),
DLSYM_ARG(EC_GROUP_get_field_type),
DLSYM_ARG(EC_GROUP_new_by_curve_name),
DLSYM_ARG(EC_POINT_free),
@@ -1735,24 +1737,43 @@ int ecc_pkey_to_curve_x_y(
if (!sym_EVP_PKEY_get_bn_param(pkey, OSSL_PKEY_PARAM_EC_PUB_Y, &bn_y))
return log_openssl_errors(LOG_DEBUG, "Failed to get ECC point y");
size_t x_size = sym_BN_num_bytes(bn_x), y_size = sym_BN_num_bytes(bn_y);
_cleanup_free_ void *x = malloc(x_size), *y = malloc(y_size);
/* The affine coordinates are elements of the curve's underlying field and must be
* marshalled at the fixed field width. BN_bn2bin() writes the minimal big-endian
* encoding, dropping any leading zero bytes, so a coordinate whose most significant byte
* is zero (roughly 1 in 256 keys per coordinate) comes out one or more bytes short. A short
* coordinate makes callers such as tpm2_tpm2b_public_from_openssl_pkey() build a
* TPM2B_PUBLIC that TPM2_LoadExternal() rejects with TPM_RC_KEY. The object name hashes
* the marshalled public area, so the name a TPM derives for the key also stops matching
* the name systemd computes in software. Size the buffers to the field width and
* zero-pad. */
_cleanup_(EC_GROUP_freep) EC_GROUP *group = sym_EC_GROUP_new_by_curve_name(curve_id);
if (!group)
return log_openssl_errors(LOG_DEBUG, "ECC curve id %d not supported", curve_id);
int bits = sym_EC_GROUP_get_degree(group);
assert(bits > 0);
size_t size = DIV_ROUND_UP(bits, 8);
_cleanup_free_ void *x = malloc(size), *y = malloc(size);
if (!x || !y)
return log_oom_debug();
assert(sym_BN_bn2bin(bn_x, x) == (int) x_size);
assert(sym_BN_bn2bin(bn_y, y) == (int) y_size);
if (sym_BN_bn2binpad(bn_x, x, size) < 0)
return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to marshal ECC point x to %zu bytes.", size);
if (sym_BN_bn2binpad(bn_y, y, size) < 0)
return log_debug_errno(SYNTHETIC_ERRNO(EIO), "Failed to marshal ECC point y to %zu bytes.", size);
if (ret_curve_id)
*ret_curve_id = curve_id;
if (ret_x)
*ret_x = TAKE_PTR(x);
if (ret_x_size)
*ret_x_size = x_size;
*ret_x_size = size;
if (ret_y)
*ret_y = TAKE_PTR(y);
if (ret_y_size)
*ret_y_size = y_size;
*ret_y_size = size;
return 0;
}
+42
View File
@@ -89,6 +89,48 @@ TEST(ecc_pkey_curve_x_y) {
assert_se(memcmp_nn(y, y_len, y2, y2_size) == 0);
}
TEST(ecc_pkey_curve_x_y_leading_zero) {
/* Regression test: a P-384 public key whose x-coordinate has a most significant zero
* byte. BN_bn2bin() would strip it and return a 47-byte coordinate; ecc_pkey_to_curve_x_y()
* must zero-pad both coordinates to the 48-byte P-384 field width. */
DEFINE_HEX_PTR(key, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b34454541434944596741454147625a626d4a32534231424a4879633178674f7a4a646e67586767566254700a5a76515070516b54564779387362777677734872635a4871307a5a76782f316c4336787a5a64615648433147303835786c636c4a7770435a57685571576866730a2f6a70337a436d6e446a49754a79505735435148494930652b575a47706263490a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a");
_cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
ASSERT_OK(openssl_pubkey_from_pem(key, key_len, &pkey));
_cleanup_free_ void *x = NULL, *y = NULL;
size_t x_size, y_size;
int curve_id;
ASSERT_OK(ecc_pkey_to_curve_x_y(pkey, &curve_id, &x, &x_size, &y, &y_size));
ASSERT_EQ(curve_id, NID_secp384r1);
DEFINE_HEX_PTR(expected_x, "0066d96e6276481d41247c9cd7180ecc976781782055b4e966f40fa50913546cbcb1bc2fc2c1eb7191ead3366fc7fd65");
ASSERT_EQ(memcmp_nn(x, x_size, expected_x, expected_x_len), 0);
DEFINE_HEX_PTR(expected_y, "0bac7365d6951c2d46d3ce7195c949c290995a152a5a17ecfe3a77cc29a70e322e2723d6e42407208d1ef96646a5b708");
ASSERT_EQ(memcmp_nn(y, y_size, expected_y, expected_y_len), 0);
}
TEST(ecc_pkey_curve_x_y_p521) {
/* P-521's field width is not a whole number of bytes: DIV_ROUND_UP(521, 8) is 66, and only
* the low bit of the leading byte can ever be set, so roughly half of all coordinates
* encode minimally in 65 bytes. This is a pinned key whose x-coordinate is one of them. */
DEFINE_HEX_PTR(key, "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d4947624d42414742797147534d343941674547425375424241416a41344747414151416f527a475046566f7935437a474b6c385359757848345764365375470a4c66674135624d2b527258474c31794273584b443434366f4c30767530664e71717a6e667a456d3959395447634a394c385771784a3774325665514255466e460a6b6a2f49324137637a4843625a6b6b795753796656706e39666b70414542473166354c53464d7244667965664953324573353035334a366441434462796458550a435958353948514f4c443044794f3851734d6f3d0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a");
_cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
ASSERT_OK(openssl_pubkey_from_pem(key, key_len, &pkey));
_cleanup_free_ void *x = NULL, *y = NULL;
size_t x_size, y_size;
int curve_id;
ASSERT_OK(ecc_pkey_to_curve_x_y(pkey, &curve_id, &x, &x_size, &y, &y_size));
ASSERT_EQ(curve_id, NID_secp521r1);
DEFINE_HEX_PTR(expected_x, "00a11cc63c5568cb90b318a97c498bb11f859de92b862df800e5b33e46b5c62f5c81b17283e38ea82f4beed1f36aab39dfcc49bd63d4c6709f4bf16ab127bb7655e4");
ASSERT_EQ(memcmp_nn(x, x_size, expected_x, expected_x_len), 0);
DEFINE_HEX_PTR(expected_y, "015059c5923fc8d80edccc709b664932592c9f5699fd7e4a401011b57f92d214cac37f279f212d84b39d39dc9e9d0020dbc9d5d40985f9f4740e2c3d03c8ef10b0ca");
ASSERT_EQ(memcmp_nn(y, y_size, expected_y, expected_y_len), 0);
}
TEST(invalid) {
_cleanup_(EVP_PKEY_freep) EVP_PKEY *pkey = NULL;
+15 -2
View File
@@ -901,7 +901,7 @@ static void check_tpm2b_public_name(const TPM2B_PUBLIC *public, const char *hexn
assert_se(memcmp_nn(name.name, name.size, expected, expected_len) == 0);
}
static void check_tpm2b_public_from_ecc_pem(const char *pem, const char *hexx, const char *hexy, const char *hexfp, const char *hexname) {
static void check_tpm2b_public_from_ecc_pem(const char *pem, TPMI_ECC_CURVE curve, const char *hexx, const char *hexy, const char *hexfp, const char *hexname) {
TPM2B_PUBLIC public = {};
TPMT_PUBLIC *p = &public.publicArea;
@@ -909,7 +909,7 @@ static void check_tpm2b_public_from_ecc_pem(const char *pem, const char *hexx, c
get_tpm2b_public_from_pem(key, key_len, &public);
assert_se(p->type == TPM2_ALG_ECC);
assert_se(p->parameters.eccDetail.curveID == TPM2_ECC_NIST_P256);
assert_se(p->parameters.eccDetail.curveID == curve);
DEFINE_HEX_PTR(expected_x, hexx);
assert_se(memcmp_nn(p->unique.ecc.x.buffer, p->unique.ecc.x.size, expected_x, expected_x_len) == 0);
@@ -944,11 +944,24 @@ static void check_tpm2b_public_from_rsa_pem(const char *pem, const char *hexn, u
TEST(tpm2b_public_from_openssl_pkey) {
/* standard ECC key */
check_tpm2b_public_from_ecc_pem("2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d466b77457759484b6f5a497a6a3043415159494b6f5a497a6a30444151634451674145726a6e4575424c73496c3972687068777976584e50686a346a426e500a44586e794a304b395579724e6764365335413532542b6f5376746b436a365a726c34685847337741515558706f426c532b7448717452714c35513d3d0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a",
TPM2_ECC_NIST_P256,
"ae39c4b812ec225f6b869870caf5cd3e18f88c19cf0d79f22742bd532acd81de",
"92e40e764fea12bed9028fa66b9788571b7c004145e9a01952fad1eab51a8be5",
"cd3373293b62a52b48c12100e80ea9bfd806266ce76893a5ec31cb128052d97c",
"000b5c127e4dbaf8fb7bac641e8db25a84a48db876ca7ee3bd317ae1a4554ff72f17");
/* ECC key whose x-coordinate has a most significant zero byte. The unpadded marshalling
* this used to produce made unique.ecc.x 47 bytes instead of 48, which both TPM2_LoadExternal()
* rejects and gives the object a different name. The name below is the one the padded
* public area must hash to; it is derived from the TPM 2.0 spec (nameAlg || SHA256 of the
* marshalled TPMT_PUBLIC), not from this implementation. */
check_tpm2b_public_from_ecc_pem("2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b34454541434944596741454147625a626d4a32534231424a4879633178674f7a4a646e67586767566254700a5a76515070516b54564779387362777677734872635a4871307a5a76782f316c4336787a5a64615648433147303835786c636c4a7770435a57685571576866730a2f6a70337a436d6e446a49754a79505735435148494930652b575a47706263490a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a",
TPM2_ECC_NIST_P384,
"0066d96e6276481d41247c9cd7180ecc976781782055b4e966f40fa50913546cbcb1bc2fc2c1eb7191ead3366fc7fd65",
"0bac7365d6951c2d46d3ce7195c949c290995a152a5a17ecfe3a77cc29a70e322e2723d6e42407208d1ef96646a5b708",
"a9943d99a720879eb87b6a96bbcf3f6d7b0116aff3ba49081172796a295a496a",
"000b0ddf2c9d545c6662b10d1f2d0a4a115eedab13f4e18da9f8c45d20d926b2c90c");
/* standard RSA key */
check_tpm2b_public_from_rsa_pem("2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d494942496a414e42676b71686b6947397730424151454641414f43415138414d49494243674b4341514541795639434950652f505852337a436f63787045300a6a575262546c3568585844436b472f584b79374b6d2f4439584942334b734f5a31436a5937375571372f674359363170697838697552756a73413464503165380a593445336c68556d374a332b6473766b626f4b64553243626d52494c2f6675627771694c4d587a41673342575278747234547545443533527a373634554650640a307a70304b68775231496230444c67772f344e67566f314146763378784b4d6478774d45683567676b73733038326332706c354a504e32587677426f744e6b4d0a5471526c745a4a35355244436170696e7153334577376675646c4e735851357746766c7432377a7637344b585165616d704c59433037584f6761304c676c536b0a79754774586b6a50542f735542544a705374615769674d5a6f714b7479563463515a58436b4a52684459614c47587673504233687a766d5671636e6b47654e540a65774944415141420a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a",
"c95f4220f7bf3d7477cc2a1cc691348d645b4e5e615d70c2906fd72b2eca9bf0fd5c80772ac399d428d8efb52aeff80263ad698b1f22b91ba3b00e1d3f57bc638137961526ec9dfe76cbe46e829d53609b99120bfdfb9bc2a88b317cc0837056471b6be13b840f9dd1cfbeb85053ddd33a742a1c11d486f40cb830ff8360568d4016fdf1c4a31dc7030487982092cb34f36736a65e493cdd97bf0068b4d90c4ea465b59279e510c26a98a7a92dc4c3b7ee76536c5d0e7016f96ddbbcefef829741e6a6a4b602d3b5ce81ad0b8254a4cae1ad5e48cf4ffb140532694ad6968a0319a2a2adc95e1c4195c29094610d868b197bec3c1de1cef995a9c9e419e3537b",