Description:
EVP_PKEY_derive via gostprov ECDHE provider returns an X-coordinate of the ECDHE shared secret that differs from the mathematically correct value for GC256A. Independently verified by two implementations — crypto-gost Java library and pure Python EC arithmetic (BigInteger, no external libraries) — both produce the matching correct result.
Fixed test case:
Curve: GC256A (id-tc26-gost-3410-2012-256-paramSetA)
Cofactor: 1
Client private key d (LE bytes, 32):
34 70 5a cb 76 bb 75 f8 cf 10 25 54 c4 7b e6 34
93 04 31 0e 58 c9 19 06 fa 9d 90 89 ff 2f 58 a0
Server public key Q, wire encoding X_LE || Y_LE (64 bytes):
77 d4 92 51 a6 8b 11 46 ee 23 f5 39 3a 64 b4 c6
c3 8e 1c c5 3c 16 fb eb ba 25 d9 a4 e7 6f ce e8
d6 1a f5 7a 7f d1 56 7b 27 5d 43 a6 75 43 22 bf
57 84 65 3a b2 be 8b b4 d0 6d 7c 90 07 81 2e a3
Expected shared secret X_LE (32 bytes, verified by crypto-gost & Python):
86 58 b4 de 65 a7 7e 50 87 92 06 2f e2 2a 5a c1
68 d2 8c 19 34 2d 59 4a 4a 34 55 c2 f4 21 97 00
Actual output of EVP_PKEY_derive: differs from expected.
How to reproduce:
/* Load client private key (LE bytes, paramset TCA = GC256A) */
EVP_PKEY_CTX *kctx = EVP_PKEY_CTX_new_from_name(NULL, "gost2012_256", NULL);
OSSL_PARAM kp[] = {
OSSL_PARAM_construct_octet_string("priv", client_priv_le, 32),
OSSL_PARAM_construct_utf8_string("paramset", "TCA", 0),
OSSL_PARAM_construct_end()
};
EVP_PKEY *priv_key = NULL;
EVP_PKEY_fromdata_init(kctx); /* void return */
EVP_PKEY_fromdata(kctx, &priv_key, EVP_PKEY_KEYPAIR, kp);
/* Load server public key (X_LE || Y_LE, 64 bytes) */
EVP_PKEY_CTX *pkctx = EVP_PKEY_CTX_new_from_name(NULL, "gost2012_256", NULL);
OSSL_PARAM pp[] = {
OSSL_PARAM_construct_octet_string("pub", server_pub_le, 64),
OSSL_PARAM_construct_utf8_string("paramset", "TCA", 0),
OSSL_PARAM_construct_end()
};
EVP_PKEY *peer_key = NULL;
EVP_PKEY_fromdata_init(pkctx);
EVP_PKEY_fromdata(pkctx, &peer_key, EVP_PKEY_PUBLIC_KEY, pp);
/* Derive shared secret */
EVP_PKEY_CTX *dctx = EVP_PKEY_CTX_new_from_pkey(NULL, priv_key, NULL);
EVP_PKEY_derive_init(dctx);
EVP_PKEY_derive_set_peer(dctx, peer_key);
unsigned char secret[32];
size_t slen = sizeof(secret);
EVP_PKEY_derive(dctx, secret, &slen);
/* Expected: secret == 8658b4de... (see above) */
/* Actual: differs */
Call chain:
ecdhe_derive() gost_prov_keyxch.c
└─ internal_compute_ecdh() gost_ec_keyx.c:27
└─ gost_ec_point_mul() gost_ec_sign.c:560-579
└─ point_mul_id_tc26_gost_3410_2012_256_paramSetA()
ecp_id_tc26_gost_3410_2012_256_paramSetA.c:3876
└─ point_mul() line 3809
└─ var_smul_rwnaf() line 3599
Probable cause:
var_smul_rwnaf() (Edwards wNAF scalar multiplication) or its normalization via fiat_id_tc26_gost_3410_2012_256_paramSetA_to_bytes() returns a non-canonical representation. The field prime p embedded in the carry chain is correct (0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD97, verified in carry() at line 584 via 0x269 multiplier). The issue is likely in the projective → affine normalization step.
Description:
EVP_PKEY_derive via gostprov ECDHE provider returns an X-coordinate of the ECDHE shared secret that differs from the mathematically correct value for GC256A. Independently verified by two implementations — crypto-gost Java library and pure Python EC arithmetic (BigInteger, no external libraries) — both produce the matching correct result.
Fixed test case:
Curve: GC256A (id-tc26-gost-3410-2012-256-paramSetA)
Cofactor: 1
Client private key d (LE bytes, 32):
34 70 5a cb 76 bb 75 f8 cf 10 25 54 c4 7b e6 34
93 04 31 0e 58 c9 19 06 fa 9d 90 89 ff 2f 58 a0
Server public key Q, wire encoding X_LE || Y_LE (64 bytes):
77 d4 92 51 a6 8b 11 46 ee 23 f5 39 3a 64 b4 c6
c3 8e 1c c5 3c 16 fb eb ba 25 d9 a4 e7 6f ce e8
d6 1a f5 7a 7f d1 56 7b 27 5d 43 a6 75 43 22 bf
57 84 65 3a b2 be 8b b4 d0 6d 7c 90 07 81 2e a3
Expected shared secret X_LE (32 bytes, verified by crypto-gost & Python):
86 58 b4 de 65 a7 7e 50 87 92 06 2f e2 2a 5a c1
68 d2 8c 19 34 2d 59 4a 4a 34 55 c2 f4 21 97 00
Actual output of EVP_PKEY_derive: differs from expected.
How to reproduce:
Call chain:
ecdhe_derive() gost_prov_keyxch.c
└─ internal_compute_ecdh() gost_ec_keyx.c:27
└─ gost_ec_point_mul() gost_ec_sign.c:560-579
└─ point_mul_id_tc26_gost_3410_2012_256_paramSetA()
ecp_id_tc26_gost_3410_2012_256_paramSetA.c:3876
└─ point_mul() line 3809
└─ var_smul_rwnaf() line 3599
Probable cause:
var_smul_rwnaf() (Edwards wNAF scalar multiplication) or its normalization via fiat_id_tc26_gost_3410_2012_256_paramSetA_to_bytes() returns a non-canonical representation. The field prime p embedded in the carry chain is correct (0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD97, verified in carry() at line 584 via 0x269 multiplier). The issue is likely in the projective → affine normalization step.