Attention is currently required from: Katie Hockman, Jonathan Rudenberg.
Filippo Valsorda would like Katie Hockman and Jonathan Rudenberg to review this change.
crypto/ed25519: implement Ed25519ph in Sign and VerifyWithOptions
Fixes #31804
Change-Id: I5a48dfc57401576902674aff20b557e4a8ce8ab8
---
M src/crypto/ed25519/ed25519_test.go
M src/crypto/ed25519/ed25519.go
2 files changed, 136 insertions(+), 11 deletions(-)
diff --git a/src/crypto/ed25519/ed25519.go b/src/crypto/ed25519/ed25519.go
index 09c5269..f76443e 100644
--- a/src/crypto/ed25519/ed25519.go
+++ b/src/crypto/ed25519/ed25519.go
@@ -77,19 +77,39 @@
return seed
}
-// 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)")
}
-
- return Sign(priv, message), nil
}
+// 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
+
+ // 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) {
@@ -141,11 +161,20 @@
// 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))
}
@@ -156,6 +185,7 @@
prefix := h[32:]
mh := sha512.New()
+ mh.Write([]byte(domPrefix))
mh.Write(prefix)
mh.Write(message)
messageDigest := make([]byte, 0, sha512.Size)
@@ -165,6 +195,7 @@
R := (&edwards25519.Point{}).ScalarBaseMult(r)
kh := sha512.New()
+ kh.Write([]byte(domPrefix))
kh.Write(R.Bytes())
kh.Write(publicKey)
kh.Write(message)
@@ -181,6 +212,36 @@
// 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))
}
@@ -195,6 +256,7 @@
}
kh := sha512.New()
+ kh.Write([]byte(domPrefix))
kh.Write(sig[:32])
kh.Write(publicKey)
kh.Write(message)
diff --git a/src/crypto/ed25519/ed25519_test.go b/src/crypto/ed25519/ed25519_test.go
index 8a973b3..80bce54 100644
--- a/src/crypto/ed25519/ed25519_test.go
+++ b/src/crypto/ed25519/ed25519_test.go
@@ -10,6 +10,7 @@
"compress/gzip"
"crypto"
"crypto/rand"
+ "crypto/sha512"
"encoding/hex"
"os"
"strings"
@@ -41,6 +42,49 @@
}
}
+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)
@@ -64,6 +108,14 @@
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()")
}
To view, visit change 373076. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Katie Hockman, Jonathan Rudenberg, Filippo Valsorda.
Filippo Valsorda uploaded patch set #3 to this change.
crypto/ed25519: implement Ed25519ph in Sign and VerifyWithOptions
Fixes #31804
Change-Id: I5a48dfc57401576902674aff20b557e4a8ce8ab8
---
M api/next.txt
M src/crypto/ed25519/ed25519_test.go
M src/crypto/ed25519/ed25519.go
3 files changed, 140 insertions(+), 11 deletions(-)
To view, visit change 373076. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Jonathan Rudenberg, Filippo Valsorda.
Patch set 3:Run-TryBot +1Code-Review +2Trust +1
1 comment:
File src/crypto/ed25519/ed25519.go:
// TODO(filippo): add Context, a string of at most 255 bytes which when
// non-zero selects Ed25519ctx.
Maybe link to an issue instead of you individually? Is there one for this specific piece?
To view, visit change 373076. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Filippo Valsorda, Jonathan Rudenberg, Katie Hockman.
Filippo Valsorda uploaded patch set #4 to this change.
crypto/ed25519: implement Ed25519ph in Sign and VerifyWithOptions
Updates #31804
Change-Id: I5a48dfc57401576902674aff20b557e4a8ce8ab8
---
A api/next/31804.txt
M src/crypto/ed25519/ed25519.go
M src/crypto/ed25519/ed25519_test.go
3 files changed, 140 insertions(+), 11 deletions(-)
To view, visit change 373076. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Filippo Valsorda, Jonathan Rudenberg, Katie Hockman.
1 comment:
File src/crypto/ed25519/ed25519.go:
// TODO(filippo): add Context, a string of at most 255 bytes which when
// non-zero selects Ed25519ctx.
Maybe link to an issue instead of you individually? Is there one for this specific piece?
Addressing this in a follow-up CL.
To view, visit change 373076. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Filippo Valsorda, Jonathan Rudenberg, Katie Hockman.
Patch set 4:Code-Review +1
Attention is currently required from: Filippo Valsorda, Jonathan Rudenberg, Katie Hockman.
Patch set 4:Code-Review +1
To view, visit change 373076. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Filippo Valsorda, Jonathan Rudenberg, Katie Hockman.
Patch set 4:Code-Review +1
To view, visit change 373076. To unsubscribe, or for help writing mail filters, visit settings.
Attention is currently required from: Filippo Valsorda, Jonathan Rudenberg, Katie Hockman.
Patch set 4:Code-Review +2
Filippo Valsorda submitted this change.
crypto/ed25519: implement Ed25519ph in Sign and VerifyWithOptions
Updates #31804
Change-Id: I5a48dfc57401576902674aff20b557e4a8ce8ab8
Reviewed-on: https://go-review.googlesource.com/c/go/+/373076
Reviewed-by: Filippo Valsorda <vals...@google.com>
Reviewed-by: Michael Knyszek <mkny...@google.com>
Run-TryBot: Filippo Valsorda <fil...@golang.org>
Reviewed-by: Roland Shoemaker <rol...@golang.org>
TryBot-Result: Gopher Robot <go...@golang.org>
Reviewed-by: Katie Hockman <ka...@golang.org>
Reviewed-by: David Chase <drc...@google.com>
---
A api/next/31804.txt
M src/crypto/ed25519/ed25519.go
M src/crypto/ed25519/ed25519_test.go
3 files changed, 148 insertions(+), 11 deletions(-)
diff --git a/api/next/31804.txt b/api/next/31804.txt
new file mode 100644
index 0000000..e5968c8
--- /dev/null
+++ b/api/next/31804.txt
@@ -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
diff --git a/src/crypto/ed25519/ed25519.go b/src/crypto/ed25519/ed25519.go
index 601da50..cb6b293 100644
--- a/src/crypto/ed25519/ed25519.go
+++ b/src/crypto/ed25519/ed25519.go
@@ -75,19 +75,39 @@
return bytes.Clone(priv[:SeedSize])
@@ -142,11 +162,20 @@
// 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 @@
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 @@
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 @@@@ -205,6 +266,7 @@
}
kh := sha512.New()
+ kh.Write([]byte(domPrefix))
kh.Write(sig[:32])
kh.Write(publicKey)
kh.Write(message)
diff --git a/src/crypto/ed25519/ed25519_test.go b/src/crypto/ed25519/ed25519_test.go
index 2e7fe23..fbd4982 100644
--- a/src/crypto/ed25519/ed25519_test.go
+++ b/src/crypto/ed25519/ed25519_test.go
@@ -11,6 +11,7 @@
"crypto"
"crypto/internal/boring"
"crypto/rand"
+ "crypto/sha512"
"encoding/hex"
"internal/testenv"
"os"
@@ -43,6 +44,49 @@
@@ -66,6 +110,14 @@
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()")
}
To view, visit change 373076. To unsubscribe, or for help writing mail filters, visit settings.