Skip to content
This repository has been archived by the owner on May 5, 2021. It is now read-only.

Commit

Permalink
Merge pull request #23 from oasisprotocol/yawning/feature/speccheck
Browse files Browse the repository at this point in the history
ed25519: Various behavior changes
  • Loading branch information
Yawning authored Oct 30, 2020
2 parents 65138ca + acecacf commit e591887
Show file tree
Hide file tree
Showing 12 changed files with 636 additions and 28 deletions.
2 changes: 2 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Copyright (c) 2019 Oasis Labs Inc. All rights reserved.
Copyright (c) 2020 Henry de Valence. All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
Expand Down
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
[godoc-badge]: https://godoc.org/github.com/oasisprotocol/ed25519?status.svg
[godoc-link]: https://godoc.org/github.com/oasisprotocol/ed25519

This package provides a drop-in replacement for `golang.org/x/crypto/ed25519`
with the aim to improve performance, primarily on systems with a low cost
`64 bit x 64 bit = 128 bit` multiply operation.
This package provides a mostly drop-in replacement for
`golang.org/x/crypto/ed25519` with the aim to improve performance,
primarily on systems with a low cost `64 bit x 64 bit = 128 bit` multiply
operation.

This implementation is derived from Andrew Moon's [ed25519-donna][1],
and is intended to be timing side-channel safe on [most architectures][2].
Expand All @@ -24,6 +25,7 @@ functionality.
* Batch signature verification.
* Faster X25519 key generation (`extra/x25519`).
* Support for RFC 8032 Ed25519ph, Ed25519ctx.
* Optional support for [ZIP-215][3] verification semantics.

#### Benchmarks

Expand All @@ -44,6 +46,23 @@ Batch verification on the same system takes approximately `5082764 ns`
to process a 64 signature batch using the `crypto/rand` entropy source
for roughly `79418 ns` per signature in the batch.

#### Verification semantics

As this is slightly different from upstream, and is a point of divergence
between many of the existing implementations, the verification semantics
are as follows:

* Both iterative and batch verification are cofactored.
* Small order A is rejected (Optional and default).
* Small order R is accepted.
* A signature's scalar component must be in canonical form (S < L).
* Non-canonical A is accepted.
* Non-canonical R is accepted.

Note that subtle differences here can lead to issues in certain use cases,
in particular systems that require distributed consensus. Historical
versions of this package used different semantics.

#### Notes

Most of the actual implementation is hidden in internal subpackages.
Expand Down Expand Up @@ -107,3 +126,4 @@ more of the stated reasons.

[1]: https://github.com/floodyberry/ed25519-donna
[2]: https://bearssl.org/ctmul.html
[3]: https://zips.z.cash/zip-0215
21 changes: 13 additions & 8 deletions batch_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
package ed25519

