From 6115a89e14a33b45879b9c07e99854fe3bea253b Mon Sep 17 00:00:00 2001 From: Gary Belvin Date: Mon, 20 Apr 2020 10:01:02 +0100 Subject: [PATCH] SECG1 Encode/Decode (#1513) * SECG1 Encode/Decode Encode and Decode elliptic curve points in compressed format. This will be superceeded by https://github.com/golang/go/issues/34105 in Go 1.15 --- .../draft-irtf-cfrg-vrf-06/conversion.go | 120 ++++++++++++++++++ .../draft-irtf-cfrg-vrf-06/conversion_test.go | 58 +++++++++ 2 files changed, 178 insertions(+) create mode 100644 core/crypto/draft-irtf-cfrg-vrf-06/conversion.go create mode 100644 core/crypto/draft-irtf-cfrg-vrf-06/conversion_test.go diff --git a/core/crypto/draft-irtf-cfrg-vrf-06/conversion.go b/core/crypto/draft-irtf-cfrg-vrf-06/conversion.go new file mode 100644 index 000000000..fa1d3ee87 --- /dev/null +++ b/core/crypto/draft-irtf-cfrg-vrf-06/conversion.go @@ -0,0 +1,120 @@ +package vrf + +import ( + "bytes" + "crypto/elliptic" + "errors" + "math/big" +) + +// i2osp converts a nonnegative integer to an octet string of a specified length. +// RFC8017 section-4.1 (big endian representation) +func i2osp(x *big.Int, rLen uint) []byte { + // 1. If x >= 256^rLen, output "integer too large" and stop. + upperBound := new(big.Int).Lsh(big.NewInt(1), rLen*8) + if x.Cmp(upperBound) >= 0 { + panic("integer too large") + } + // 2. Write the integer x in its unique rLen-digit representation in base 256: + // x = x_(rLen-1) 256^(rLen-1) + x_(rLen-2) 256^(rLen-2) + ... + x_1 256 + x_0, + // where 0 <= x_i < 256 + // (note that one or more leading digits will be zero if x is less than 256^(rLen-1)). + // 3. Let the octet X_i have the integer value x_(rLen-i) for 1 <= i <= rLen. + // Output the octet string X = X_1 X_2 ... X_rLen. + + var b bytes.Buffer + xLen := (uint(x.BitLen()) + 7) >> 3 + if rLen > xLen { + b.Write(make([]byte, rLen-xLen)) // prepend 0s + } + b.Write(x.Bytes()) + return b.Bytes()[uint(b.Len())-rLen:] // The rightmost rLen bytes. +} + +// SECG1EncodeCompressed converts an EC point to an octet string according to +// the encoding specified in Section 2.3.3 of [SECG1] with point compression +// on. This implies ptLen = 2n + 1 = 33. +// +// SECG1 Section 2.3.3 https://www.secg.org/sec1-v1.99.dif.pdf +// +// (Note that certain software implementations do not introduce a separate +// elliptic curve point type and instead directly treat the EC point as an +// octet string per above encoding. When using such an implementation, the +// point_to_string function can be treated as the identity function.) +func secg1EncodeCompressed(curve elliptic.Curve, x, y *big.Int) []byte { + byteLen := (curve.Params().BitSize + 7) >> 3 + ret := make([]byte, 1+byteLen) + ret[0] = 2 // compressed point + + xBytes := x.Bytes() + copy(ret[1+byteLen-len(xBytes):], xBytes) + ret[0] += byte(y.Bit(0)) + return ret +} + +// This file implements compressed point unmarshaling. Preferably this +// functionality would be in a standard library. Code borrowed from: +// https://go-review.googlesource.com/#/c/1883/2/src/crypto/elliptic/elliptic.go + +// SECG1Decode decodes a EC point, given as a compressed string. +// If the decoding fails x and y will be nil. +// +// http://www.secg.org/sec1-v2.pdf +// https://tools.ietf.org/html/rfc8032#section-5.1.3 +// Section 4.3.6 of ANSI X9.62. + +var errInvalidPoint = errors.New("invalid point") + +func secg1Decode(curve elliptic.Curve, data []byte) (x, y *big.Int, err error) { + byteLen := (curve.Params().BitSize + 7) >> 3 + if (data[0] &^ 1) != 2 { + return nil, nil, errors.New("unrecognized point encoding") + } + if len(data) != 1+byteLen { + return nil, nil, errors.New("invalid length for curve") + } + + // Based on Routine 2.2.4 in NIST Mathematical routines paper + params := curve.Params() + tx := new(big.Int).SetBytes(data[1 : 1+byteLen]) + y2 := y2(params, tx) + sqrt := defaultSqrt + ty := sqrt(y2, params.P) + if ty == nil { + return nil, nil, errInvalidPoint // "y^2" is not a square + } + var y2c big.Int + y2c.Mul(ty, ty).Mod(&y2c, params.P) + if y2c.Cmp(y2) != 0 { + return nil, nil, errInvalidPoint // sqrt(y2)^2 != y2: invalid point + } + if ty.Bit(0) != uint(data[0]&1) { + ty.Sub(params.P, ty) + } + + return tx, ty, nil // valid point: return it +} + +// Use the curve equation to calculate y² given x. +// only applies to curves of the form y² = x³ - 3x + b. +func y2(curve *elliptic.CurveParams, x *big.Int) *big.Int { + // y² = x³ - 3x + b + x3 := new(big.Int).Mul(x, x) + x3.Mul(x3, x) + + threeX := new(big.Int).Lsh(x, 1) + threeX.Add(threeX, x) + + y2 := new(big.Int).Sub(x3, threeX) + y2.Add(y2, curve.B) + y2.Mod(y2, curve.P) + return y2 +} + +func defaultSqrt(x, p *big.Int) *big.Int { + var r big.Int + if nil == r.ModSqrt(x, p) { + return nil // x is not a square + } + return &r +} diff --git a/core/crypto/draft-irtf-cfrg-vrf-06/conversion_test.go b/core/crypto/draft-irtf-cfrg-vrf-06/conversion_test.go new file mode 100644 index 000000000..6587e0d7b --- /dev/null +++ b/core/crypto/draft-irtf-cfrg-vrf-06/conversion_test.go @@ -0,0 +1,58 @@ +package vrf + +import ( + "bytes" + "crypto/elliptic" + "crypto/rand" + "fmt" + "math/big" + "testing" +) + +func TestI2OSP(t *testing.T) { + for i, tc := range []struct { + x int64 + xLen uint + want []byte + wantPanic bool + }{ + {x: 1, xLen: 1, want: []byte{0x01}}, + {x: 2, xLen: 1, want: []byte{0x02}}, + {x: 2, xLen: 2, want: []byte{0, 2}}, + {x: 256, xLen: 8, want: []byte{0, 0, 0, 0, 0, 0, 1, 0}}, + {x: 256, xLen: 1, wantPanic: true}, + {x: 255, xLen: 1, want: []byte{0xff}}, + } { + t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { + defer func() { + r := recover() + if panicked := r != nil; panicked != tc.wantPanic { + t.Errorf("Panicked: %v, wantPanic %v", r, tc.wantPanic) + } + }() + if got := i2osp(big.NewInt(tc.x), tc.xLen); !bytes.Equal(got, tc.want) { + t.Errorf("I2OSP(%v, %v): %v, want %v", tc.x, tc.xLen, got, tc.want) + } + }) + } +} + +func TestSEG1EncodeDecode(t *testing.T) { + c := elliptic.P256() + _, Ax, Ay, err := elliptic.GenerateKey(c, rand.Reader) + if err != nil { + t.Fatal(err) + } + + b := secg1EncodeCompressed(c, Ax, Ay) + Bx, By, err := secg1Decode(c, b) + if err != nil { + t.Fatal(err) + } + if Bx.Cmp(Ax) != 0 { + t.Fatalf("Bx: %v, want %v", Bx, Ax) + } + if By.Cmp(Ay) != 0 { + t.Fatalf("By: %v, want %v", By, Ay) + } +}