Support precomputed ed25519 public keys

This commit is contained in:
Chad Retz 2018-05-19 07:20:36 -05:00
parent c11e3bb90d
commit 00909b144c
7 changed files with 356 additions and 45 deletions

View File

@ -99,22 +99,22 @@ func (r *RSAKey) Blob() string {
}
// ED25519Key is a Key for AddOnion that is a ed25519 key (i.e. v3).
type ED25519Key ed25519.PrivateKey
type ED25519Key struct{ ed25519.KeyPair }
// ED25519KeyFromBlob creates a ED25519Key for the given response blob.
func ED25519KeyFromBlob(blob string) (ED25519Key, error) {
func ED25519KeyFromBlob(blob string) (*ED25519Key, error) {
byts, err := base64.StdEncoding.DecodeString(blob)
if err != nil {
return nil, err
}
return ED25519Key(ed25519.PrivateKey(byts)), nil
return &ED25519Key{ed25519.PrivateKey(byts).KeyPair()}, nil
}
// Type implements Key.Type.
func (ED25519Key) Type() KeyType { return KeyTypeED25519V3 }
func (*ED25519Key) Type() KeyType { return KeyTypeED25519V3 }
// Blob implements Key.Blob.
func (e ED25519Key) Blob() string { return base64.StdEncoding.EncodeToString(e) }
func (e *ED25519Key) Blob() string { return base64.StdEncoding.EncodeToString(e.PrivateKey()) }
// AddOnionRequest is a set of request params for AddOnion.
type AddOnionRequest struct {

View File

@ -11,12 +11,14 @@ import (
"github.com/stretchr/testify/require"
)
var torEnabled bool
var torExePath string
var torVerbose bool
var torIncludeNetworkTests bool
var globalEnabledNetworkContext *TestContext
func TestMain(m *testing.M) {
flag.BoolVar(&torEnabled, "tor", false, "Whether any of the integration tests are enabled")
flag.StringVar(&torExePath, "tor.path", "tor", "The Tor exe path")
flag.BoolVar(&torVerbose, "tor.verbose", false, "Show verbose test info")
flag.BoolVar(&torIncludeNetworkTests, "tor.network", false, "Include network tests")
@ -29,14 +31,10 @@ func TestMain(m *testing.M) {
os.Exit(exitCode)
}
func SkipIfExcludingNetworkTests(t *testing.T) {
if !torIncludeNetworkTests {
t.Skip("Only runs if -tor.network is set")
}
}
func GlobalEnabledNetworkContext(t *testing.T) *TestContext {
SkipIfExcludingNetworkTests(t)
if !torEnabled || !torIncludeNetworkTests {
t.Skip("Only runs if -tor and -tor.network is set")
}
if globalEnabledNetworkContext == nil {
ctx := NewTestContext(t, nil)
ctx.CloseTorOnClose = false
@ -61,6 +59,9 @@ type TestContext struct {
}
func NewTestContext(t *testing.T, conf *tor.StartConf) *TestContext {
if !torEnabled {
t.Skip("Only runs if -tor is set")
}
// Build start conf
if conf == nil {
conf = &tor.StartConf{}

View File

@ -21,7 +21,7 @@ type OnionService struct {
// Key is the private key for this service. It is either the set key, the
// generated key, or nil if asked to discard the key. If present, it is
// *crypto/rsa.PrivateKey (1024 bit) when Version3 is false or
// github.com/cretz/bine/torutil/ed25519.PrivateKey when Version3 is true.
// github.com/cretz/bine/torutil/ed25519.KeyPair when Version3 is true.
Key crypto.PrivateKey
// Version3 says whether or not this service is a V3 service.
@ -66,7 +66,7 @@ type ListenConf struct {
// Key is the private key to use. If not present, a key is generated based
// on whether Version3 is true or false. If present, it must be a
// *crypto/rsa.PrivateKey (1024 bit), a
// github.com/cretz/bine/torutil/ed25519.PrivateKey, a
// github.com/cretz/bine/torutil/ed25519.KeyPair, a
// golang.org/x/crypto/ed25519.PrivateKey, or a
// github.com/cretz/bine/control.Key.
Key crypto.PrivateKey
@ -179,17 +179,17 @@ func (t *Tor) Listen(ctx context.Context, conf *ListenConf) (*OnionService, erro
} else {
req.Key = key
}
case ed25519.PrivateKey:
case ed25519.KeyPair:
svc.Key = key
svc.Version3 = true
req.Key = control.ED25519Key(key)
req.Key = &control.ED25519Key{key}
case othered25519.PrivateKey:
properKey := ed25519.FromCryptoPrivateKey(key)
svc.Key = properKey
svc.Version3 = true
req.Key = control.ED25519Key(properKey)
case control.ED25519Key:
svc.Key = ed25519.PrivateKey(key)
req.Key = &control.ED25519Key{properKey}
case *control.ED25519Key:
svc.Key = key.KeyPair
svc.Version3 = true
req.Key = key
default:
@ -235,8 +235,8 @@ func (t *Tor) Listen(ctx context.Context, conf *ListenConf) (*OnionService, erro
// Do nothing
case *control.RSAKey:
svc.Key = key.PrivateKey
case control.ED25519Key:
svc.Key = ed25519.PrivateKey(key)
case *control.ED25519Key:
svc.Key = key.KeyPair
default:
err = fmt.Errorf("Unrecognized result key type: %T", key)
}

View File

@ -16,21 +16,31 @@ import (
"golang.org/x/crypto/ed25519"
)
// Ref: https://stackoverflow.com/questions/44810708/ed25519-public-result-is-different
const (
// PublicKeySize is the size, in bytes, of public keys as used in this package.
PublicKeySize = 32
// PrivateKeySize is the size, in bytes, of private keys as used in this package.
PrivateKeySize = 64
// SignatureSize is the size, in bytes, of signatures generated and verified by this package.
SignatureSize = 64
)
// PrivateKey is a 64-byte Ed25519 private key.
// PrivateKey is a 64-byte Ed25519 private key. Unlike
// golang.org/x/crypto/ed25519, this is just the digest and does not contain
// the public key within it. Instead call PublicKey() or better, call KeyPair()
// which stores the precomputed public key.
type PrivateKey []byte
// PublicKey is a 32-byte Ed25519 public key.
type PublicKey []byte
// FromCryptoPrivateKey converts a Go private key to the one in this package.
func FromCryptoPrivateKey(key ed25519.PrivateKey) PrivateKey {
func FromCryptoPrivateKey(key ed25519.PrivateKey) KeyPair {
digest := sha512.Sum512(key[:32])
digest[0] &= 248
digest[31] &= 127
digest[31] |= 64
return digest[:]
return &precomputedKeyPair{PrivateKeyBytes: digest[:], PublicKeyBytes: PublicKey(key[32:])}
}
// FromCryptoPublicKey converts a Go public key to the one in this package.
@ -38,16 +48,24 @@ func FromCryptoPublicKey(key ed25519.PublicKey) PublicKey {
return PublicKey(key)
}
// KeyPair returns a new key pair with the public key precomputed.
func (p PrivateKey) KeyPair() KeyPair {
return &precomputedKeyPair{PrivateKeyBytes: p, PublicKeyBytes: p.PublicKey()}
}
// PrivateKey simply returns itself. Implements KeyPair.PrivateKey.
func (p PrivateKey) PrivateKey() PrivateKey { return p }
// Public simply delegates to PublicKey() to satisfy crypto.Signer. This method
// does a bit more work than the traditional Go ed25519's private key's Public()
// method so developers are encouraged to reuse the result.
func (p PrivateKey) Public() crypto.PublicKey {
return p.PublicKey()
}
// method so developers are encouraged to reuse the result or use KeyPair()
// which stores this value.
func (p PrivateKey) Public() crypto.PublicKey { return p.PublicKey() }
// PublicKey generates a public key for this private key. This method does a bit
// more work than the traditional Go ed25519's private key's Public() method so
// developers are encouraged to reuse the result.
// developers are encouraged to reuse the result or use KeyPair() which stores
// this value. Implements KeyPair.PublicKey.
func (p PrivateKey) PublicKey() PublicKey {
var A edwards25519.ExtendedGroupElement
var hBytes [32]byte
@ -58,28 +76,114 @@ func (p PrivateKey) PublicKey() PublicKey {
return publicKeyBytes[:]
}
// Sign is not yet implemented.
func (p PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) {
// Sign signs the given message with priv. Ed25519 performs two passes over
// messages to be signed and therefore cannot handle pre-hashed messages. Thus
// opts.HashFunc() must return zero to indicate the message hasn't been hashed.
// This can be achieved by passing crypto.Hash(0) as the value for opts.
func (p PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error) {
if opts.HashFunc() != crypto.Hash(0) {
return nil, errors.New("ed25519: cannot sign hashed message")
}
panic("TODO")
return Sign(p, message), nil
}
// Verify simply calls PublicKey().Verify(). Callers are encouraged to instead
// store a precomputed KeyPair (via KeyPair() or GenerateKey()) and call Verify
// on that.
func (p PrivateKey) Verify(message []byte, sig []byte) bool {
return p.PublicKey().Verify(message, sig)
}
// Verify simply calls the package-level function Verify().
func (p PublicKey) Verify(message []byte, sig []byte) bool {
return Verify(p, message, sig)
}
// KeyPair is an interface for types with both keys. While PrivateKey does
// implement this, it generates the PublicKey on demand. For better performance,
// use the result of GenerateKey directly or call PrivateKey.KeyPair().
type KeyPair interface {
crypto.Signer
PrivateKey() PrivateKey
PublicKey() PublicKey
Verify(message []byte, sig []byte) bool
}
type precomputedKeyPair struct {
PrivateKeyBytes PrivateKey
PublicKeyBytes PublicKey
}
func (p *precomputedKeyPair) PrivateKey() PrivateKey { return p.PrivateKeyBytes }
func (p *precomputedKeyPair) PublicKey() PublicKey { return p.PublicKeyBytes }
func (p *precomputedKeyPair) Public() crypto.PublicKey { return p.PublicKey() }
func (p *precomputedKeyPair) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) ([]byte, error) {
if opts.HashFunc() != crypto.Hash(0) {
return nil, errors.New("ed25519: cannot sign hashed message")
}
return Sign(p, message), nil
}
func (p *precomputedKeyPair) Verify(message []byte, sig []byte) bool {
return p.PublicKeyBytes.Verify(message, sig)
}
// GenerateKey generates a public/private key pair using entropy from rand.
// If rand is nil, crypto/rand.Reader will be used.
func GenerateKey(rnd io.Reader) (publicKey PublicKey, privateKey PrivateKey, err error) {
func GenerateKey(rnd io.Reader) (KeyPair, error) {
if rnd == nil {
rnd = rand.Reader
}
_, err = io.ReadFull(rnd, privateKey[:32])
if err == nil {
digest := sha512.Sum512(privateKey[:32])
digest[0] &= 248
digest[31] &= 127
digest[31] |= 64
privateKey = digest[:]
publicKey = privateKey.PublicKey()
rndByts := make([]byte, 32)
if _, err := io.ReadFull(rnd, rndByts); err != nil {
return nil, err
}
return
digest := sha512.Sum512(rndByts)
digest[0] &= 248
digest[31] &= 127
digest[31] |= 64
return PrivateKey(digest[:]).KeyPair(), nil
}
// Sign signs the message with the given key pair.
func Sign(keyPair KeyPair, message []byte) []byte {
// Ref: https://stackoverflow.com/questions/44810708/ed25519-public-result-is-different
var privateKeyA [32]byte
copy(privateKeyA[:], keyPair.PrivateKey()) // we need this in an array later
var messageDigest, hramDigest [64]byte
h := sha512.New()
h.Write(keyPair.PrivateKey()[32:])
h.Write(message)
h.Sum(messageDigest[:0])
var messageDigestReduced [32]byte
edwards25519.ScReduce(&messageDigestReduced, &messageDigest)
var R edwards25519.ExtendedGroupElement
edwards25519.GeScalarMultBase(&R, &messageDigestReduced)
var encodedR [32]byte
R.ToBytes(&encodedR)
h.Reset()
h.Write(encodedR[:])
h.Write(keyPair.PublicKey())
h.Write(message)
h.Sum(hramDigest[:0])
var hramDigestReduced [32]byte
edwards25519.ScReduce(&hramDigestReduced, &hramDigest)
var s [32]byte
edwards25519.ScMulAdd(&s, &hramDigestReduced, &privateKeyA, &messageDigestReduced)
signature := make([]byte, 64)
copy(signature[:], encodedR[:])
copy(signature[32:], s[:])
return signature
}
// Verify verifies a signed message.
func Verify(p PublicKey, message []byte, sig []byte) bool {
return ed25519.Verify(ed25519.PublicKey(p), message, sig)
}

View File

@ -0,0 +1,206 @@
package ed25519
import (
"bufio"
"bytes"
"compress/gzip"
"crypto"
"crypto/rand"
"encoding/hex"
"os"
"strings"
"testing"
"github.com/cretz/bine/torutil/ed25519/internal/edwards25519"
)
// Taken from https://github.com/golang/crypto/blob/1a580b3eff7814fc9b40602fd35256c63b50f491/ed25519/ed25519_test.go
type zeroReader struct{}
func (zeroReader) Read(buf []byte) (int, error) {
for i := range buf {
buf[i] = 0
}
return len(buf), nil
}
func TestUnmarshalMarshal(t *testing.T) {
pair, _ := GenerateKey(rand.Reader)
var A edwards25519.ExtendedGroupElement
var pubBytes [32]byte
copy(pubBytes[:], pair.PublicKey())
if !A.FromBytes(&pubBytes) {
t.Fatalf("ExtendedGroupElement.FromBytes failed")
}
var pub2 [32]byte
A.ToBytes(&pub2)
if pubBytes != pub2 {
t.Errorf("FromBytes(%v)->ToBytes does not round-trip, got %x\n", pubBytes, pub2)
}
}
func TestSignVerify(t *testing.T) {
var zero zeroReader
pair, _ := GenerateKey(zero)
message := []byte("test message")
sig := Sign(pair, message)
if !Verify(pair.PublicKey(), message, sig) {
t.Errorf("valid signature rejected")
}
wrongMessage := []byte("wrong message")
if Verify(pair.PublicKey(), wrongMessage, sig) {
t.Errorf("signature of different message accepted")
}
}
func TestCryptoSigner(t *testing.T) {
var zero zeroReader
pair, _ := GenerateKey(zero)
signer := crypto.Signer(pair)
publicInterface := signer.Public()
public2, ok := publicInterface.(PublicKey)
if !ok {
t.Fatalf("expected PublicKey from Public() but got %T", publicInterface)
}
if !bytes.Equal(pair.PublicKey(), public2) {
t.Errorf("public keys do not match: original:%x vs Public():%x", pair.PublicKey(), public2)
}
message := []byte("message")
var noHash crypto.Hash
signature, err := signer.Sign(zero, message, noHash)
if err != nil {
t.Fatalf("error from Sign(): %s", err)
}
if !Verify(pair.PublicKey(), message, signature) {
t.Errorf("Verify failed on signature from Sign()")
}
}
func TestGolden(t *testing.T) {
// sign.input.gz is a selection of test cases from
// https://ed25519.cr.yp.to/python/sign.input
testDataZ, err := os.Open("testdata/sign.input.gz")
if err != nil {
t.Fatal(err)
}
defer testDataZ.Close()
testData, err := gzip.NewReader(testDataZ)
if err != nil {
t.Fatal(err)
}
defer testData.Close()
scanner := bufio.NewScanner(testData)
lineNo := 0
for scanner.Scan() {
lineNo++
line := scanner.Text()
parts := strings.Split(line, ":")
if len(parts) != 5 {
t.Fatalf("bad number of parts on line %d", lineNo)
}
privBytes, _ := hex.DecodeString(parts[0])
pubKey, _ := hex.DecodeString(parts[1])
msg, _ := hex.DecodeString(parts[2])
sig, _ := hex.DecodeString(parts[3])
// The signatures in the test vectors also include the message
// at the end, but we just want R and S.
sig = sig[:SignatureSize]
if l := len(pubKey); l != PublicKeySize {
t.Fatalf("bad public key length on line %d: got %d bytes", lineNo, l)
}
var otherPriv [PrivateKeySize]byte
copy(otherPriv[:], privBytes)
copy(otherPriv[32:], pubKey)
priv := FromCryptoPrivateKey(otherPriv[:])
sig2 := Sign(priv, msg)
if !bytes.Equal(sig, sig2[:]) {
t.Errorf("different signature result on line %d: %x vs %x", lineNo, sig, sig2)
}
if !Verify(pubKey, msg, sig2) {
t.Errorf("signature failed to verify on line %d", lineNo)
}
}
if err := scanner.Err(); err != nil {
t.Fatalf("error reading test data: %s", err)
}
}
func TestMalleability(t *testing.T) {
// https://tools.ietf.org/html/rfc8032#section-5.1.7 adds an additional test
// that s be in [0, order). This prevents someone from adding a multiple of
// order to s and obtaining a second valid signature for the same message.
msg := []byte{0x54, 0x65, 0x73, 0x74}
sig := []byte{
0x7c, 0x38, 0xe0, 0x26, 0xf2, 0x9e, 0x14, 0xaa, 0xbd, 0x05, 0x9a,
0x0f, 0x2d, 0xb8, 0xb0, 0xcd, 0x78, 0x30, 0x40, 0x60, 0x9a, 0x8b,
0xe6, 0x84, 0xdb, 0x12, 0xf8, 0x2a, 0x27, 0x77, 0x4a, 0xb0, 0x67,
0x65, 0x4b, 0xce, 0x38, 0x32, 0xc2, 0xd7, 0x6f, 0x8f, 0x6f, 0x5d,
0xaf, 0xc0, 0x8d, 0x93, 0x39, 0xd4, 0xee, 0xf6, 0x76, 0x57, 0x33,
0x36, 0xa5, 0xc5, 0x1e, 0xb6, 0xf9, 0x46, 0xb3, 0x1d,
}
publicKey := []byte{
0x7d, 0x4d, 0x0e, 0x7f, 0x61, 0x53, 0xa6, 0x9b, 0x62, 0x42, 0xb5,
0x22, 0xab, 0xbe, 0xe6, 0x85, 0xfd, 0xa4, 0x42, 0x0f, 0x88, 0x34,
0xb1, 0x08, 0xc3, 0xbd, 0xae, 0x36, 0x9e, 0xf5, 0x49, 0xfa,
}
if Verify(publicKey, msg, sig) {
t.Fatal("non-canonical signature accepted")
}
}
func BenchmarkKeyGeneration(b *testing.B) {
var zero zeroReader
for i := 0; i < b.N; i++ {
if _, err := GenerateKey(zero); err != nil {
b.Fatal(err)
}
}
}
func BenchmarkSigning(b *testing.B) {
var zero zeroReader
pair, err := GenerateKey(zero)
if err != nil {
b.Fatal(err)
}
message := []byte("Hello, world!")
b.ResetTimer()
for i := 0; i < b.N; i++ {
Sign(pair, message)
}
}
func BenchmarkVerification(b *testing.B) {
var zero zeroReader
pair, err := GenerateKey(zero)
if err != nil {
b.Fatal(err)
}
message := []byte("Hello, world!")
signature := Sign(pair, message)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Verify(pair.PublicKey(), message, signature)
}
}

BIN
torutil/ed25519/testdata/sign.input.gz vendored Normal file

Binary file not shown.

View File

@ -17,12 +17,12 @@ var serviceIDEncoding = base32.StdEncoding.WithPadding(base32.NoPadding)
// OnionServiceIDFromPrivateKey generates the onion service ID from the given
// private key. This panics if the private key is not a crypto/*rsa.PrivateKey
// or github.com/cretz/bine/torutil/ed25519.PrivateKey.
// or github.com/cretz/bine/torutil/ed25519.KeyPair.
func OnionServiceIDFromPrivateKey(key crypto.PrivateKey) string {
switch k := key.(type) {
case *rsa.PrivateKey:
return OnionServiceIDFromV2PublicKey(&k.PublicKey)
case ed25519.PrivateKey:
case ed25519.KeyPair:
return OnionServiceIDFromV3PublicKey(k.PublicKey())
}
panic(fmt.Sprintf("Unrecognized private key type: %T", key))