* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
+437
View File
@@ -0,0 +1,437 @@
//
// Copyright Coinbase, Inc. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
// Package simplest implements the "Verified Simplest OT", as defined in "protocol 7" of [DKLs18](https://eprint.iacr.org/2018/499.pdf).
// The original "Simplest OT" protocol is presented in [CC15](https://eprint.iacr.org/2015/267.pdf).
// In our implementation, we run OTs for multiple choice bits in parallel. Furthermore, as described in the DKLs paper,
// we implement this as Random OT protocol. We also add encryption and decryption steps as defined in the protocol, but
// emphasise that these steps are optional. Specifically, in the setting where this OT is used as the seed OT in an
// OT Extension protocol, the encryption and decryption steps are not needed.
//
// Limitation: currently we only support batch OTs that are multiples of 8.
//
// Ideal functionalities:
// - We have used ZKP Schnorr for the F^{R_{DL}}_{ZK}
// - We have used HMAC for realizing the Random Oracle Hash function, the key for HMAC is received as input to the protocol.
package simplest
import (
"crypto/rand"
"crypto/subtle"
"fmt"
"github.com/gtank/merlin"
"github.com/pkg/errors"
"golang.org/x/crypto/sha3"
"github.com/sonr-io/sonr/crypto/core/curves"
"github.com/sonr-io/sonr/crypto/zkp/schnorr"
)
const (
// keyCount is the number of encryption keys created. Since this is a 1-out-of-2 OT, the key count is set to 2.
keyCount = 2
// DigestSize is the length of hash. Similarly, when it comes to encrypting and decryption, it is the size of the
// plaintext and ciphertext.
DigestSize = 32
)
type (
// OneTimePadDecryptionKey is the type of Rho^w, Rho^0, and RHo^1 in the paper.
OneTimePadDecryptionKey = [DigestSize]byte
// OneTimePadEncryptionKeys is the type of Rho^0, and RHo^1 in the paper.
OneTimePadEncryptionKeys = [keyCount][DigestSize]byte
// OtChallenge is the type of xi in the paper.
OtChallenge = [DigestSize]byte
// OtChallengeResponse is the type of Rho' in the paper.
OtChallengeResponse = [DigestSize]byte
// ChallengeOpening is the type of hashed Rho^0 and Rho^1
ChallengeOpening = [keyCount][DigestSize]byte
// ReceiversMaskedChoices corresponds to the "A" value in the paper in compressed format.
ReceiversMaskedChoices = []byte
)
// SenderOutput are the outputs that the sender will obtain as a result of running the "random" OT protocol.
type SenderOutput struct {
// OneTimePadEncryptionKeys are Rho^0 and Rho^1, the output of the random OT.
// These can be used to encrypt and send two messages to the receiver.
// Therefore, for readability they are called OneTimePadEncryptionKeys in the code.
OneTimePadEncryptionKeys []OneTimePadEncryptionKeys
}
// ReceiverOutput are the outputs that the receiver will obtain as a result of running the "random" OT protocol.
type ReceiverOutput struct {
// PackedRandomChoiceBits is a packed version of the choice vector, the packing is done for performance reasons.
PackedRandomChoiceBits []byte
// RandomChoiceBits is the choice vector represented as unpacked int array. Initialed from PackedRandomChoiceBits.
RandomChoiceBits []int
// OneTimePadDecryptionKey is Rho^w, the output of the random OT. For the receiver, there is just 1 output per execution.
// This value will be used to decrypt one of the messages sent by the sender.
// Therefore, for readability this is called OneTimePadDecryptionKey in the code.
OneTimePadDecryptionKey []OneTimePadDecryptionKey
}
// Sender stores state for the "sender" role in OT. see Protocol 7 in Appendix A of DKLs18.
type Sender struct {
// Output is the output that is produced as a result of running random OT protocol.
Output *SenderOutput
curve *curves.Curve
// secretKey is the value `b` in the paper, which is the discrete log of B, which will be (re)used in _all_ executions of the OT.
secretKey curves.Scalar
// publicKey is the public key of the secretKey.
publicKey curves.Point
// batchSize is the number of parallel OTs.
batchSize int
transcript *merlin.Transcript
}
// Receiver stores state for the "receiver" role in OT. Protocol 7, Appendix A, of DKLs.
type Receiver struct {
// Output is the output that is produced as a result of running random OT protocol.
Output *ReceiverOutput
curve *curves.Curve
// senderPublicKey corresponds to "B" in the paper.
senderPublicKey curves.Point
// senderChallenge is "xi" in the protocol.
senderChallenge []OtChallenge
// batchSize is the number of parallel OTs.
batchSize int
transcript *merlin.Transcript
}
// NewSender creates a new "sender" object, ready to participate in a _random_ verified simplest OT in the role of the sender.
// no messages are specified by the sender, because random ones will be sent (hence the random OT).
// ultimately, the `Sender`'s `Output` field will be appropriately populated.
// you can use it directly, or alternatively bootstrap it into an _actual_ (non-random) OT using `Round7Encrypt` below
func NewSender(
curve *curves.Curve,
batchSize int,
uniqueSessionId [DigestSize]byte,
) (*Sender, error) {
if batchSize&0x07 != 0 { // This is the same as `batchSize % 8 != 0`, but is constant time
return nil, errors.New("batch size should be a multiple of 8")
}
transcript := merlin.NewTranscript("Coinbase_DKLs_SeedOT")
transcript.AppendMessage([]byte("session_id"), uniqueSessionId[:])
return &Sender{
Output: &SenderOutput{},
curve: curve,
batchSize: batchSize,
transcript: transcript,
}, nil
}
// NewReceiver is a Random OT receiver. Therefore, the choice bits are created randomly.
// The choice bits are stored in a packed format (e.g., each choice is a single bit in a byte array).
func NewReceiver(
curve *curves.Curve,
batchSize int,
uniqueSessionId [DigestSize]byte,
) (*Receiver, error) {
// This is the same as `batchSize % 8 != 0`, but is constant time
if batchSize&0x07 != 0 {
return nil, errors.New("batch size should be a multiple of 8")
}
transcript := merlin.NewTranscript("Coinbase_DKLs_SeedOT")
transcript.AppendMessage([]byte("session_id"), uniqueSessionId[:])
receiver := &Receiver{
Output: &ReceiverOutput{},
curve: curve,
batchSize: batchSize,
transcript: transcript,
}
batchSizeBytes := batchSize >> 3 // divide by 8
receiver.Output.PackedRandomChoiceBits = make([]byte, batchSizeBytes)
if _, err := rand.Read(receiver.Output.PackedRandomChoiceBits[:]); err != nil {
return nil, errors.Wrap(err, "choosing random choice bits")
}
// Unpack into Choice bits
receiver.initChoice()
return receiver, nil
}
// Round1ComputeAndZkpToPublicKey is the first phase of the protocol.
// computes and stores public key and returns the schnorr proof. serialized / packed.
// This implements step 1 of Protocol 7 of DKLs18, page 16.
func (sender *Sender) Round1ComputeAndZkpToPublicKey() (*schnorr.Proof, error) {
var err error
// Sample the secret key and compute the public key.
sender.secretKey = sender.curve.Scalar.Random(rand.Reader)
sender.publicKey = sender.curve.ScalarBaseMult(sender.secretKey)
// Generate the ZKP proof.
uniqueSessionId := [DigestSize]byte{}
copy(
uniqueSessionId[:],
sender.transcript.ExtractBytes([]byte("sender schnorr proof"), DigestSize),
)
prover := schnorr.NewProver(sender.curve, nil, uniqueSessionId[:])
proof, err := prover.Prove(sender.secretKey)
if err != nil {
return nil, errors.Wrap(err, "creating zkp proof for secret key in seed OT sender round 1")
}
return proof, nil
}
// Round2VerifySchnorrAndPadTransfer verifies the schnorr proof of the public key sent by the sender, i.e., step 2),
// and then does receiver's "Pad Transfer" phase in OT, i.e., step 3), of Protocol 7 (page 16) of the paper.
func (receiver *Receiver) Round2VerifySchnorrAndPadTransfer(
proof *schnorr.Proof,
) ([]ReceiversMaskedChoices, error) {
receiver.senderPublicKey = proof.Statement
uniqueSessionId := [DigestSize]byte{}
copy(
uniqueSessionId[:],
receiver.transcript.ExtractBytes([]byte("sender schnorr proof"), DigestSize),
)
if err := schnorr.Verify(proof, receiver.curve, nil, uniqueSessionId[:]); err != nil {
return nil, errors.Wrap(err, "verifying schnorr proof in seed OT receiver round 2")
}
result := make([]ReceiversMaskedChoices, receiver.batchSize)
receiver.Output.OneTimePadDecryptionKey = make([]OneTimePadDecryptionKey, receiver.batchSize)
copy(
uniqueSessionId[:],
receiver.transcript.ExtractBytes([]byte("random oracle salts"), DigestSize),
)
for i := 0; i < receiver.batchSize; i++ {
a := receiver.curve.Scalar.Random(rand.Reader)
// Computing `A := a . G + w . B` in constant time, by first computing option0 = a.G and option1 = a.G+B and then
// constant time choosing one of them by first assuming that the output is option0, and overwrite it if the choice bit is 1.
option0 := receiver.curve.ScalarBaseMult(a)
option0Bytes := option0.ToAffineCompressed()
option1 := option0.Add(receiver.senderPublicKey)
option1Bytes := option1.ToAffineCompressed()
result[i] = option0Bytes
subtle.ConstantTimeCopy(receiver.Output.RandomChoiceBits[i], result[i], option1Bytes)
// compute the internal rho
rho := receiver.senderPublicKey.Mul(a)
hash := sha3.New256()
if _, err := hash.Write(uniqueSessionId[:]); err != nil {
return nil, errors.Wrap(err, "writing seed to hash in round 2 pad transfer")
}
if _, err := hash.Write([]byte{byte(i)}); err != nil {
return nil, errors.Wrap(err, "writing i to hash in round 2 pad transfer")
}
if _, err := hash.Write(rho.ToAffineCompressed()); err != nil {
return nil, errors.Wrap(err, "writing point to hash in round 2 pad transfer")
}
copy(receiver.Output.OneTimePadDecryptionKey[i][:], hash.Sum(nil))
}
return result, nil
}
// Round3PadTransfer is the sender's "Pad Transfer" phase in OT; see steps 4 and 5 of page 16 of the paper.
// Returns the challenges xi
func (sender *Sender) Round3PadTransfer(
compressedReceiversMaskedChoice []ReceiversMaskedChoices,
) ([]OtChallenge, error) {
var err error
challenge := make([]OtChallenge, sender.batchSize)
sender.Output.OneTimePadEncryptionKeys = make([]OneTimePadEncryptionKeys, sender.batchSize)
negSenderPublicKey := sender.publicKey.Neg()
receiversMaskedChoice := make([]curves.Point, len(compressedReceiversMaskedChoice))
for i := 0; i < len(compressedReceiversMaskedChoice); i++ {
if receiversMaskedChoice[i], err = sender.curve.Point.FromAffineCompressed(compressedReceiversMaskedChoice[i]); err != nil {
return nil, errors.Wrap(err, "uncompress the point")
}
}
baseEncryptionKeyMaterial := make([]curves.Point, keyCount)
var hashedKey [keyCount][DigestSize]byte
uniqueSessionId := [DigestSize]byte{}
copy(
uniqueSessionId[:],
sender.transcript.ExtractBytes([]byte("random oracle salts"), DigestSize),
)
for i := 0; i < sender.batchSize; i++ {
// Sender creates two options that will eventually be used as her encryption keys.
// `baseEncryptionKeyMaterial[0]` and `baseEncryptionKeyMaterial[0]` correspond to rho_0 and rho_1 in the paper, respectively.
baseEncryptionKeyMaterial[0] = receiversMaskedChoice[i].Mul(sender.secretKey)
receiverChoiceMinusSenderPublicKey := receiversMaskedChoice[i].Add(negSenderPublicKey)
baseEncryptionKeyMaterial[1] = receiverChoiceMinusSenderPublicKey.Mul(sender.secretKey)
for k := 0; k < keyCount; k++ {
hash := sha3.New256()
if _, err = hash.Write(uniqueSessionId[:]); err != nil {
return nil, errors.Wrap(err, "writing seed to hash in round 3 pad transfer")
}
if _, err = hash.Write([]byte{byte(i)}); err != nil {
return nil, errors.Wrap(err, "writing i to hash in round 3 pad transfer")
}
if _, err = hash.Write(baseEncryptionKeyMaterial[k].ToAffineCompressed()); err != nil {
return nil, errors.Wrap(err, "writing point to hash in round 3 pad transfer")
}
copy(sender.Output.OneTimePadEncryptionKeys[i][k][:], hash.Sum(nil))
if err != nil {
return nil, errors.Wrap(err, "compute the encryption keys")
}
// Compute a challenge by XORing the hash of the hash of the key. Not a typo ;)
hashedKey[k] = sha3.Sum256(sender.Output.OneTimePadEncryptionKeys[i][k][:])
hashedKey[k] = sha3.Sum256(hashedKey[k][:])
}
challenge[i] = xorBytes(hashedKey[0], hashedKey[1])
}
return challenge, nil
}
// Round4RespondToChallenge corresponds to initial round of the receiver's "Verify" phase; see step 6 of page 16 of the paper.
// this is just the start of Verification. In this round, the receiver outputs "rho'", which the sender will check.
func (receiver *Receiver) Round4RespondToChallenge(
challenge []OtChallenge,
) ([]OtChallengeResponse, error) {
// store to be used in future steps
receiver.senderChallenge = challenge
// challengeResponses is Rho' in the paper.
challengeResponses := make([]OtChallengeResponse, receiver.batchSize)
for i := 0; i < receiver.batchSize; i++ {
// Constant-time xor of the hashed key and the challenge, based on the choice bit.
hashedKey := sha3.Sum256(receiver.Output.OneTimePadDecryptionKey[i][:])
hashedKey = sha3.Sum256(hashedKey[:])
challengeResponses[i] = hashedKey
alternativeChallengeResponse := xorBytes(receiver.senderChallenge[i], hashedKey)
subtle.ConstantTimeCopy(
receiver.Output.RandomChoiceBits[i],
challengeResponses[i][:],
alternativeChallengeResponse[:],
)
}
return challengeResponses, nil
}
// Round5Verify verifies the challenge response. If the verification passes, sender opens his challenges to the receiver.
// See step 7 of page 16 of the paper.
// Abort if Rho' != H(H(Rho^0)) in other words, if challengeResponse != H(H(encryption key 0)).
// opening is H(encryption key)
func (sender *Sender) Round5Verify(
challengeResponses []OtChallengeResponse,
) ([]ChallengeOpening, error) {
opening := make([]ChallengeOpening, sender.batchSize)
for i := 0; i < sender.batchSize; i++ {
for k := 0; k < keyCount; k++ {
opening[i][k] = sha3.Sum256(sender.Output.OneTimePadEncryptionKeys[i][k][:])
}
// Verify
hashedKey0 := sha3.Sum256(opening[i][0][:])
if subtle.ConstantTimeCompare(hashedKey0[:], challengeResponses[i][:]) != 1 {
return nil, errors.New("receiver's challenge response didn't match H(H(rho^0))")
}
}
return opening, nil
}
// Round6Verify is the _last_ part of the "Verification" phase of OT; see p. 16 of https://eprint.iacr.org/2018/499.pdf.
// See step 8 of page 16 of the paper.
// Abort if H(Rho^w) != the one it calculated itself or
//
// if Xi != H(H(Rho^0)) XOR H(H(Rho^1))
//
// In other words,
//
// if opening_w != H(decryption key) or
// if challenge != H(opening 0) XOR H(opening 0)
func (receiver *Receiver) Round6Verify(challengeOpenings []ChallengeOpening) error {
for i := 0; i < receiver.batchSize; i++ {
hashedDecryptionKey := sha3.Sum256(receiver.Output.OneTimePadDecryptionKey[i][:])
w := receiver.Output.RandomChoiceBits[i]
if subtle.ConstantTimeCompare(hashedDecryptionKey[:], challengeOpenings[i][w][:]) != 1 {
return fmt.Errorf("sender's supposed H(rho^omega) doesn't match our own")
}
hashedKey0 := sha3.Sum256(challengeOpenings[i][0][:])
hashedKey1 := sha3.Sum256(challengeOpenings[i][1][:])
reconstructedChallenge := xorBytes(hashedKey0, hashedKey1)
if subtle.ConstantTimeCompare(
reconstructedChallenge[:],
receiver.senderChallenge[i][:],
) != 1 {
return fmt.Errorf(
"sender's openings H(rho^0) and H(rho^1) didn't decommit to its prior message",
)
}
}
return nil
}
// Round7Encrypt wraps an `Encrypt` operation on the Sender's underlying output from the random OT; see `Encrypt` below.
// this is optional; it will be used only in circumstances when you want to run "actual" (i.e., non-random) OT
func (sender *Sender) Round7Encrypt(
messages [][keyCount][DigestSize]byte,
) ([][keyCount][DigestSize]byte, error) {
return sender.Output.Encrypt(messages)
}
// Round8Decrypt wraps a `Decrypt` operation on the Receiver's underlying output from the random OT; see `Decrypt` below
// this is optional; it will be used only in circumstances when you want to run "actual" (i.e., non-random) OT
func (receiver *Receiver) Round8Decrypt(
ciphertext [][keyCount][DigestSize]byte,
) ([][DigestSize]byte, error) {
return receiver.Output.Decrypt(ciphertext)
}
// Encrypt runs step 9) of the seed OT Protocol 7) of https://eprint.iacr.org/2018/499.pdf,
// in which the seed OT sender "encrypts" both messages under the "one-time keys" output by the random OT.
func (s *SenderOutput) Encrypt(
plaintexts [][keyCount][DigestSize]byte,
) ([][keyCount][DigestSize]byte, error) {
batchSize := len(s.OneTimePadEncryptionKeys)
if len(plaintexts) != batchSize {
return nil, errors.New("message size should be same as batch size")
}
ciphertexts := make([][keyCount][DigestSize]byte, batchSize)
for i := 0; i < len(plaintexts); i++ {
for k := 0; k < keyCount; k++ {
ciphertexts[i][k] = xorBytes(s.OneTimePadEncryptionKeys[i][k], plaintexts[i][k])
}
}
return ciphertexts, nil
}
// Decrypt is step 10) of the seed OT Protocol 7) of https://eprint.iacr.org/2018/499.pdf,
// where the seed OT receiver "decrypts" the message it's receiving using the "key" it received in the random OT.
func (r *ReceiverOutput) Decrypt(
ciphertexts [][keyCount][DigestSize]byte,
) ([][DigestSize]byte, error) {
batchSize := len(r.OneTimePadDecryptionKey)
if len(ciphertexts) != batchSize {
return nil, errors.New("number of ciphertexts should be same as batch size")
}
plaintexts := make([][DigestSize]byte, batchSize)
for i := 0; i < len(ciphertexts); i++ {
choice := r.RandomChoiceBits[i]
plaintexts[i] = xorBytes(r.OneTimePadDecryptionKey[i], ciphertexts[i][choice])
}
return plaintexts, nil
}
+96
View File
@@ -0,0 +1,96 @@
//
// Copyright Coinbase, Inc. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
package simplest_test
import (
"crypto/rand"
"crypto/sha256"
"fmt"
"testing"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/crypto/core/curves"
"github.com/sonr-io/sonr/crypto/ot/base/simplest"
"github.com/sonr-io/sonr/crypto/ot/ottest"
)
func TestOtOnMultipleCurves(t *testing.T) {
curveInstances := []*curves.Curve{
curves.K256(),
curves.P256(),
}
for _, curve := range curveInstances {
batchSize := 256
hashKeySeed := [32]byte{}
_, err := rand.Read(hashKeySeed[:])
require.NoError(t, err)
sender, receiver, err := ottest.RunSimplestOT(curve, batchSize, hashKeySeed)
require.NoError(t, err)
for i := 0; i < batchSize; i++ {
require.Equal(
t,
receiver.OneTimePadDecryptionKey[i],
sender.OneTimePadEncryptionKeys[i][receiver.RandomChoiceBits[i]],
)
}
// Transfer messages
messages := make([][2][32]byte, batchSize)
for i := 0; i < batchSize; i++ {
messages[i] = [2][32]byte{
sha256.Sum256([]byte(fmt.Sprintf("message[%d][0]", i))),
sha256.Sum256([]byte(fmt.Sprintf("message[%d][1]", i))),
}
}
ciphertexts, err := sender.Encrypt(messages)
require.NoError(t, err)
decrypted, err := receiver.Decrypt(ciphertexts)
require.NoError(t, err)
for i := 0; i < batchSize; i++ {
choice := receiver.RandomChoiceBits[i]
require.Equal(t, messages[i][choice], decrypted[i])
require.NotEqual(t, messages[i][1-choice], decrypted[i])
}
}
}
func TestOTStreaming(t *testing.T) {
batchSize := 256
curve := curves.K256()
hashKeySeed := [32]byte{}
_, err := rand.Read(hashKeySeed[:])
require.NoError(t, err)
sender, err := simplest.NewSender(curve, batchSize, hashKeySeed)
require.Nil(t, err)
receiver, err := simplest.NewReceiver(curve, batchSize, hashKeySeed)
require.Nil(t, err)
senderPipe, receiverPipe := simplest.NewPipeWrappers()
errorsChannel := make(
chan error,
2,
) // warning: if one party errors, the other will sit there forever. add timeouts.
go func() {
errorsChannel <- simplest.SenderStreamOTRun(sender, senderPipe)
}()
go func() {
errorsChannel <- simplest.ReceiverStreamOTRun(receiver, receiverPipe)
}()
for i := 0; i < 2; i++ {
require.Nil(t, <-errorsChannel)
}
for i := 0; i < batchSize; i++ {
require.Equal(
t,
receiver.Output.OneTimePadDecryptionKey[i],
sender.Output.OneTimePadEncryptionKeys[i][receiver.Output.RandomChoiceBits[i]],
)
}
}
+100
View File
@@ -0,0 +1,100 @@
package simplest
import (
"encoding/gob"
"io"
"github.com/pkg/errors"
"github.com/sonr-io/sonr/crypto/core/curves"
"github.com/sonr-io/sonr/crypto/zkp/schnorr"
)
// ReceiverStreamOTRun exposes the entire seed OT process for the receiver in "stream mode" to the user.
// what this means is that instead of calling the component methods in the process manually, and manually handling
// the encoding and decoding of the resulting output and input structs, the user needs _only_ to pass a ReadWriter
// (in practice this will be something like a websocket object), and this method will handle the entire process.
// this serves the dual (though related) purpose of conveniently bundling up the entire seed OT process,
// for use in tests, both in this package, as well as in the other packages which use this one (like cOT and mult).
func ReceiverStreamOTRun(receiver *Receiver, rw io.ReadWriter) error {
enc := gob.NewEncoder(rw)
dec := gob.NewDecoder(rw)
gob.Register(&curves.ScalarK256{})
gob.Register(&curves.PointK256{})
proof := &schnorr.Proof{}
if err := dec.Decode(proof); err != nil {
return errors.Wrap(err, "failed to decode proof in receiver stream OT")
}
receiversMaskedChoice, err := receiver.Round2VerifySchnorrAndPadTransfer(proof)
if err != nil {
return errors.Wrap(err, "error in round 2 in receiver stream OT")
}
if err = enc.Encode(receiversMaskedChoice); err != nil {
return errors.Wrap(err, "error encoding result of round 1 in receiver stream OT")
}
var challenge []OtChallenge
err = dec.Decode(&challenge)
if err != nil {
return errors.Wrap(err, "error decoding challenge in receiver stream OT")
}
challengeResponse, err := receiver.Round4RespondToChallenge(challenge)
if err != nil {
return errors.Wrap(err, "error computing round 2 challenge response in receiver stream OT")
}
if err = enc.Encode(challengeResponse); err != nil {
return errors.Wrap(err, "error encoding challenge response in receiver stream OT")
}
var openings []ChallengeOpening
err = dec.Decode(&openings)
if err != nil {
return errors.Wrap(err, "error decoding challenge openings in receiver stream OT")
}
return receiver.Round6Verify(openings)
}
// SenderStreamOTRun exposes the entire seed OT process for the sender in "stream mode" to the user.
// similarly to the above, this means that the user needs only to pass a `ReadWriter` representing the comm channel;
// this method will handle all encoding and decoding + writing and reading to the channel.
func SenderStreamOTRun(sender *Sender, rw io.ReadWriter) error {
// again a high-level helper method showing the overall flow, this time for the sender.
enc := gob.NewEncoder(rw)
dec := gob.NewDecoder(rw)
gob.Register(&curves.ScalarK256{})
gob.Register(&curves.PointK256{})
proof, err := sender.Round1ComputeAndZkpToPublicKey()
if err != nil {
return err
}
if err = enc.Encode(proof); err != nil {
return err
}
var receiversMaskedChoice []ReceiversMaskedChoices
err = dec.Decode(&receiversMaskedChoice)
if err != nil {
return errors.Wrap(err, "error decoding receiver's masked choice in sender stream OT")
}
challenge, err := sender.Round3PadTransfer(receiversMaskedChoice)
if err != nil {
return errors.Wrap(err, "error during round 2 pad transfer in sender stream OT")
}
err = enc.Encode(challenge)
if err != nil {
return errors.Wrap(err, "error encoding challenge in sender stream OT")
}
var challengeResponses []OtChallengeResponse
err = dec.Decode(&challengeResponses)
if err != nil {
return errors.Wrap(err, "error decoding challenges responses in sender stream OT")
}
opening, err := sender.Round5Verify(challengeResponses)
if err != nil {
return errors.Wrap(err, "error in round 3 verify in sender stream OT")
}
return enc.Encode(opening)
}
+55
View File
@@ -0,0 +1,55 @@
package simplest
import (
"io"
)
// xorBytes computes c = a xor b.
func xorBytes(a, b [DigestSize]byte) (c [DigestSize]byte) {
for i := 0; i < DigestSize; i++ {
c[i] = a[i] ^ b[i]
}
return c
}
// initChoice initializes the receiver's choice array from the PackedRandomChoiceBits array
func (receiver *Receiver) initChoice() {
// unpack the random values in PackedRandomChoiceBits into bits in Choice
receiver.Output.RandomChoiceBits = make([]int, receiver.batchSize)
for i := 0; i < len(receiver.Output.RandomChoiceBits); i++ {
receiver.Output.RandomChoiceBits[i] = int(
ExtractBitFromByteVector(receiver.Output.PackedRandomChoiceBits, i),
)
}
}
// ExtractBitFromByteVector interprets the byte-vector `vector` as if it were a _bit_-vector with len(vector) * 8 bits.
// it extracts the `index`th such bit, interpreted in the little-endian way (i.e., both across bytes and within bytes).
func ExtractBitFromByteVector(vector []byte, index int) byte {
// the bitwise tricks index >> 3 == index // 8 and index & 0x07 == index % 8 are designed to avoid CPU division.
return vector[index>>3] >> (index & 0x07) & 0x01
}
type pipeWrapper struct {
r *io.PipeReader
w *io.PipeWriter
exchanged int // used this during testing, to track bytes exchanged
}
func (wrapper *pipeWrapper) Write(p []byte) (n int, err error) {
n, err = wrapper.w.Write(p)
wrapper.exchanged += n
return n, err
}
func (wrapper *pipeWrapper) Read(p []byte) (n int, err error) {
n, err = wrapper.r.Read(p)
wrapper.exchanged += n
return n, err
}
func NewPipeWrappers() (*pipeWrapper, *pipeWrapper) {
leftOut, leftIn := io.Pipe()
rightOut, rightIn := io.Pipe()
return &pipeWrapper{r: leftOut, w: rightIn}, &pipeWrapper{r: rightOut, w: leftIn}
}
+475
View File
@@ -0,0 +1,475 @@
//
// Copyright Coinbase, Inc. All Rights Reserved.
//
// SPDX-License-Identifier: Apache-2.0
//
// Package kos in an implementation of maliciously secure OT extension protocol defined in "Protocol 9" of
// [DKLs18](https://eprint.iacr.org/2018/499.pdf). The original protocol was presented in
// [KOS15](https://eprint.iacr.org/2015/546.pdf).
package kos
import (
"crypto/rand"
"crypto/subtle"
"encoding/binary"
"fmt"
"golang.org/x/crypto/sha3"
"github.com/pkg/errors"
"github.com/sonr-io/sonr/crypto/core/curves"
"github.com/sonr-io/sonr/crypto/ot/base/simplest"
)
const (
// below are the "cryptographic parameters", including computational and statistical,
// as well as the cOT block size parameters, which depend on these in a pre-defined way.
// Kappa is the computational security parameter.
Kappa = 256
// KappaBytes is same as Kappa // 8, but avoids cpu division.
KappaBytes = Kappa >> 3
// L is the batch size used in the cOT functionality.
L = 2*Kappa + 2*s
// COtBlockSizeBytes is same as L // 8, but avoids cpu division.
COtBlockSizeBytes = L >> 3
// OtWidth is the number of scalars processed per "slot" of the cOT. by definition of this parameter,
// for each of the receiver's choice bits, the sender will provide `OTWidth` scalars.
// in turn, both the sender and receiver will obtain `OTWidth` shares _per_ slot / bit of the cOT.
// by definition of the cOT, these "vectors of" scalars will add (componentwise) to the sender's original scalars.
OtWidth = 2
s = 80 // statistical security parameter.
kappaOT = Kappa + s
lPrime = L + kappaOT // length of pseudorandom seed expansion, used within cOT protocol
cOtExtendedBlockSizeBytes = lPrime >> 3
)
type Receiver struct {
// OutputAdditiveShares are the ultimate output received. basically just the "pads".
OutputAdditiveShares [L][OtWidth]curves.Scalar
// seedOtResults are the results that this party has received by playing the sender role in a base OT protocol.
seedOtResults *simplest.SenderOutput
// extendedPackedChoices is storage for "choice vector || gamma^{ext}" in a packed format.
extendedPackedChoices [cOtExtendedBlockSizeBytes]byte
psi [lPrime][KappaBytes]byte // transpose of v^0. gets retained between messages
curve *curves.Curve
uniqueSessionId [simplest.DigestSize]byte // store this between rounds
}
type Sender struct {
// OutputAdditiveShares are the ultimate output received. basically just the "pads".
OutputAdditiveShares [L][OtWidth]curves.Scalar
// seedOtResults are the results that this party has received by playing the receiver role in a base OT protocol.
seedOtResults *simplest.ReceiverOutput
curve *curves.Curve
}
func binaryFieldMul(A []byte, B []byte) []byte {
// multiplies `A` and `B` in the finite field of order 2^256.
// The reference is Hankerson, Vanstone and Menezes, Guide to Elliptic Curve Cryptography. https://link.springer.com/book/10.1007/b97644
// `A` and `B` are both assumed to be 32-bytes slices. here we view them as little-endian coordinate representations of degree-255 polynomials.
// the multiplication takes place modulo the irreducible (over F_2) polynomial f(X) = X^256 + X^10 + X^5 + X^2 + 1. see Table A.1.
// the techniques we use are given in section 2.3, Binary field arithmetic.
// for the multiplication part, we use Algorithm 2.34, "Right-to-left comb method for polynomial multiplication".
// for the reduction part, we use a variant of the idea of Figure 2.9, customized to our setting.
const W = 64 // the machine word width, in bits.
const t = 4 // the number of words needed to represent a polynomial.
c := make([]uint64, 2*t) // result
a := make([]uint64, t)
b := make([]uint64, t+1) // will hold a copy of b, shifted by some amount
for i := 0; i < 32; i++ { // "condense" `A` and `B` into word-vectors, instead of byte-vectors
a[i>>3] |= uint64(A[i]) << (i & 0x07 << 3)
b[i>>3] |= uint64(B[i]) << (i & 0x07 << 3)
}
for k := 0; k < W; k++ {
for j := 0; j < t; j++ {
// conditionally add a copy of (the appropriately shifted) B to C, depending on the appropriate bit of A
// do this in constant-time; i.e., independent of A.
// technically, in each time we call this, the right-hand argument is a public datum,
// so we could arrange things so that it's _not_ constant-time, but the variable-time stuff always depends on something public.
// better to just be safe here though and make it constant-time anyway.
mask := -(a[j] >> k & 0x01) // if A[j] >> k & 0x01 == 1 then 0xFFFFFFFFFFFFFFFF else 0x0000000000000000
for i := 0; i < t+1; i++ {
c[j+i] ^= b[i] & mask // conditionally add B to C{j}
}
}
for i := t; i > 0; i-- {
b[i] = b[i]<<1 | b[i-1]>>63
}
b[0] <<= 1
}
// multiplication complete; begin reduction.
// things become actually somewhat simpler in our case, because the degree of the polynomial is a multiple of the word size
// the technique to come up with the numbers below comes essentially from going through the exact same process as on page 54,
// but with the polynomial f(X) = X^256 + X^10 + X^5 + X^2 + 1 above instead, and with parameters m = 256, W = 64, t = 4.
// the idea is exactly as described informally on that page, even though this particular polynomial isn't explicitly treated.
for i := 2*t - 1; i >= t; i-- {
c[i-4] ^= c[i] << 10
c[i-3] ^= c[i] >> 54
c[i-4] ^= c[i] << 5
c[i-3] ^= c[i] >> 59
c[i-4] ^= c[i] << 2
c[i-3] ^= c[i] >> 62
c[i-4] ^= c[i]
}
C := make([]byte, 32)
for i := 0; i < 32; i++ {
C[i] = byte(c[i>>3] >> (i & 0x07 << 3)) // truncate word to byte
}
return C
}
// NewCOtReceiver creates a `Receiver` instance, ready for use as the receiver in the KOS cOT protocol
// you must supply the output gotten by running an instance of seed OT as the _sender_ (note the reversal of roles)
func NewCOtReceiver(seedOTResults *simplest.SenderOutput, curve *curves.Curve) *Receiver {
return &Receiver{
seedOtResults: seedOTResults,
curve: curve,
}
}
// NewCOtSender creates a `Sender` instance, ready for use as the sender in the KOS cOT protocol.
// you must supply the output gotten by running an instance of seed OT as the _receiver_ (note the reversal of roles)
func NewCOtSender(seedOTResults *simplest.ReceiverOutput, curve *curves.Curve) *Sender {
return &Sender{
seedOtResults: seedOTResults,
curve: curve,
}
}
// Round1Output is Bob's first message to Alice during cOT extension;
// these outputs are described in step 4) of Protocol 9) https://eprint.iacr.org/2018/499.pdf
type Round1Output struct {
U [Kappa][cOtExtendedBlockSizeBytes]byte
WPrime [simplest.DigestSize]byte
VPrime [simplest.DigestSize]byte
}
// Round2Output this is Alice's response to Bob in cOT extension;
// the values `tau` are specified in Alice's step 6) of Protocol 9) https://eprint.iacr.org/2018/499.pdf
type Round2Output struct {
Tau [L][OtWidth]curves.Scalar
}
// convertBitToBitmask converts a "bit"---i.e., a `byte` which is _assumed to be_ either 0 or 1---into a bitmask,
// namely, it outputs 0x00 if `bit == 0` and 0xFF if `bit == 1`.
func convertBitToBitmask(bit byte) byte {
return ^(bit - 0x01)
}
// the below code takes as input a `kappa` by `lPrime` _boolean_ matrix, whose rows are actually "compacted" as bytes.
// so in actuality, it's a `kappa` by `lPrime >> 3 == cOtExtendedBlockSizeBytes` matrix of _bytes_.
// its output is the same boolean matrix, but transposed, so it has dimensions `lPrime` by `kappa`.
// but likewise we want to compact the output matrix as bytes, again _row-wise_.
// so the output matrix's dimensions are lPrime by `kappa >> 3 == KappaBytes`, as a _byte_ matrix.
// the technique is fairly straightforward, but involves some bitwise operations.
func transposeBooleanMatrix(input [Kappa][cOtExtendedBlockSizeBytes]byte) [lPrime][KappaBytes]byte {
output := [lPrime][KappaBytes]byte{}
for rowByte := 0; rowByte < KappaBytes; rowByte++ {
for rowBitWithinByte := 0; rowBitWithinByte < 8; rowBitWithinByte++ {
for columnByte := 0; columnByte < cOtExtendedBlockSizeBytes; columnByte++ {
for columnBitWithinByte := 0; columnBitWithinByte < 8; columnBitWithinByte++ {
rowBit := rowByte<<3 + rowBitWithinByte
columnBit := columnByte<<3 + columnBitWithinByte
// the below code grabs the _bit_ at input[rowBit][columnBit], if input were a viewed as a boolean matrix.
// in reality, it's packed into bytes, so instead we have to grab the `columnBitWithinByte`th bit within the appropriate byte.
bitAtInputRowBitColumnBit := input[rowBit][columnByte] >> columnBitWithinByte & 0x01
// now that we've grabbed the bit we care about, we need to write it into the appropriate place in the output matrix
// the output matrix is also packed---but in the "opposite" way (the short dimension is packed, instead of the long one)
// what we're going to do is take the _bit_ we got, and shift it by rowBitWithinByte.
// this has the effect of preparing for us to write it into the appropriate place into the output matrix.
shiftedBit := bitAtInputRowBitColumnBit << rowBitWithinByte
output[columnBit][rowByte] |= shiftedBit
}
}
}
}
return output
}
// Round1Initialize initializes the OT Extension. see page 17, steps 1), 2), 3) and 4) of Protocol 9 of the paper.
// The input `choice` vector is "packed" (i.e., the underlying abstract vector of `L` bits is represented as a `cOTBlockSizeBytes` bytes).
func (receiver *Receiver) Round1Initialize(
uniqueSessionId [simplest.DigestSize]byte,
choice [COtBlockSizeBytes]byte,
) (*Round1Output, error) {
// salt the transcript with the OT-extension session ID
receiver.uniqueSessionId = uniqueSessionId
// write the input choice vector into our local data. Since `otBatchSize` is the number of bits, we are working with
// bytes, we first need to calculate how many bytes are needed to store that many bits.
copy(receiver.extendedPackedChoices[0:COtBlockSizeBytes], choice[:])
// Fill the rest of the extended choice vector with random values. These random values correspond to `gamma^{ext}`.
if _, err := rand.Read(receiver.extendedPackedChoices[COtBlockSizeBytes:]); err != nil {
return nil, errors.Wrap(err, "sampling random coins for gamma^{ext}")
}
v := [2][Kappa][cOtExtendedBlockSizeBytes]byte{} // kappa * L array of _bits_, in "dense" form. contains _both_ v_0 and v_1.
result := &Round1Output{}
hash := sha3.New256() // basically this will contain a hash of the matrix U.
for i := 0; i < Kappa; i++ {
for j := 0; j < 2; j++ {
shake := sha3.NewCShake256(uniqueSessionId[:], []byte("Coinbase_DKLs_cOT"))
if _, err := shake.Write(receiver.seedOtResults.OneTimePadEncryptionKeys[i][j][:]); err != nil {
return nil, errors.Wrap(err, "writing seed OT into shake in cOT receiver round 1")
}
// this is the core pseudorandom expansion of the secret OT input seeds s_i^0 and s_i^1
// see Extension, 2), in Protocol 9, page 17 of DKLs https://eprint.iacr.org/2018/499.pdf
// use the uniqueSessionId as the "domain separator", and the _secret_ seed rho as the input!
if _, err := shake.Read(v[j][i][:]); err != nil {
return nil, errors.Wrap(
err,
"reading from shake to compute v^j in cOT receiver round 1",
)
}
}
for j := 0; j < cOtExtendedBlockSizeBytes; j++ {
result.U[i][j] = v[0][i][j] ^ v[1][i][j] ^ receiver.extendedPackedChoices[j]
// U := v_i^0 ^ v_i^1 ^ w. note: in step 4) of Prot. 9, i think `w` should be bolded?
}
if _, err := hash.Write(result.U[i][:]); err != nil {
return nil, err
}
}
receiver.psi = transposeBooleanMatrix(v[0])
digest := hash.Sum(
nil,
) // go ahead and record this, so that we only have to hash the big matrix U once.
for j := 0; j < lPrime; j++ {
hash = sha3.New256()
jBytes := [2]byte{}
binary.BigEndian.PutUint16(jBytes[:], uint16(j))
if _, err := hash.Write(jBytes[:]); err != nil { // write j into shake
return nil, errors.Wrap(
err,
"writing nonce into hash while computing chiJ in cOT receiver round 1",
)
}
if _, err := hash.Write(digest); err != nil {
return nil, errors.Wrap(
err,
"writing input digest into hash while computing chiJ in cOT receiver round 1",
)
}
chiJ := hash.Sum(nil)
wJ := convertBitToBitmask(
simplest.ExtractBitFromByteVector(receiver.extendedPackedChoices[:], j),
) // extract j^th bit from vector of bytes w.
psiJTimesChiJ := binaryFieldMul(receiver.psi[j][:], chiJ)
for k := 0; k < KappaBytes; k++ {
result.WPrime[k] ^= wJ & chiJ[k]
result.VPrime[k] ^= psiJTimesChiJ[k]
}
}
return result, nil
}
// Round2Transfer computes the OT sender ("Alice")'s part of cOT; this includes steps 2) 5) and 6) of Protocol 9
// `input` is the sender's main vector of inputs alpha_j; these are the things tA_j and tB_j will add to if w_j == 1.
// `message` contains the message the receiver ("Bob") sent us. this itself contains Bob's values WPrime, VPrime, and U
// the output is just the values `Tau` we send back to Bob.
// as a side effect of this function, our (i.e., the sender's) outputs tA_j from the cOT will be populated.
func (sender *Sender) Round2Transfer(
uniqueSessionId [simplest.DigestSize]byte,
input [L][OtWidth]curves.Scalar,
round1Output *Round1Output,
) (*Round2Output, error) {
z := [Kappa][cOtExtendedBlockSizeBytes]byte{}
hash := sha3.New256() // basically this will contain a hash of the matrix U.
for i := 0; i < Kappa; i++ {
v := make(
[]byte,
cOtExtendedBlockSizeBytes,
) // will contain alice's expanded PRG output for the row i, namely v_i^{\Nabla_i}.
shake := sha3.NewCShake256(uniqueSessionId[:], []byte("Coinbase_DKLs_cOT"))
if _, err := shake.Write(sender.seedOtResults.OneTimePadDecryptionKey[i][:]); err != nil {
return nil, errors.Wrap(
err,
"sender writing seed OT decryption key into shake in sender round 2 transfer",
)
}
if _, err := shake.Read(v); err != nil {
return nil, errors.Wrap(
err,
"reading from shake into row `v` in sender round 2 transfer",
)
}
// use the idExt as the domain separator, and the _secret_ seed rho as the input!
mask := convertBitToBitmask(byte(sender.seedOtResults.RandomChoiceBits[i]))
for j := 0; j < cOtExtendedBlockSizeBytes; j++ {
z[i][j] = v[j] ^ mask&round1Output.U[i][j]
}
if _, err := hash.Write(round1Output.U[i][:]); err != nil {
return nil, errors.Wrap(err, "writing matrix U to hash in cOT sender round 2 transfer")
}
}
zeta := transposeBooleanMatrix(z)
digest := hash.Sum(
nil,
) // go ahead and record this, so that we only have to hash the big matrix U once.
zPrime := [simplest.DigestSize]byte{}
for j := 0; j < lPrime; j++ {
hash = sha3.New256()
jBytes := [2]byte{}
binary.BigEndian.PutUint16(jBytes[:], uint16(j))
if _, err := hash.Write(jBytes[:]); err != nil { // write j into hash
return nil, errors.Wrap(
err,
"writing nonce into hash while computing chiJ in cOT sender round 2 transfer",
)
}
if _, err := hash.Write(digest); err != nil {
return nil, errors.Wrap(
err,
"writing input digest into hash while computing chiJ in cOT sender round 2 transfer",
)
}
chiJ := hash.Sum(nil)
zetaJTimesChiJ := binaryFieldMul(zeta[j][:], chiJ)
for k := 0; k < KappaBytes; k++ {
zPrime[k] ^= zetaJTimesChiJ[k]
}
}
rhs := [simplest.DigestSize]byte{}
nablaTimesWPrime := binaryFieldMul(
sender.seedOtResults.PackedRandomChoiceBits,
round1Output.WPrime[:],
)
for i := 0; i < KappaBytes; i++ {
rhs[i] = round1Output.VPrime[i] ^ nablaTimesWPrime[i]
}
if subtle.ConstantTimeCompare(zPrime[:], rhs[:]) != 1 {
return nil, fmt.Errorf(
"cOT receiver's consistency check failed; this may be an attempted attack; do NOT re-run the protocol",
)
}
result := &Round2Output{}
for j := 0; j < L; j++ {
column := make([]byte, OtWidth*simplest.DigestSize)
shake := sha3.NewCShake256(uniqueSessionId[:], []byte("Coinbase_DKLs_cOT"))
jBytes := [2]byte{}
binary.BigEndian.PutUint16(jBytes[:], uint16(j))
if _, err := shake.Write(jBytes[:]); err != nil { // write j into hash
return nil, errors.Wrap(
err,
"writing nonce into shake while computing OutputAdditiveShares in cOT sender round 2 transfer",
)
}
if _, err := shake.Write(zeta[j][:]); err != nil {
return nil, errors.Wrap(
err,
"writing input zeta_j into shake while computing OutputAdditiveShares in cOT sender round 2 transfer",
)
}
if _, err := shake.Read(column[:]); err != nil {
return nil, errors.Wrap(
err,
"reading shake into column while computing OutputAdditiveShares in cOT sender round 2 transfer",
)
}
var err error
for k := 0; k < OtWidth; k++ {
sender.OutputAdditiveShares[j][k], err = sender.curve.Scalar.SetBytes(
column[k*simplest.DigestSize : (k+1)*simplest.DigestSize],
)
if err != nil {
return nil, errors.Wrap(err, "OutputAdditiveShares scalar from bytes")
}
}
for i := 0; i < KappaBytes; i++ {
zeta[j][i] ^= sender.seedOtResults.PackedRandomChoiceBits[i] // note: overwrites zeta_j. just using it as a place to store
}
column = make([]byte, OtWidth*simplest.DigestSize)
shake = sha3.NewCShake256(uniqueSessionId[:], []byte("Coinbase_DKLs_cOT"))
binary.BigEndian.PutUint16(jBytes[:], uint16(j))
if _, err := shake.Write(jBytes[:]); err != nil { // write j into hash
return nil, errors.Wrap(
err,
"writing nonce into shake while computing tau in cOT sender round 2 transfer",
)
}
if _, err := shake.Write(zeta[j][:]); err != nil {
return nil, errors.Wrap(
err,
"writing input zeta_j into shake while computing tau in cOT sender round 2 transfer",
)
}
if _, err := shake.Read(column[:]); err != nil {
return nil, errors.Wrap(
err,
"reading shake into column while computing tau in cOT sender round 2 transfer",
)
}
for k := 0; k < OtWidth; k++ {
result.Tau[j][k], err = sender.curve.Scalar.SetBytes(
column[k*simplest.DigestSize : (k+1)*simplest.DigestSize],
)
if err != nil {
return nil, errors.Wrap(err, "scalar Tau from bytes")
}
result.Tau[j][k] = result.Tau[j][k].Sub(sender.OutputAdditiveShares[j][k])
result.Tau[j][k] = result.Tau[j][k].Add(input[j][k])
}
}
return result, nil
}
// Round3Transfer does the receiver (Bob)'s step 7) of Protocol 9, namely the computation of the outputs tB.
func (receiver *Receiver) Round3Transfer(round2Output *Round2Output) error {
for j := 0; j < L; j++ {
column := make([]byte, OtWidth*simplest.DigestSize)
shake := sha3.NewCShake256(receiver.uniqueSessionId[:], []byte("Coinbase_DKLs_cOT"))
jBytes := [2]byte{}
binary.BigEndian.PutUint16(jBytes[:], uint16(j))
if _, err := shake.Write(jBytes[:]); err != nil { // write j into hash
return errors.Wrap(
err,
"writing nonce into shake while computing tB in cOT receiver round 3 transfer",
)
}
if _, err := shake.Write(receiver.psi[j][:]); err != nil {
return errors.Wrap(
err,
"writing input zeta_j into shake while computing tB in cOT receiver round 3 transfer",
)
}
if _, err := shake.Read(column[:]); err != nil {
return errors.Wrap(
err,
"reading shake into column while computing tB in cOT receiver round 3 transfer",
)
}
bit := int(simplest.ExtractBitFromByteVector(receiver.extendedPackedChoices[:], j))
var err error
for k := 0; k < OtWidth; k++ {
receiver.OutputAdditiveShares[j][k], err = receiver.curve.Scalar.SetBytes(
column[k*simplest.DigestSize : (k+1)*simplest.DigestSize],
)
if err != nil {
return errors.Wrap(err, "scalar output additive shares from bytes")
}
receiver.OutputAdditiveShares[j][k] = receiver.OutputAdditiveShares[j][k].Neg()
wj0 := receiver.OutputAdditiveShares[j][k].Bytes()
wj1 := receiver.OutputAdditiveShares[j][k].Add(round2Output.Tau[j][k]).Bytes()
subtle.ConstantTimeCopy(bit, wj0, wj1)
if receiver.OutputAdditiveShares[j][k], err = receiver.curve.Scalar.SetBytes(wj0); err != nil {
return errors.Wrap(err, "scalar output additive shares from bytes")
}
}
}
return nil
}
+153
View File
@@ -0,0 +1,153 @@
package kos
import (
"crypto/rand"
"testing"
"github.com/stretchr/testify/require"
"github.com/sonr-io/sonr/crypto/core/curves"
"github.com/sonr-io/sonr/crypto/ot/base/simplest"
"github.com/sonr-io/sonr/crypto/ot/ottest"
)
func TestBinaryMult(t *testing.T) {
for i := 0; i < 100; i++ {
temp := make([]byte, 32)
_, err := rand.Read(temp)
require.NoError(t, err)
expected := make([]byte, 32)
copy(expected, temp)
// this test is based on Fermat's little theorem.
// the multiplicative group of units of a finite field has order |F| - 1
// (in fact, it's necessarily cyclic; see e.g. https://math.stackexchange.com/a/59911, but this test doesn't rely on that fact)
// thus raising any element to the |F|th power should yield that element itself.
// this is a good test because it relies on subtle facts about the field structure, and will fail if anything goes wrong.
for j := 0; j < 256; j++ {
expected = binaryFieldMul(expected, expected)
}
require.Equal(t, temp, expected)
}
}
func TestCOTExtension(t *testing.T) {
curveInstances := []*curves.Curve{
curves.K256(),
curves.P256(),
}
for _, curve := range curveInstances {
uniqueSessionId := [simplest.DigestSize]byte{}
_, err := rand.Read(uniqueSessionId[:])
require.NoError(t, err)
baseOtSenderOutput, baseOtReceiverOutput, err := ottest.RunSimplestOT(
curve,
Kappa,
uniqueSessionId,
)
require.NoError(t, err)
for i := 0; i < Kappa; i++ {
require.Equal(
t,
baseOtReceiverOutput.OneTimePadDecryptionKey[i],
baseOtSenderOutput.OneTimePadEncryptionKeys[i][baseOtReceiverOutput.RandomChoiceBits[i]],
)
}
sender := NewCOtSender(baseOtReceiverOutput, curve)
receiver := NewCOtReceiver(baseOtSenderOutput, curve)
choice := [COtBlockSizeBytes]byte{} // receiver's input, namely choice vector. just random
_, err = rand.Read(choice[:])
require.NoError(t, err)
input := [L][OtWidth]curves.Scalar{} // sender's input, namely integer "sums" in case w_j == 1.
for i := 0; i < L; i++ {
for j := 0; j < OtWidth; j++ {
input[i][j] = curve.Scalar.Random(rand.Reader)
require.NoError(t, err)
}
}
firstMessage, err := receiver.Round1Initialize(uniqueSessionId, choice)
require.NoError(t, err)
responseTau, err := sender.Round2Transfer(uniqueSessionId, input, firstMessage)
require.NoError(t, err)
err = receiver.Round3Transfer(responseTau)
require.NoError(t, err)
for j := 0; j < L; j++ {
bit := simplest.ExtractBitFromByteVector(choice[:], j) == 1
for k := 0; k < OtWidth; k++ {
temp := sender.OutputAdditiveShares[j][k].Add(receiver.OutputAdditiveShares[j][k])
if bit {
require.Equal(t, temp, input[j][k])
} else {
require.Equal(t, temp, curve.Scalar.Zero())
}
}
}
}
}
func TestCOTExtensionStreaming(t *testing.T) {
curve := curves.K256()
hashKeySeed := [simplest.DigestSize]byte{}
_, err := rand.Read(hashKeySeed[:])
require.NoError(t, err)
baseOtReceiver, err := simplest.NewReceiver(curve, Kappa, hashKeySeed)
require.NoError(t, err)
sender := NewCOtSender(baseOtReceiver.Output, curve)
baseOtSender, err := simplest.NewSender(curve, Kappa, hashKeySeed)
require.NoError(t, err)
receiver := NewCOtReceiver(baseOtSender.Output, curve)
// first run the seed OT
senderPipe, receiverPipe := simplest.NewPipeWrappers()
errorsChannel := make(chan error, 2)
go func() {
errorsChannel <- simplest.SenderStreamOTRun(baseOtSender, senderPipe)
}()
go func() {
errorsChannel <- simplest.ReceiverStreamOTRun(baseOtReceiver, receiverPipe)
}()
for i := 0; i < 2; i++ {
require.Nil(t, <-errorsChannel)
}
for i := 0; i < Kappa; i++ {
require.Equal(
t,
baseOtReceiver.Output.OneTimePadDecryptionKey[i],
baseOtSender.Output.OneTimePadEncryptionKeys[i][baseOtReceiver.Output.RandomChoiceBits[i]],
)
}
// begin test of cOT extension. first populate both parties' inputs randomly
choice := [COtBlockSizeBytes]byte{} // receiver's input, namely choice vector. just random
_, err = rand.Read(choice[:])
require.NoError(t, err)
input := [L][OtWidth]curves.Scalar{} // sender's input, namely integer "sums" in case w_j == 1. random for the test
for i := 0; i < L; i++ {
for j := 0; j < OtWidth; j++ {
input[i][j] = curve.Scalar.Random(rand.Reader)
require.NoError(t, err)
}
}
// now actually run it, stream-wise
go func() {
errorsChannel <- SenderStreamCOtRun(sender, hashKeySeed, input, receiverPipe)
}()
go func() {
errorsChannel <- ReceiverStreamCOtRun(receiver, hashKeySeed, choice, senderPipe)
}()
for i := 0; i < 2; i++ {
require.Nil(t, <-errorsChannel)
}
for j := 0; j < L; j++ {
bit := simplest.ExtractBitFromByteVector(choice[:], j) == 1
for k := 0; k < OtWidth; k++ {
temp := sender.OutputAdditiveShares[j][k].Add(receiver.OutputAdditiveShares[j][k])
if bit {
require.Equal(t, temp, input[j][k])
} else {
require.Equal(t, temp, curve.Scalar.Zero())
}
}
}
}
+67
View File
@@ -0,0 +1,67 @@
package kos
import (
"encoding/gob"
"io"
"github.com/pkg/errors"
"github.com/sonr-io/sonr/crypto/core/curves"
"github.com/sonr-io/sonr/crypto/ot/base/simplest"
)
// ReceiverStreamCOtRun exposes an end-to-end "streaming" version of the cOT process for the receiver.
// this is similar to what we're also doing in the base OT side. the user only passes an arbitrary `ReadWriter` here,
// together with the relevant inputs (namely a choice vector); this method handles all parts of the process,
// including both encoding / decoding and writing to / reading from the stream.
func ReceiverStreamCOtRun(
receiver *Receiver,
hashKeySeed [simplest.DigestSize]byte,
choice [COtBlockSizeBytes]byte,
rw io.ReadWriter,
) error {
enc := gob.NewEncoder(rw)
dec := gob.NewDecoder(rw)
firstMessage, err := receiver.Round1Initialize(hashKeySeed, choice)
if err != nil {
return errors.Wrap(err, "computing first message in receiver stream cOT")
}
if err = enc.Encode(firstMessage); err != nil {
return errors.Wrap(err, "encoding first message in receiver stream cOT")
}
responseTau := &Round2Output{}
if err = dec.Decode(responseTau); err != nil {
return errors.Wrap(err, "decoding responseTau in receiver stream OT")
}
if err = receiver.Round3Transfer(responseTau); err != nil {
return errors.Wrap(err, "error during round 3 in receiver stream OT")
}
return nil
}
// SenderStreamCOtRun exposes the end-to-end "streaming" version of cOT for the sender.
// the sender should pass an arbitrary ReadWriter together with their input; this will handle the whole process,
// including all component methods, plus reading to and writing from the network.
func SenderStreamCOtRun(
sender *Sender,
hashKeySeed [simplest.DigestSize]byte,
input [L][OtWidth]curves.Scalar,
rw io.ReadWriter,
) error {
enc := gob.NewEncoder(rw)
dec := gob.NewDecoder(rw)
firstMessage := &Round1Output{}
if err := dec.Decode(firstMessage); err != nil {
return errors.Wrap(err, "decoding first message in sender stream cOT")
}
responseTau, err := sender.Round2Transfer(hashKeySeed, input, firstMessage)
if err != nil {
return errors.Wrap(err, "error in round 2 in sender stream cOT")
}
if err = enc.Encode(responseTau); err != nil {
return errors.Wrap(err, "encoding responseTau in sender stream cOT")
}
return nil
}
+54
View File
@@ -0,0 +1,54 @@
// Package ottest contains some utilities to test ot functions. The main goal is to reduce the code duplication in
// various other packages that need to run an OT in their test setup stage.
package ottest
import (
"github.com/pkg/errors"
"github.com/sonr-io/sonr/crypto/core/curves"
"github.com/sonr-io/sonr/crypto/ot/base/simplest"
)
// RunSimplestOT is a utility function used _only_ during various tests.
// essentially, it encapsulates the entire process of running a base OT, so that other tests can use it / bootstrap themselves.
// it handles the creation of the base OT sender and receiver, as well as orchestrates the rounds on them;
// it returns their outsputs, so that others can use them.
func RunSimplestOT(
curve *curves.Curve,
batchSize int,
uniqueSessionId [simplest.DigestSize]byte,
) (*simplest.SenderOutput, *simplest.ReceiverOutput, error) {
receiver, err := simplest.NewReceiver(curve, batchSize, uniqueSessionId)
if err != nil {
return nil, nil, errors.Wrap(err, "constructing OT receiver in run simplest OT")
}
sender, err := simplest.NewSender(curve, batchSize, uniqueSessionId)
if err != nil {
return nil, nil, errors.Wrap(err, "constructing OT sender in run simplest OT")
}
proof, err := sender.Round1ComputeAndZkpToPublicKey()
if err != nil {
return nil, nil, errors.Wrap(err, "sender round 1 in run simplest OT")
}
receiversMaskedChoice, err := receiver.Round2VerifySchnorrAndPadTransfer(proof)
if err != nil {
return nil, nil, errors.Wrap(err, "receiver round 2 in run simplest OT")
}
challenge, err := sender.Round3PadTransfer(receiversMaskedChoice)
if err != nil {
return nil, nil, errors.Wrap(err, "sender round 3 in run simplest OT")
}
challengeResponse, err := receiver.Round4RespondToChallenge(challenge)
if err != nil {
return nil, nil, errors.Wrap(err, "receiver round 4 in run simplest OT")
}
challengeOpenings, err := sender.Round5Verify(challengeResponse)
if err != nil {
return nil, nil, errors.Wrap(err, "sender round 5 in run simplest OT")
}
err = receiver.Round6Verify(challengeOpenings)
if err != nil {
return nil, nil, errors.Wrap(err, "receiver round 6 in run simplest OT")
}
return sender.Output, receiver.Output, nil
}