Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

code example please for signing ethereum transactions #2889

Closed
fxfactorial opened this issue Mar 3, 2022 · 8 comments
Closed

code example please for signing ethereum transactions #2889

fxfactorial opened this issue Mar 3, 2022 · 8 comments

Comments

@fxfactorial
Copy link

as a follow up to #2883 (thank you folks) , I'm looking for dcrd v4 example code that can be a drop in replacement for https://github.com/ethereum/go-ethereum/blob/master/crypto/secp256k1/secp256.go#L70

@chappjc
Copy link
Member

chappjc commented Mar 3, 2022

You'd follow the example of the existing no-cgo Sign function here https://github.com/ethereum/go-ethereum/blob/8fddf27a989e246659fd018ea9be37b2b4f55326/crypto/signature_nocgo.go#L61
Note that the cgo Sign function in the crypto package simply serializes the private key and calls into Sign function you linked to in the crypto/secp256k1 package: https://github.com/ethereum/go-ethereum/blob/8fddf27a989e246659fd018ea9be37b2b4f55326/crypto/signature_cgo.go#L55-L61

So to use dcrd's secp256k1/v4/ecdsa instead, just use that instead of btcec's.

@chappjc
Copy link
Member

chappjc commented Mar 3, 2022

Oh, the only real "trick" you have to address is using dcrd/secp256k1/v4.PrivKeyFromBytes as needed since you can't pull off the same hack they used of casting between a *btcec.PrivateKey and a std lib *crypto/ecdsa.PrivateKey. Otherwise it's straight forward to use dcrd's SignCompact.

@davecgh
Copy link
Member

davecgh commented Mar 4, 2022

This is not polished by any means, but should be the broad strokes.

Run it on the playground: https://go.dev/play/p/gIbvbly7n9h

package main

import (
	"errors"
	"fmt"

	"github.com/decred/dcrd/dcrec/secp256k1/v4"
	"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
)

var (
	ErrInvalidMsgLen       = errors.New("invalid message length, need 32 bytes")
	ErrInvalidKey          = errors.New("invalid private key")
	ErrInvalidSignatureLen = errors.New("invalid signature length")
	ErrInvalidRecoveryID   = errors.New("invalid signature recovery id")
	ErrRecoverFailed       = errors.New("recovery failed")
)

func Sign(msg []byte, seckey []byte) ([]byte, error) {
	if len(msg) != 32 {
		return nil, ErrInvalidMsgLen
	}
	if len(seckey) != 32 {
		return nil, ErrInvalidKey
	}
	var privKey secp256k1.PrivateKey
	if overflow := privKey.Key.SetByteSlice(seckey); overflow || privKey.Key.IsZero() {
		return nil, ErrInvalidKey
	}
	sig := ecdsa.SignCompact(&privKey, msg, false)
	privKey.Zero()

	// Convert to Ethereum signature format with 'recovery id' v at the end.
	v := sig[0] - 27
	copy(sig, sig[1:])
	sig[64] = v
	return sig, nil
}

func checkSignature(sig []byte) error {
	if len(sig) != 65 {
		return ErrInvalidSignatureLen
	}
	if sig[64] >= 4 {
		return ErrInvalidRecoveryID
	}
	return nil
}

func RecoverPubkey(msg []byte, sig []byte) ([]byte, error) {
	if len(msg) != 32 {
		return nil, ErrInvalidMsgLen
	}
	if err := checkSignature(sig); err != nil {
		return nil, err
	}

	// Convert from Ethereum signature format with 'recovery id' v at the end.
	var convertedSig [65]byte
	copy(convertedSig[1:], sig)
	convertedSig[0] = sig[64] + 27

	pub, _, err := ecdsa.RecoverCompact(convertedSig[:], msg)
	if err != nil {
		return nil, ErrRecoverFailed
	}

	return pub.SerializeUncompressed(), nil
}

