From a1901f898bc05aac966edd247ff122f52fbb8d2e Mon Sep 17 00:00:00 2001 From: Filippo Valsorda Date: Thu, 5 May 2022 09:23:44 -0400 Subject: [PATCH] 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 Reviewed-by: Michael Knyszek Run-TryBot: Filippo Valsorda Reviewed-by: Roland Shoemaker TryBot-Result: Gopher Robot Reviewed-by: Katie Hockman Reviewed-by: David Chase --- api/next/31804.txt | 4 ++ src/crypto/ed25519/ed25519.go | 82 ++++++++++++++++++++++++++---- src/crypto/ed25519/ed25519_test.go | 52 +++++++++++++++++++ 3 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 api/next/31804.txt diff --git a/api/next/31804.txt b/api/next/31804.txt new file mode 100644 index 0000000000..e5968c8826 --- /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 601da50a1a..cb6b293d47 100644 --- a/src/crypto/ed25519/ed25519.go +++ b/src/crypto/ed25519/ed25519.go @@ -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) diff --git a/src/crypto/ed25519/ed25519_test.go b/src/crypto/ed25519/ed25519_test.go index 2e7fe23025..fbd4982cc2 100644 --- a/src/crypto/ed25519/ed25519_test.go +++ b/src/crypto/ed25519/ed25519_test.go @@ -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()") } -- 2.48.1