Files
threshold/pkg/math/curve/ristretto255_hash.go
zeekay d1cc69af98 Make the Schnorr challenge cover the message it signs
The signer and the verifier each built c = H(R || Y || m) by passing the
concatenation to curve.FromHash, which does not hash: it truncates its
input to the group order's byte length and reads it big-endian, the SECG
convention for an ECDSA digest. Y and m fell off the end, so the
challenge was a function of R alone and one signature verified under
every message.

The derivation now lives in one place and hashes, each piece length- and
domain-separated, with the digest read wide.
2026-09-01 19:31:18 -07:00

109 lines
3.8 KiB
Go

package curve
import (
"crypto/sha512"
"encoding/binary"
"errors"
r255 "github.com/gtank/ristretto255"
)
// Hashing a message to a GROUP ELEMENT, which is a different operation from
// hashing it to a scalar and is not interchangeable with one.
//
// FromHash gives a scalar s, and s·G is a point — but the caller then knows
// that point's discrete logarithm. Anything whose security rests on the caller
// NOT knowing it is broken by that substitution. An oblivious PRF is the clear
// case: F(k, x) = k·H(x) is a PRF the key holder gates, and a client that knows
// h with H(x) = h·G computes k·H(x) = h·(k·G) from the public key alone, for
// every x, without asking. The map below has no such shortcut: nobody learns a
// discrete logarithm from it.
// ErrHashLength is returned for a length expand_message_xmd cannot produce, or
// a domain separation tag it cannot carry: RFC 9380 §3.1 requires at least one
// byte, and DST' appends the tag's length as a single byte, so 255 is the most
// it can state.
var ErrHashLength = errors.New("curve: requested hash length out of range")
// expandXMD is expand_message_xmd with SHA-512 (RFC 9380 §5.3.1): a
// domain-separated extendable output built from a fixed-width hash.
//
// b_0 = H(Z_pad ‖ msg ‖ I2OSP(len,2) ‖ I2OSP(0,1) ‖ DST')
// b_1 = H(b_0 ‖ I2OSP(1,1) ‖ DST'), b_i = H(b_0 ⊕ b_{i-1} ‖ I2OSP(i,1) ‖ DST')
//
// DST' carries its own length, which is what keeps two different tags from
// producing the same stream.
func expandXMD(msg, dst []byte, length int) ([]byte, error) {
const bInBytes = sha512.Size // 64
const sInBytes = 128 // SHA-512 block
if length <= 0 || length > 255*bInBytes || len(dst) == 0 || len(dst) > 255 {
return nil, ErrHashLength
}
ell := (length + bInBytes - 1) / bInBytes
dstPrime := append(append([]byte{}, dst...), byte(len(dst)))
var lenBytes [2]byte
binary.BigEndian.PutUint16(lenBytes[:], uint16(length))
h := sha512.New()
h.Write(make([]byte, sInBytes)) // Z_pad
h.Write(msg)
h.Write(lenBytes[:])
h.Write([]byte{0})
h.Write(dstPrime)
b0 := h.Sum(nil)
h.Reset()
h.Write(b0)
h.Write([]byte{1})
h.Write(dstPrime)
bi := h.Sum(nil)
out := append([]byte{}, bi...)
for i := 2; i <= ell; i++ {
tmp := make([]byte, bInBytes)
for j := range tmp {
tmp[j] = b0[j] ^ bi[j]
}
h.Reset()
h.Write(tmp)
h.Write([]byte{byte(i)})
h.Write(dstPrime)
bi = h.Sum(nil)
out = append(out, bi...)
}
return out[:length], nil
}
// HashToPoint maps a message to a group element under a domain separation tag,
// as hash_to_ristretto255 (RFC 9380 §B): expand to 64 uniform bytes and apply
// the ristretto255 one-way map.
//
// Every tag gives an independent map, so two protocols sharing this group
// cannot be made to answer each other's questions.
func (Ristretto255) HashToPoint(dst, msg []byte) (Point, error) {
uniform, err := expandXMD(msg, dst, 64)
if err != nil {
return nil, err
}
return &Ristretto255Point{value: r255.NewElement().FromUniformBytes(uniform)}, nil
}
// HashToScalar maps a message to a scalar under a domain separation tag, as
// RFC 9497 §4.1 does: expand to 64 uniform bytes and reduce them wide.
//
// This is NOT FromHash. FromHash truncates the input to the order's byte length
// and reads it big-endian, which is the SECG/OpenSSL convention for ECDSA and
// is a different function from this one. A protocol that specifies the wide
// reduction — every RFC 9497 challenge and composite scalar does — gets a
// different scalar from FromHash for the same bytes, and would interoperate
// with nothing.
func (Ristretto255) HashToScalar(dst, msg []byte) (Scalar, error) {
uniform, err := expandXMD(msg, dst, 64)
if err != nil {
return nil, err
}
return &Ristretto255Scalar{value: r255.NewScalar().FromUniformBytes(uniform)}, nil
}