func main() {
	var msg [32]byte         // Ordinarily would be the hash to sign...
	var seckey = [32]byte{1} // Ordinarily would come from somewhere else...
	sig, err := Sign(msg[:], seckey[:])
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("sig: %x\n", sig)

	pubkey, err := RecoverPubkey(msg[:], sig)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("recovered pubkey: %x\n", pubkey)
}

@fxfactorial
Copy link
Author

excellent, works

@davecgh
Copy link
Member

davecgh commented Mar 5, 2022

I should also note that my original example code was not verifying the provided private key is within the valid range for compact signing with Ethereum, which is [1, N-1], so you would need to implement that for compatibility purposes as well. Namely, as the documentation for secp256k1.PrivKeyFromBytes calls out:

PrivKeyFromBytes returns a private based on the provided byte slice which is interpreted as an unsigned 256-bit big-endian integer in the range [0, N-1], where N is the order of the curve.

Note that this means passing a slice with more than 32 bytes is truncated and that truncated value is reduced modulo N. It is up to the caller to either provide a value in the appropriate range or choose to accept the described behavior.

Typically callers should simply make use of GeneratePrivateKey when creating private keys which properly handles generation of appropriate values.

Notice that the range it accepts includes 0 (which is not a not a valid private key for compact signatures) and it automatically reduces anything >= N.

Thus, you would really want to check the incoming raw private key bytes to ensure they are neither a 0 nor overflow (are >= N) for full compatibility.

I updated the example code to do that.

@ryandotsmith
Copy link

I should also note that my original example code was not verifying the provided private key is within the valid range for compact signing with Ethereum, which is [1, N-1]

Is this necessary when signing with a *secp256k1.PrivateKey using github.com/decred/dcrd/dcrec/secp256k1/v4?

For example, is this code safe to use?

func Sign(msg []byte, privKey *secp256k1.PrivateKey) ([]byte, error) {
	if len(msg) != 32 {
		return nil, ErrInvalidMsgLen
	}
	sig := ecdsa.SignCompact(privKey, msg, false)
	// Convert to Ethereum signature format with 'recovery id' v at the end.
	v := sig[0] - 27
	copy(sig, sig[1:])
	sig[64] = v
	return sig, nil
}

@davecgh
Copy link
Member

davecgh commented Nov 4, 2022

For example, is this code safe to use?

func Sign(msg []byte, privKey *secp256k1.PrivateKey) ([]byte, error) {
	if len(msg) != 32 {
		return nil, ErrInvalidMsgLen
	}
	sig := ecdsa.SignCompact(privKey, msg, false)
	// Convert to Ethereum signature format with 'recovery id' v at the end.
	v := sig[0] - 27
	copy(sig, sig[1:])
	sig[64] = v
	return sig, nil
}

Mostly. A secp256k1.PrivateKey internally uses a secp256k1.ModNScalar (because private keys always work modulo the group order), so it is guaranteed to be in the range [0, N-1], and thus this code is fine in terms of the upper limit. However, the 0 case would depend on how the caller created the private key. The private key is guaranteed to be in the correct range if it was created via secp256k1.GeneratePrivateKey. However, as noted above, if the caller is blindly and manually converting bytes to a private key without checking for zero as is required, it is technically possible to create one that is the invalid value of 0.

That said, realistically speaking, callers should never create a private key that is 0 because that is always invalid in all ECC crypto given public keys are generated by performing a scalar multiplication by the group generator and anything multiplied by 0 is obviously never a valid point on the curve. Moreover, a private key of zero would lead to never being able to create a valid signature which would result in an infinite loop in the signing code.

Since your proposed function has an error return, and if it is an exported function that is a part of a public API, you could be extra paranoid and do something like if privKey.Key.IsZero() { return nil, ErrInvalidKey } to protect against misbehaving callers.

@ryandotsmith
Copy link

@davecgh This makes sense. Thank you for taking the time to explain the details. I have a much better understanding now.

Also, I've been following your development through various projects --including this project and btcd-- and I have learned a lot from your code and your excellent writing. Thank you so much! Legend!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants