Przeglądaj źródła

Go version of New Hope post-quantum key exchange.

(Code mostly due to agl.)

Change-Id: Iec77396141954e5f8e845cc261eadab77f551f08
Reviewed-on: https://boringssl-review.googlesource.com/7990
Reviewed-by: Adam Langley <agl@google.com>
kris/onging/CECPQ3_patch15
Matt Braithwaite 8 lat temu
committed by Adam Langley
rodzic
commit
c82b70155d
3 zmienionych plików z 560 dodań i 0 usunięć
  1. +305
    -0
      ssl/test/runner/newhope/newhope.go
  2. +140
    -0
      ssl/test/runner/newhope/newhope_test.go
  3. +115
    -0
      ssl/test/runner/newhope/reconciliation.go

+ 305
- 0
ssl/test/runner/newhope/newhope.go Wyświetl plik

@@ -0,0 +1,305 @@
// package newhope contains a post-quantum key agreement algorithm,
// reimplemented from the reference implementation at
// https://github.com/tpoeppelmann/newhope.
//
// Note that this package does not interoperate with the reference
// implementation.
package newhope

import (
"crypto/aes"
"crypto/cipher"
"errors"
"io"
)

const (
// q is the prime that defines the field.
q = 12289
// n is the number of coefficients in polynomials.
n = 1024
// k is the width of the noise distribution.
k = 16

// These values are used in the NTT calculation. See the paper for
// details about their origins.
omega = 49
invOmega = 1254
sqrtOmega = 7
invSqrtOmega = 8778
invN = 12277

// encodedPolyLen is the length, in bytes, of an encoded polynomial. The
// encoding uses 14 bits per coefficient.
encodedPolyLen = (n * 14) / 8

// offerMsgLen is the length, in bytes, of the offering (first) message of
// the key exchange.
OfferMsgLen = encodedPolyLen + 32

// acceptMsgLen is the length, in bytes, of the accepting (second) message
// of the key exchange.
AcceptMsgLen = encodedPolyLen + 256
)

// count16Bits returns the number of '1' bits in v.
func count16Bits(v uint16) (sum uint16) {
for i := 0; i < 16; i++ {
sum += v & 1
v >>= 1
}

return sum
}

// Poly is a polynomial of n coefficients.
type Poly [n]uint16

// Key is the result of a key agreement.
type Key [32]uint8

// sampleNoise returns a random polynomial where the coefficients are
// drawn from the noise distribution.
func sampleNoise(rand io.Reader) *Poly {
poly := new(Poly)
buf := make([]byte, 4)

for i := range poly {
if _, err := io.ReadFull(rand, buf); err != nil {
panic(err)
}
a := count16Bits(uint16(buf[0])<<8 | uint16(buf[1]))
b := count16Bits(uint16(buf[2])<<8 | uint16(buf[3]))
poly[i] = (q + a - b) % q
}

return poly
}

// randomPolynomial returns a random polynomial where the coefficients are
// drawn uniformly at random from the underlying field.
func randomPolynomial(rand io.Reader) *Poly {
poly := new(Poly)

buf := make([]byte, 2)
for i := range poly {
for {
if _, err := io.ReadFull(rand, buf); err != nil {
panic(err)
}

v := uint16(buf[1])<<8 | uint16(buf[0])
v &= 0x3fff

if v < q {
poly[i] = v
break
}
}
}

return poly
}

type zeroReader struct {
io.Reader
}

func (z *zeroReader) Read(dst []byte) (n int, err error) {
for i := range dst {
dst[i] = 0
}
return len(dst), nil
}

// seedToPolynomial uses AES-CTR to generate a pseudo-random polynomial given a
// 32-byte seed.
func seedToPolynomial(seed []byte) *Poly {
aes, err := aes.NewCipher(seed[0:16])
if err != nil {
panic(err)
}
stream := cipher.NewCTR(aes, seed[16:32])
reader := &cipher.StreamReader{S: stream, R: &zeroReader{}}
return randomPolynomial(reader)
}

// forwardNTT converts |in| into the frequency domain.
func forwardNTT(in *Poly) *Poly {
return ntt(in, omega, sqrtOmega, 1, 1)
}

// inverseNTT converts |in| into the time domain.
func inverseNTT(in *Poly) *Poly {
return ntt(in, invOmega, 1, invSqrtOmega, invN)
}

