I’ve been scratching my head on this, I don’t have answers but I do have a few things that may help. Lets start!
I presume this is your actual ECDH test here?
getShared :: Hex96 -> Hex32 -> IO Hex32
getShared kp pu = do
sh <- mallocBytes 32
(sec', 96) <- getPtr $ un96 kp
(pub', 64) <- parsePub pu >>= getPtr . un64
r <- ecdh ctx sh pub' sec' nullPtr nullPtr
if r == 1
then Hex32 <$> packPtr (sh, 32)
else error "hh" -- getShared kp pu
It takes a secret and a public key, and does stuff with it before passing it into ecdh
. I noticed that you call parsePub
on the public key before sending it in.
parsePub :: Hex32 -> IO Hex64
parsePub (Hex32 bs) = do
pub64 <- mallocBytes 64
(pub32, 32) <- getPtr bs
ret <- schnorrXOnlyPubKeyParse ctx pub64 pub32
case ret of
1 -> Hex64 <$> packPtr (pub64, 64)
_ -> free pub64 >> error "parsePub error"
If we look at it, we see that it uses schnorrXOnlyPubKeyParse
, and we’re not doing schnorr keys, we’re doing secp256k1 ecdh! So that’s one item of concern, but then I realize its actually binding to secp256k1_xonly_pubkey_parse
, using only the x coordinate of the public point - I don’t know if this is wrong here, but the ECDH example does not do this. More on that later.
Alrighty, lets hop up a level, and see how getShared
is itself being used - I want to know about key generation and stuff. We find that getShared
is only used in getNipTest4
so lets look at the relevant bits:
getNip4Test = do
-- ...
kx <- M.replicateM 100 do
k' <- genKeyPair
p' <- exportPub k'
pure (k', p')
let keys = P.map fst kx
let pubs = P.map snd kx
matrikx <- V.fromList . P.map V.fromList <$> sequenceA [ sequenceA [ getShared ki pj
| pj <- pubs]
| ki <- keys]
-- ...
Here, we see you are using genKeyPair
(all good) to get the keypair, and then exportPub
to extract the public key, and then getShared gets called with all the permutations. Neat, but then I notice that you are passing in the keypair and public key to getShared
- and not just the secret key and public key. Less good. This means that instead of getting passed a secret key, ecdh
is passed a keypair!
It may work by accident, if the keypair (96 bytes) starts with the secret key (32 bytes) then its just a longer-than-expected buffer, but the following note makes it clear we should not rely on this behavior. This is another concern.
From secp256k1_extrakeys.h describing secp256k1_keypair
“The exact representation of data inside is implementation defined and not guaranteed to be portable between different platforms or versions.”
Alright, lets take a look at exportPub
, our last mystery!
exportPub :: Hex96 -> IO Hex32
exportPub (Hex96 bs) = do
(priv, 96) <- getPtr bs
pub64 <- mallocBytes 64
void $ keyPairXOnlyPubKey ctx pub64 nullPtr priv
pub <- mallocBytes 32
void $ schnorrPubKeySerialize ctx pub (castPtr pub64)
Hex32 <$> packPtr (pub, 32)
More schnorr secp256k1_xonly_pubkey_parse
? Also, why do we need to export and then parse the public key?
So, all of that looks a little bit funky. A few more things jump out at me:
-
you have a global ctx
that isn’t randomized, stays alive, and gets reused
-
You are using the xonly
functions, instead of secp256k1_ec_pubkey_create
?
-
secp256k1_pubkey
and secp256k1_xonly_pubkey
are different structures
-
secp256k1_ecdh
expects a secp256k1_pubkey
, but are giving it a secp256k1_xonly_pubkey
From: secp256k1_extrakeys.h describing the xonly functions
“An x-only pubkey encodes a point whose Y coordinate is even.”
This may not be relevant, but it might explain your apparent 50/50 success / failure.
I did find a nice lovely C example for libsecp256k1
ECDH, and its usage looks very different from what you are doing here. You might try re-implementing the example line-by-line, and see how that work for you.