]> Cypherpunks.ru repositories - gostls13.git/commitdiff
crypto/ed25519: implement Ed25519ph in Sign and VerifyWithOptions
authorFilippo Valsorda <filippo@golang.org>
Thu, 5 May 2022 13:23:44 +0000 (09:23 -0400)
committerFilippo Valsorda <filippo@golang.org>
Mon, 24 Oct 2022 12:11:20 +0000 (12:11 +0000)
Updates #31804

Change-Id: I5a48dfc57401576902674aff20b557e4a8ce8ab8
Reviewed-on: https://go-review.googlesource.com/c/go/+/373076
Reviewed-by: Filippo Valsorda <valsorda@google.com>
Reviewed-by: Michael Knyszek <mknyszek@google.com>
Run-TryBot: Filippo Valsorda <filippo@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Katie Hockman <katie@golang.org>
Reviewed-by: David Chase <drchase@google.com>
api/next/31804.txt [new file with mode: 0644]
src/crypto/ed25519/ed25519.go
src/crypto/ed25519/ed25519_test.go

diff --git a/api/next/31804.txt b/api/next/31804.txt
new file mode 100644 (file)
index 0000000..e5968c8
--- /dev/null
@@ -0,0 +1,4 @@
+pkg crypto/ed25519, func VerifyWithOptions(PublicKey, []uint8, []uint8, *Options) error #31804
+pkg crypto/ed25519, method (*Options) HashFunc() crypto.Hash #31804
+pkg crypto/ed25519, type Options struct #31804
+pkg crypto/ed25519, type Options struct, Hash crypto.Hash #31804
index 601da50a1afc03165cff3e7c5eeed6c3cf0b1e75..cb6b293d475f95e0d30a1e29d31e703828394d6c 100644 (file)
@@ -75,19 +75,39 @@ func (priv PrivateKey) Seed() []byte {
        return bytes.Clone(priv[:SeedSize])
 }
 