// ntt performs the number-theoretic transform (a discrete Fourier transform in
// a field) on in. Significant magic is in effect here. See the paper for the
// details of how this works.
func ntt(in *Poly, omega, preScaleBase, postScaleBase, postScale uint16) *Poly {
out := new(Poly)
omega_to_the_i := 1

for i := range out {
omegaToTheIJ := 1
preScale := int(1)
sum := 0

for j := range in {
t := (int(in[j]) * preScale) % q
sum += (t * omegaToTheIJ) % q
omegaToTheIJ = (omegaToTheIJ * omega_to_the_i) % q
preScale = (int(preScaleBase) * preScale) % q
}

out[i] = uint16((sum * int(postScale)) % q)

omega_to_the_i = (omega_to_the_i * int(omega)) % q
postScale = uint16((int(postScale) * int(postScaleBase)) % q)
}

return out
}

// encodeRec encodes the reconciliation data compactly, for use in the accept
// message.
func encodeRec(rec *reconciliationData) []byte {
var ret [n / 4]byte

for i := 0; i < n/4; i++ {
ret[i] = rec[4*i] | rec[4*i+1]<<2 | rec[4*i+2]<<4 | rec[4*i+3]<<6
}

return ret[:]
}

// decodeRec decodes reconciliation data from the accept message.
func decodeRec(message []byte) (rec *reconciliationData) {
rec = new(reconciliationData)

for i, b := range message {
rec[4*i] = b & 0x03
rec[4*i+1] = (b >> 2) & 0x3
rec[4*i+2] = (b >> 4) & 0x3
rec[4*i+3] = b >> 6
}

return rec
}

// encodePoly returns a byte array that encodes a polynomial compactly, with 14
// bits per coefficient.
func encodePoly(poly *Poly) []byte {
ret := make([]byte, encodedPolyLen)

for i := 0; i < n/4; i++ {
t0 := poly[4*i]
t1 := poly[4*i+1]
t2 := poly[4*i+2]
t3 := poly[4*i+3]

ret[7*i] = byte(t0)
ret[7*i+1] = byte(t0>>8) | byte(t1<<6)
ret[7*i+2] = byte(t1 >> 2)
ret[7*i+3] = byte(t1>>10) | byte(t2<<4)
ret[7*i+4] = byte(t2 >> 4)
ret[7*i+5] = byte(t2>>12) | byte(t3<<2)
ret[7*i+6] = byte(t3 >> 6)
}

return ret
}

// decodePoly inverts encodePoly.
func decodePoly(encoded []byte) *Poly {
ret := new(Poly)

for i := 0; i < n/4; i++ {
ret[4*i] = uint16(encoded[7*i]) | uint16(encoded[7*i+1]&0x3f)<<8
ret[4*i+1] = uint16(encoded[7*i+1])>>6 | uint16(encoded[7*i+2])<<2 | uint16(encoded[7*i+3]&0x0f)<<10
ret[4*i+2] = uint16(encoded[7*i+3])>>4 | uint16(encoded[7*i+4])<<4 | uint16(encoded[7*i+5]&0x03)<<12
ret[4*i+3] = uint16(encoded[7*i+5])>>2 | uint16(encoded[7*i+6])<<6
}

return ret
}

// Offer starts a new key exchange. It returns a message that should be
// transmitted to the peer, and a polynomial that must be retained in order to
// complete the exchange.
func Offer(rand io.Reader) (offerMsg []byte, sFreq *Poly) {
seed := make([]byte, 32)

if _, err := io.ReadFull(rand, seed); err != nil {
panic(err)
}

aFreq := seedToPolynomial(seed)
sFreq = forwardNTT(sampleNoise(rand))
eFreq := forwardNTT(sampleNoise(rand))

bFreq := new(Poly)
for i := range bFreq {
bFreq[i] = uint16((int(sFreq[i])*int(aFreq[i]) + int(eFreq[i])) % q)
}

offerMsg = encodePoly(bFreq)
offerMsg = append(offerMsg, seed[:]...)
return offerMsg, sFreq
}