import (
"bytes"
cryptorand "crypto/rand"
"crypto/sha512"
"errors"
Expand Down Expand Up @@ -255,16 +254,17 @@ func multiScalarmultVartime(r *ge25519.Ge25519, heap *batchHeap, count int) {

func isNeutralVartime(p *ge25519.Ge25519) bool {
// static int ge25519_is_neutral_vartime(const ge25519 *p)
var zero [32]byte
var pointBuffer [3][32]byte
curve25519.Contract(pointBuffer[0][:], p.X())
curve25519.Contract(pointBuffer[1][:], p.Y())
curve25519.Contract(pointBuffer[2][:], p.Z())
if testBatchSaveY {
// Save off the final Y coord if we are testing the batch verification.
copy(testBatchY[:], pointBuffer[1][:])
curve25519.Contract(testBatchY[:], p.Y())
}
return bytes.Equal(pointBuffer[0][:], zero[:]) && bytes.Equal(pointBuffer[1][:], pointBuffer[2][:])

// Multiply by the cofactor.
var q ge25519.Ge25519
ge25519.CofactorMultiply(&q, p)

// Check against the identity point (neutral element).
return ge25519.IsNeutralVartime(&q)
}

// VerifyBatch reports whether sigs are valid signatures of messages by
Expand Down Expand Up @@ -368,6 +368,11 @@ func VerifyBatch(rand io.Reader, publicKeys []PublicKey, messages, sigs [][]byte
failBatch(i + offset)
break
}
// Reject small order A to make the scheme strongly binding.
if !opts.ZIP215Verify && isSmallOrderVartime(publicKeys[i+offset]) {
failBatch(i + offset)
break
}

// The message should be sized corectly if this is Ed25519ph.
msg := messages[i+offset]
Expand Down
66 changes: 55 additions & 11 deletions ed25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ type Options struct {
// Warning: If Hash is crypto.Hash(0) and Context is a zero length
// string, plain Ed25519 will be used instead of Ed25519ctx.
Context string

// ZIP215Verify specifies that verification should follow Zcash's
// ZIP-215 semantics.
ZIP215Verify bool
}

// HashFunc returns an identifier for the hash function used to produce
Expand Down Expand Up @@ -199,6 +203,16 @@ func (pub PublicKey) Equal(x crypto.PublicKey) bool {
return bytes.Equal(pub, xx)
}

// IsSmallOrder returns true iff a Public Key is a small order point.
// This routine will panic if the public key length is invalid.
func (pub PublicKey) IsSmallOrder() bool {
if l := len(pub); l != PublicKeySize {
panic("ed25519: bad public key length: " + strconv.Itoa(l))
}

return isSmallOrderVartime(pub)
}

// Sign signs the message with privateKey and returns a signature. It will
// panic if len(privateKey) is not PrivateKeySize.
func Sign(privateKey PrivateKey, message []byte) []byte {
Expand Down Expand Up @@ -272,19 +286,23 @@ func sign(privateKey PrivateKey, message []byte, f dom2Flag, c []byte) []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, fPure, nil)
return verify(publicKey, message, sig, fPure, nil, false)
}

func verify(publicKey PublicKey, message, sig []byte, f dom2Flag, c []byte) bool {
func verify(publicKey PublicKey, message, sig []byte, f dom2Flag, c []byte, zip215 bool) bool {
if l := len(publicKey); l != PublicKeySize {
panic("ed25519: bad public key length: " + strconv.Itoa(l))
}

// Reject small order A to make the scheme strongly binding.
if !zip215 && isSmallOrderVartime(publicKey) {
return false
}

var (
hash [64]byte
checkR [32]byte
R, A ge25519.Ge25519
hram, S modm.Bignum256
hash [64]byte
Rproj, R, A, checkR ge25519.Ge25519
hram, S modm.Bignum256
)

if len(sig) != SignatureSize || (sig[63]&224 != 0) || !ge25519.UnpackNegativeVartime(&A, publicKey) {
Expand All @@ -308,15 +326,19 @@ func verify(publicKey PublicKey, message, sig []byte, f dom2Flag, c []byte) bool
return false
}

if !ge25519.UnpackVartime(&checkR, sig[:32]) {
return false
}

// S
modm.Expand(&S, sig[32:])

// SB - H(R,A,m)A
ge25519.DoubleScalarmultVartime(&R, &A, &hram, &S)
ge25519.Pack(checkR[:], &R)
ge25519.DoubleScalarmultVartime(&Rproj, &A, &hram, &S)
ge25519.ProjectiveToExtended(&R, &Rproj)

// check that R = SB - H(R,A,m)A
return bytes.Equal(checkR[:], sig[:32])
// check that [8](R - (SB - H(R,A,m)A)) == 0
return ge25519.CofactorEqual(&R, &checkR)
}

// VerifyWithOptions reports whether sig is a valid Ed25519 signature by
Expand Down Expand Up @@ -351,7 +373,7 @@ func verifyWithOptionsNoPanic(publicKey PublicKey, message, sig []byte, opts *Op
return false, errors.New("ed25519: bad public key length: " + strconv.Itoa(l))
}

return verify(publicKey, message, sig, f, context), nil
return verify(publicKey, message, sig, f, context, opts.ZIP215Verify), nil
}

// NewKeyFromSeed calculates a private key from a seed. It will panic if
Expand Down Expand Up @@ -426,6 +448,16 @@ var order = [4]uint64{0x5812631a5cf5d3ed, 0x14def9dea2f79cd6, 0, 0x1000000000000
// scMinimal returns true if the given scalar is less than the order of the
// curve.
func scMinimal(scalar []byte) bool {
if scalar[31]&240 == 0 {
// 4 most significant bits unset, succeed fast
return true
}
if scalar[31]&244 != 0 {
// Any of the 3 most significant bits set, fail fast
return false
}

// 4th most significant bit set (unlikely), actually check vs order
for i := 3; ; i-- {
v := binary.LittleEndian.Uint64(scalar[i*8:])
if v > order[i] {
Expand All @@ -440,6 +472,18 @@ func scMinimal(scalar []byte) bool {
return true
}

func isSmallOrderVartime(s []byte) bool {
var t1, t2 ge25519.Ge25519

if !ge25519.UnpackVartime(&t1, s) {
panic("ed25519/isSmallOrderVartime: failed to unpack")
}

ge25519.CofactorMultiply(&t2, &t1)

return ge25519.IsNeutralVartime(&t2)
}

type dom2Flag byte

const (
Expand Down
7 changes: 1 addition & 6 deletions extra/x25519/x25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,13 +198,8 @@ func edwardsToMontgomeryX(outX, y *curve25519.Bignum25519) {
// EdPublicKeyToX25519 converts an Ed25519 public key into the X25519 public
// key that would be generated from the same private key.
func EdPublicKeyToX25519(publicKey ed25519.PublicKey) ([]byte, bool) {
// Negate a copy of the public key, due to UnpackNegativeVartime.
var pkCopy [32]byte
copy(pkCopy[:], publicKey)
pkCopy[31] ^= (1 << 7)

var A ge25519.Ge25519
if !ge25519.UnpackNegativeVartime(&A, pkCopy[:]) {
if !ge25519.UnpackVartime(&A, publicKey[:]) {
return nil, false
}

Expand Down
116 changes: 116 additions & 0 deletions internal/ge25519/cofactor_equal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) 2009 The Go Authors. All rights reserved.
// Copyright (c) 2020 Henry de Valence. All rights reserved.
// Copyright (c) 2020 Oasis Labs Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

package ge25519

import (
"bytes"

"github.com/oasisprotocol/ed25519/internal/curve25519"
)

// At some point to reduce my frustration and increase my sanity, I should
// just port ed25519-dalek to Go or something, instead of having to deal
// with a mess of ref10 derivatives every single time.
//
// For now, shamelessly steal the CofactorEqual routine from:
// https://github.com/hdevalence/ed25519consensus/blob/main/internal/edwards25519/edwards25519.go

func geSub(r *ge25519p1p1, p *Ge25519, q *ge25519pniels) {
var t0 curve25519.Bignum25519

curve25519.Add(&r.x, &p.y, &p.x)
curve25519.Sub(&r.y, &p.y, &p.x)
curve25519.Mul(&r.z, &r.x, &q.ysubx)
curve25519.Mul(&r.y, &r.y, &q.xaddy)
curve25519.Mul(&r.t, &q.t2d, &p.t)
curve25519.Mul(&r.x, &p.z, &q.z)
curve25519.Add(&t0, &r.x, &r.x)
curve25519.Sub(&r.x, &r.z, &r.y)
curve25519.Add(&r.y, &r.z, &r.y)
curve25519.Sub(&r.z, &t0, &r.t)
curve25519.Add(&r.t, &t0, &r.t)
}

// ProjectiveToExtended converts p from a projective group element to an
// extended group element.
func ProjectiveToExtended(r, p *Ge25519) {
curve25519.Mul(&r.x, &p.x, &p.z)
curve25519.Mul(&r.y, &p.y, &p.z)
curve25519.Square(&r.z, &p.z)
curve25519.Mul(&r.t, &p.x, &p.y)
}

// CofactorEqual checks whether p, q are equal up to cofactor multiplication
// (ie. if their difference is of small order).
func CofactorEqual(p, q *Ge25519) bool {
var t1 ge25519pniels
var t2 ge25519p1p1
var t3 Ge25519

fullToPniels(&t1, q)
geSub(&t2, p, &t1) // t2 = (P - Q)
p1p1ToFull(&t3, &t2) // t3 = (P - Q)
CofactorMultiply(&t3, &t3) // t3 = [8](P-Q)

// Now we want to check whether the point t3 is the identity.
return IsNeutralVartime(&t3)
}

// CofactorMultiply multiplies the full group element by the cofactor (8).
func CofactorMultiply(r, p *Ge25519) {
var t1 ge25519p1p1
var t2 Ge25519

doubleP1p1(&t1, p) // t1 = [2]P
p1p1ToFull(&t2, &t1) // t2 = [2]P
doubleP1p1(&t1, &t2) // t1 = [4]P
p1p1ToFull(&t2, &t1) // t2 = [4]P
doubleP1p1(&t1, &t2) // t1 = [8]P
p1p1ToFull(r, &t1) // r = [8]P
}

// IsNeutralVartime returns true iff the q is the identity point.
func IsNeutralVartime(q *Ge25519) bool {
// In projective coordinates this is (X:Y:Z) ~ (0:1:0)
// ie. X/Z = 0, Y/Z = 1
// <=> X = 0, Y = Z

var zero [32]byte
var xBytes [32]byte
var yBytes [32]byte
var zBytes [32]byte

curve25519.Contract(xBytes[:], &q.x)
curve25519.Contract(yBytes[:], &q.y)
curve25519.Contract(zBytes[:], &q.z)

return bytes.Equal(zero[:], xBytes[:]) && bytes.Equal(yBytes[:], zBytes[:])
}
9 changes: 9 additions & 0 deletions internal/ge25519/ge25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,15 @@ func UnpackNegativeVartime(r *Ge25519, p []byte) bool {
return true
}

func UnpackVartime(r *Ge25519, p []byte) bool {
// Negate a copy of p, so we can call UnpackNegativeVartime.
var pCopy [32]byte
copy(pCopy[:], p)
pCopy[31] ^= (1 << 7)

return UnpackNegativeVartime(r, pCopy[:])
}

//
// scalarmults
//
Expand Down
Loading

0 comments on commit e591887

Please sign in to comment.