-// 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.
+// Sign signs the given message with priv. rand is ignored. If opts.HashFunc()
+// is crypto.SHA512, the pre-hashed variant Ed25519ph is used and message is
+// expected to be a SHA-512 hash, otherwise opts.HashFunc() must be
+// crypto.Hash(0) and the message must not be hashed, as Ed25519 performs two
+// passes over messages to be signed.
 func (priv PrivateKey) Sign(rand io.Reader, message []byte, opts crypto.SignerOpts) (signature []byte, err error) {
-       if opts.HashFunc() != crypto.Hash(0) {
-               return nil, errors.New("ed25519: cannot sign hashed message")
+       switch opts.HashFunc() {
+       case crypto.SHA512:
+               if l := len(message); l != sha512.Size {
+                       return nil, errors.New("ed25519: bad Ed25519ph message hash length: " + strconv.Itoa(l))
+               }
+               signature := make([]byte, SignatureSize)
+               sign(signature, priv, message, domPrefixPh)
+               return signature, nil
+       case crypto.Hash(0):
+               return Sign(priv, message), nil
+       default:
+               return nil, errors.New("ed25519: expected opts zero (unhashed message, for standard Ed25519) or SHA-512 (for Ed25519ph)")
        }
+}
+
+// Options can be used with PrivateKey.Sign or VerifyWithOptions
+// to select Ed25519 variants.
+type Options struct {
+       // Hash can be zero for regular Ed25519, or crypto.SHA512 for Ed25519ph.
+       Hash crypto.Hash
 
-       return Sign(priv, message), nil
+       // TODO(filippo): add Context, a string of at most 255 bytes which when
+       // non-zero selects Ed25519ctx.
 }
 
+func (o *Options) HashFunc() crypto.Hash { return o.Hash }
+
 // GenerateKey generates a public/private key pair using entropy from rand.
 // If rand is nil, crypto/rand.Reader will be used.
 func GenerateKey(rand io.Reader) (PublicKey, PrivateKey, error) {
@@ -142,11 +162,20 @@ func Sign(privateKey PrivateKey, message []byte) []byte {
        // Outline the function body so that the returned signature can be
        // stack-allocated.
        signature := make([]byte, SignatureSize)
-       sign(signature, privateKey, message)
+       sign(signature, privateKey, message, domPrefixPure)
        return signature
 }
 
-func sign(signature, privateKey, message []byte) {
+// Domain separation prefixes used to disambiguate Ed25519/Ed25519ph.
+// See RFC 8032, Section 2 and Section 5.1.
+const (
+       // domPrefixPure is empty for pure Ed25519.
+       domPrefixPure = ""
+       // domPrefixPh is dom2(phflag=1, context="") for Ed25519ph.
+       domPrefixPh = "SigEd25519 no Ed25519 collisions\x01\x00"
+)
+
+func sign(signature, privateKey, message []byte, domPrefix string) {
        if l := len(privateKey); l != PrivateKeySize {
                panic("ed25519: bad private key length: " + strconv.Itoa(l))
        }
@@ -160,6 +189,7 @@ func sign(signature, privateKey, message []byte) {
        prefix := h[32:]
 
        mh := sha512.New()
+       mh.Write([]byte(domPrefix))
        mh.Write(prefix)
        mh.Write(message)
        messageDigest := make([]byte, 0, sha512.Size)
@@ -172,6 +202,7 @@ func sign(signature, privateKey, message []byte) {
        R := (&edwards25519.Point{}).ScalarBaseMult(r)
 
        kh := sha512.New()
+       kh.Write([]byte(domPrefix))
        kh.Write(R.Bytes())
        kh.Write(publicKey)
        kh.Write(message)
@@ -191,6 +222,36 @@ func sign(signature, privateKey, message []byte) {
 // Verify reports whether sig is a valid signature of message by publicKey. It
 // will panic if len(publicKey) is not PublicKeySize.
 func Verify(publicKey PublicKey, message, sig []byte) bool {
+       return verify(publicKey, message, sig, domPrefixPure)
+}
+
+// VerifyWithOptions reports whether sig is a valid signature of message by
+// publicKey. A valid signature is indicated by returning a nil error.
+// If opts.HashFunc() is crypto.SHA512, the pre-hashed variant Ed25519ph is used
+// and message is expected to be a SHA-512 hash, otherwise opts.HashFunc() must
+// be crypto.Hash(0) and the message must not be hashed, as Ed25519 performs two
+// passes over messages to be signed.
+func VerifyWithOptions(publicKey PublicKey, message, sig []byte, opts *Options) error {
+       switch opts.HashFunc() {
+       case crypto.SHA512:
+               if l := len(message); l != sha512.Size {
+                       return errors.New("ed25519: bad Ed25519ph message hash length: " + strconv.Itoa(l))
+               }
+               if !verify(publicKey, message, sig, domPrefixPh) {
+                       return errors.New("ed25519: invalid signature")
+               }
+               return nil
+       case crypto.Hash(0):
+               if !verify(publicKey, message, sig, domPrefixPure) {
+                       return errors.New("ed25519: invalid signature")
+               }
+               return nil
+       default:
+               return errors.New("ed25519: expected opts zero (unhashed message, for standard Ed25519) or SHA-512 (for Ed25519ph)")
+       }
+}
+
+func verify(publicKey PublicKey, message, sig []byte, domPrefix string) bool {
        if l := len(publicKey); l != PublicKeySize {
                panic("ed25519: bad public key length: " + strconv.Itoa(l))
        }
@@ -205,6 +266,7 @@ func Verify(publicKey PublicKey, message, sig []byte) bool {
        }
 
        kh := sha512.New()
+       kh.Write([]byte(domPrefix))
        kh.Write(sig[:32])
        kh.Write(publicKey)
        kh.Write(message)
index 2e7fe23025bb304872373bca50073744f891e762..fbd4982cc2de2d4634290557fd599c5678b8110f 100644 (file)
@@ -11,6 +11,7 @@ import (
        "crypto"
        "crypto/internal/boring"
        "crypto/rand"
+       "crypto/sha512"
        "encoding/hex"
        "internal/testenv"
        "os"
@@ -43,6 +44,49 @@ func TestSignVerify(t *testing.T) {
        }
 }
 
+func TestSignVerifyHashed(t *testing.T) {
+       // From RFC 8032, Section 7.3
+       key, _ := hex.DecodeString("833fe62409237b9d62ec77587520911e9a759cec1d19755b7da901b96dca3d42ec172b93ad5e563bf4932c70e1245034c35467ef2efd4d64ebf819683467e2bf")
+       expectedSig, _ := hex.DecodeString("98a70222f0b8121aa9d30f813d683f809e462b469c7ff87639499bb94e6dae4131f85042463c2a355a2003d062adf5aaa10b8c61e636062aaad11c2a26083406")
+       message, _ := hex.DecodeString("616263")
+
+       private := PrivateKey(key)
+       public := private.Public().(PublicKey)
+       hash := sha512.Sum512(message)
+       sig, err := private.Sign(nil, hash[:], crypto.SHA512)
+       if err != nil {
+               t.Fatal(err)
+       }
+       if !bytes.Equal(sig, expectedSig) {
+               t.Error("signature doesn't match test vector")
+       }
+       sig, err = private.Sign(nil, hash[:], &Options{Hash: crypto.SHA512})
+       if err != nil {
+               t.Fatal(err)
+       }
+       if !bytes.Equal(sig, expectedSig) {
+               t.Error("signature doesn't match test vector")
+       }
+       if err := VerifyWithOptions(public, hash[:], sig, &Options{Hash: crypto.SHA512}); err != nil {
+               t.Errorf("valid signature rejected: %v", err)
+       }
+
+       wrongHash := sha512.Sum512([]byte("wrong message"))
+       if VerifyWithOptions(public, wrongHash[:], sig, &Options{Hash: crypto.SHA512}) == nil {
+               t.Errorf("signature of different message accepted")
+       }
+
+       sig[0] ^= 0xff
+       if VerifyWithOptions(public, hash[:], sig, &Options{Hash: crypto.SHA512}) == nil {
+               t.Errorf("invalid signature accepted")
+       }
+       sig[0] ^= 0xff
+       sig[SignatureSize-1] ^= 0xff
+       if VerifyWithOptions(public, hash[:], sig, &Options{Hash: crypto.SHA512}) == nil {
+               t.Errorf("invalid signature accepted")
+       }
+}
+
 func TestCryptoSigner(t *testing.T) {
        var zero zeroReader
        public, private, _ := GenerateKey(zero)
@@ -66,6 +110,14 @@ func TestCryptoSigner(t *testing.T) {
                t.Fatalf("error from Sign(): %s", err)
        }
 
+       signature2, err := signer.Sign(zero, message, &Options{Hash: noHash})
+       if err != nil {
+               t.Fatalf("error from Sign(): %s", err)
+       }
+       if !bytes.Equal(signature, signature2) {
+               t.Errorf("signatures keys do not match")
+       }
+
        if !Verify(public, message, signature) {
                t.Errorf("Verify failed on signature from Sign()")
        }