// Accept processes a message generated by |Offer| and returns a reply message
// and the shared key.
func Accept(rand io.Reader, offerMsg []byte) (sharedKey Key, acceptMsg []byte, err error) {
if len(offerMsg) != OfferMsgLen {
return sharedKey, nil, errors.New("newhope: offer message has incorrect length")
}

bFreq := decodePoly(offerMsg)
seed := offerMsg[encodedPolyLen:]

aFreq := seedToPolynomial(seed)
sPrimeFreq := forwardNTT(sampleNoise(rand))
ePrimeFreq := forwardNTT(sampleNoise(rand))

uFreq := new(Poly)
for i := range uFreq {
uFreq[i] = uint16((int(sPrimeFreq[i])*int(aFreq[i]) + int(ePrimeFreq[i])) % q)
}

vFreq := new(Poly)
for i := range vFreq {
vFreq[i] = uint16((int(sPrimeFreq[i]) * int(bFreq[i])) % q)
}

v := inverseNTT(vFreq)
ePrimePrime := sampleNoise(rand)
for i := range v {
v[i] = uint16((int(v[i]) + int(ePrimePrime[i])) % q)
}

rec := helprec(rand, v)

sharedKey = reconcile(v, rec)
acceptMsg = encodePoly(uFreq)
acceptMsg = append(acceptMsg, encodeRec(rec)[:]...)
return sharedKey, acceptMsg, nil
}

// Finish processes the reply from the peer and returns the shared key.
func (sk *Poly) Finish(acceptMsg []byte) (sharedKey Key, err error) {
if len(acceptMsg) != AcceptMsgLen {
return sharedKey, errors.New("newhope: accept message has incorrect length")
}

uFreq := decodePoly(acceptMsg[:encodedPolyLen])
rec := decodeRec(acceptMsg[encodedPolyLen:])

for i, u := range uFreq {
uFreq[i] = uint16((int(u) * int(sk[i])) % q)
}
u := inverseNTT(uFreq)

return reconcile(u, rec), nil
}

+ 140
- 0
ssl/test/runner/newhope/newhope_test.go Wyświetl plik

@@ -0,0 +1,140 @@
package newhope

import (
"bytes"
"crypto/rand"
"fmt"
"io"
"io/ioutil"
"testing"
)

func TestNTTRoundTrip(t *testing.T) {
var a Poly
for i := range a {
a[i] = uint16(i)
}

frequency := forwardNTT(&a)
original := inverseNTT(frequency)

for i, v := range a {
if v != original[i] {
t.Errorf("NTT didn't invert correctly: original[%d] = %d", i, original[i])
break
}
}
}

func TestNTTInv(t *testing.T) {
var a Poly
for i := range a {
a[i] = uint16(i)
}

result := ntt(&a, invOmega, 1, invSqrtOmega, invN)
if result[0] != 6656 || result[1] != 1792 || result[2] != 1234 {
t.Errorf("NTT^-1 gave bad result: %v", result[:8])
}
}

func disabledTestNoise(t *testing.T) {
var buckets [1 + 2*k]int
numSamples := 100

for i := 0; i < numSamples; i++ {
noise := sampleNoise(rand.Reader)
for _, v := range noise {
value := (int(v) + k) % q
buckets[value]++
}
}

sum := 0
squareSum := 0

for i, count := range buckets {
sum += (i - k) * count
squareSum += (i - k) * (i - k) * count
}

mean := float64(sum) / float64(n*numSamples)
if mean < -0.5 || 0.5 < mean {
t.Errorf("mean out of range: %f", mean)
}

expectedVariance := 0.5 * 0.5 * float64(k*2) // I think?
variance := float64(squareSum)/float64(n*numSamples) - mean*mean

if variance < expectedVariance-1.0 || expectedVariance+1.0 < variance {
t.Errorf("variance out of range: got %f, want %f", variance, expectedVariance)
}

file, err := ioutil.TempFile("", "noise")
fmt.Printf("writing noise to %s\n", file.Name())
if err != nil {
t.Fatal(err)
}
for i, count := range buckets {
dots := ""
for i := 0; i < count/(3*numSamples); i++ {
dots += "++"
}
fmt.Fprintf(file, "%+d\t%d\t%s\n", i-k, count, dots)
}
file.Close()
}

func TestSeedToPolynomial(t *testing.T) {
seed := make([]byte, 32)
seed[0] = 1
seed[31] = 2

poly := seedToPolynomial(seed)
if poly[0] != 3313 || poly[1] != 9277 || poly[2] != 11020 {
t.Errorf("bad result: %v", poly[:3])
}
}

func TestEncodeDecodePoly(t *testing.T) {
poly := randomPolynomial(rand.Reader)
poly2 := decodePoly(encodePoly(poly))
if *poly != *poly2 {
t.Errorf("decodePoly(encodePoly) isn't the identity function")
}
}

func TestEncodeDecodeRec(t *testing.T) {
var r reconciliationData
if _, err := io.ReadFull(rand.Reader, r[:]); err != nil {
panic(err)
}
for i := range r {
r[i] &= 3
}

encoded := encodeRec(&r)
decoded := decodeRec(encoded)

if *decoded != r {
t.Errorf("bad decode of rec")
}
}

func TestExchange(t *testing.T) {
for count := 0; count < 64; count++ {
offerMsg, state := Offer(rand.Reader)
sharedKey1, acceptMsg, err := Accept(rand.Reader, offerMsg)
if err != nil {
t.Errorf("Accept: %v", err)
}
sharedKey2, err := state.Finish(acceptMsg)
if err != nil {
t.Fatal("Finish: %v", err)
}

if !bytes.Equal(sharedKey1[:], sharedKey2[:]) {
t.Fatalf("keys mismatched on iteration %d: %x vs %x", count, sharedKey1, sharedKey2)
}
}
}

+ 115
- 0
ssl/test/runner/newhope/reconciliation.go Wyświetl plik

@@ -0,0 +1,115 @@
package newhope

// This file contains the reconciliation algorithm for NewHope. This is simply a
// monkey-see-monkey-do version of the reference code, with the exception that
// the key resulting from reconciliation is whitened with SHA2 rather than SHA3.

import (
"crypto/sha256"
"io"
)

func abs(v int32) int32 {
mask := v >> 31
return (v ^ mask) - mask
}

func f(x int32) (v0, v1, k int32) {
// Next 6 lines compute t = x/q;
b := x * 2730
t := b >> 25
b = x - t*12289
b = 12288 - b
b >>= 31
t -= b

r := t & 1
xit := (t >> 1)
v0 = xit + r // v0 = round(x/(2*q))

t -= 1
r = t & 1
v1 = (t >> 1) + r

k = abs(x - (v0 * 2 * q))
return
}

// reconciliationData is the data needed for reconciliation. There are 2 bits
// per coefficient; this is the unpacked form.
type reconciliationData [n]uint8

func helprec(rand io.Reader, v *Poly) *reconciliationData {
var randBits [n / (4 * 8)]byte
if _, err := io.ReadFull(rand, randBits[:]); err != nil {
panic(err)
}

ret := new(reconciliationData)

for i := uint(0); i < n/4; i++ {
rbit := int32((randBits[i>>3] >> (i & 7)) & 1)

a0, b0, k0 := f(8*int32(v[i]) + 4*rbit)
a1, b1, k1 := f(8*int32(v[256+i]) + 4*rbit)
a2, b2, k2 := f(8*int32(v[512+i]) + 4*rbit)
a3, b3, k3 := f(8*int32(v[768+i]) + 4*rbit)

k := (2*q - 1 - (k0 + k1 + k2 + k3)) >> 31

v0 := ((^k) & a0) ^ (k & b0)
v1 := ((^k) & a1) ^ (k & b1)
v2 := ((^k) & a2) ^ (k & b2)
v3 := ((^k) & a3) ^ (k & b3)

ret[i] = uint8((v0 - v3) & 3)
ret[i+256] = uint8((v1 - v3) & 3)
ret[i+512] = uint8((v2 - v3) & 3)
ret[i+768] = uint8((-k + 2*v3) & 3)
}

return ret
}

func g(x int32) int32 {
// Next 6 lines compute t = x/(4*q);
b := x * 2730
t := b >> 27
b = x - t*49156
b = 49155 - b
b >>= 31
t -= b

c := t & 1
t = (t >> 1) + c // t = round(x/(8*q))

t *= 8 * q

return abs(t - x)
}

func ldDecode(xi0, xi1, xi2, xi3 int32) uint8 {
t := g(xi0)
t += g(xi1)
t += g(xi2)
t += g(xi3)

t -= 8 * q
t >>= 31
return uint8(t & 1)
}

func reconcile(v *Poly, reconciliation *reconciliationData) Key {
key := new(Key)

for i := uint(0); i < n/4; i++ {
t0 := 16*q + 8*int32(v[i]) - q*(2*int32(reconciliation[i])+int32(reconciliation[i+768]))
t1 := 16*q + 8*int32(v[i+256]) - q*(2*int32(reconciliation[256+i])+int32(reconciliation[i+768]))
t2 := 16*q + 8*int32(v[i+512]) - q*(2*int32(reconciliation[512+i])+int32(reconciliation[i+768]))
t3 := 16*q + 8*int32(v[i+768]) - q*int32(reconciliation[i+768])

key[i>>3] |= ldDecode(t0, t1, t2, t3) << (i & 7)
}

return sha256.Sum256(key[:])
}

Ładowanie…
Anuluj
Zapisz