Skip to content

Commit

Permalink
Merge pull request #40 from katzenpost/add_mkem
Browse files Browse the repository at this point in the history
Add mkem scheme for pigeonhole mixnet protocols
  • Loading branch information
david415 authored Oct 1, 2024
2 parents e6c8202 + 2a1635f commit 5779fd0
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 0 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ require (

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/katzenpost/chacha20poly1305 v0.0.0-20211026103954-7b6fb2fc0129 // indirect
github.com/mattn/go-pointer v0.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/sys v0.16.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
13 changes: 13 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBS
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-faster/xor v1.0.0 h1:2o8vTOgErSGHP3/7XwA5ib1FTtUsNtwCoLLBjl31X38=
github.com/go-faster/xor v1.0.0/go.mod h1:x5CaDY9UKErKzqfRfFZdfu+OSTfoZny3w5Ak7UxcipQ=
github.com/henrydcase/nobs v0.0.0-20230313231516-25b66236df73 h1:d3rq/Tz+RJ5h1xk6Lt3jbObJN3WhvZm7rV41OCIzUyI=
github.com/henrydcase/nobs v0.0.0-20230313231516-25b66236df73/go.mod h1:ptK2MJqVLVEa/V/oK8n+MEyUDCSjSylW+jeNmCG1DJo=
github.com/katzenpost/chacha20 v0.0.0-20190910113340-7ce890d6a556 h1:9gHByAWH1LydGefFGorN1ZBRZ/Oz9iozdzMvRTWpyRw=
github.com/katzenpost/chacha20 v0.0.0-20190910113340-7ce890d6a556/go.mod h1:d9kxwmGOcutgP6bQwr2xaLInaW5yJsxsoPRyUIG0J/E=
github.com/katzenpost/chacha20poly1305 v0.0.0-20211026103954-7b6fb2fc0129 h1:BkXIK2/3bAtqFbzea4tMVfhu8BQ9j05yCVy/z8iCz3s=
github.com/katzenpost/chacha20poly1305 v0.0.0-20211026103954-7b6fb2fc0129/go.mod h1:moU3dWG1uk4xayUNdUPs2Q3dx2Zsk5i9Ysfx+ujQrwM=
github.com/katzenpost/circl v1.3.9-0.20240222183521-1cd9a34e9a0c h1:FYy03rLIjdyjklBOI6YSCb3q7OubTx0dVDWYOgDsvA8=
github.com/katzenpost/circl v1.3.9-0.20240222183521-1cd9a34e9a0c/go.mod h1:+EBrwiGYs9S+qZqaqxujN1CReTNCMAG6p+31KkEDeeA=
github.com/katzenpost/sntrup4591761 v0.0.0-20231024131303-8755eb1986b8 h1:TsKxH0x2RUwf5rBw67k15bqVM3oVbexA9oaTZQLIy3Y=
Expand All @@ -36,16 +40,25 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
gitlab.com/elixxir/crypto v0.0.9 h1:vt7+yRvGd/G9VtpQCaXa7EKPmHTgThf2hXbzZ1vlU6I=
gitlab.com/elixxir/crypto v0.0.9/go.mod h1:KV3F+kO0VVusLIKPqSKMk4HZ4yQiS12SaIsOXg9LJb0=
gitlab.com/xx_network/crypto v0.0.6 h1:+C44rBhclcbWrGa5EOic5yDF3NrXAbXScCb/mXmm3Ro=
gitlab.com/xx_network/crypto v0.0.6/go.mod h1:C69/+XTiqJvKkYzcyA47+LdxvAofl9AzR/Nyo36y9hs=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20190902133755-9109b7679e13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
80 changes: 80 additions & 0 deletions kem/mkem/ciphertext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
// SPDX-FileCopyrightText: Copyright (C) 2024 David Stainton
// SPDX-License-Identifier: AGPL-3.0-only

// Package mkem provides multiparty KEM construction.
package mkem

import (
"github.com/fxamacker/cbor/v2"

"github.com/katzenpost/hpqc/nike"
)

var (
// Create reusable EncMode interface with immutable options, safe for concurrent use.
ccbor cbor.EncMode
)

type Ciphertext struct {
EphemeralPublicKey nike.PublicKey
DEKCiphertexts [][]byte
Envelope []byte
}

type IntermediaryCiphertext struct {
EphemeralPublicKey []byte
DEKCiphertexts [][]byte
Envelope []byte
}

func (i *IntermediaryCiphertext) Bytes() []byte {
blob, err := ccbor.Marshal(i)
if err != nil {
panic(err)
}
return blob
}

func (i *IntermediaryCiphertext) FromBytes(b []byte) error {
err := cbor.Unmarshal(b, i)
if err != nil {
return err
}
return nil
}

func CiphertextFromBytes(scheme *Scheme, b []byte) (*Ciphertext, error) {
ic := &IntermediaryCiphertext{}
err := ic.FromBytes(b)
if err != nil {
return nil, err
}
pubkey, err := scheme.nike.UnmarshalBinaryPublicKey(ic.EphemeralPublicKey)
if err != nil {
return nil, err
}
c := &Ciphertext{
EphemeralPublicKey: pubkey,
DEKCiphertexts: ic.DEKCiphertexts,
Envelope: ic.Envelope,
}
return c, nil
}

func (m *Ciphertext) Marshal() []byte {
ic := &IntermediaryCiphertext{
EphemeralPublicKey: m.EphemeralPublicKey.Bytes(),
DEKCiphertexts: m.DEKCiphertexts,
Envelope: m.Envelope,
}
return ic.Bytes()
}

func init() {
var err error
opts := cbor.CanonicalEncOptions()
ccbor, err = opts.EncMode()
if err != nil {
panic(err)
}
}
133 changes: 133 additions & 0 deletions kem/mkem/mkem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-FileCopyrightText: Copyright (C) 2024 David Stainton
// SPDX-License-Identifier: AGPL-3.0-only

// Package mkem provides multiparty KEM construction.
package mkem

import (
"crypto/cipher"
"crypto/rand"
"errors"

"github.com/katzenpost/chacha20poly1305"
"github.com/katzenpost/hpqc/hash"
"github.com/katzenpost/hpqc/nike"
"github.com/katzenpost/hpqc/nike/hybrid"
)

// Scheme is an MKEM scheme.
type Scheme struct {
nike nike.Scheme
}

func NewScheme() *Scheme {
return &Scheme{
nike: hybrid.CTIDH1024X25519,
}
}

func (s *Scheme) GenerateKeyPair() (nike.PublicKey, nike.PrivateKey, error) {
pubkey, privkey, err := s.nike.GenerateKeyPair()
if err != nil {
return nil, nil, err
}
return pubkey, privkey, nil
}

func (s *Scheme) createCipher(key []byte) cipher.AEAD {
aead, err := chacha20poly1305.New(key)
if err != nil {
panic(err)
}
return aead
}

func (s *Scheme) encrypt(key []byte, plaintext []byte) []byte {
aead := s.createCipher(key)
nonce := make([]byte, aead.NonceSize())
_, err := rand.Reader.Read(nonce)
if err != nil {
panic(err)
}
return aead.Seal(nonce, nonce, plaintext, nil)
}

func (s *Scheme) decrypt(key []byte, ciphertext []byte) ([]byte, error) {
aead := s.createCipher(key)
nonce := ciphertext[:aead.NonceSize()]
ciphertext = ciphertext[aead.NonceSize():]
return aead.Open(nil, nonce, ciphertext, nil)
}

func (s *Scheme) EnvelopeReply(privkey nike.PrivateKey, pubkey nike.PublicKey, plaintext []byte) []byte {
secret := hash.Sum256(s.nike.DeriveSecret(privkey, pubkey))
ciphertext := s.encrypt(secret[:], plaintext)
c := &Ciphertext{
EphemeralPublicKey: pubkey,
DEKCiphertexts: nil,
Envelope: ciphertext,
}
return c.Marshal()
}

func (s *Scheme) DecryptEnvelope(privkey nike.PrivateKey, pubkey nike.PublicKey, ciphertext []byte) ([]byte, error) {
c, err := CiphertextFromBytes(s, ciphertext)
if err != nil {
return nil, err
}

secret := hash.Sum256(s.nike.DeriveSecret(privkey, pubkey))
plaintext, err := s.decrypt(secret[:], c.Envelope)
if err != nil {
return nil, err
}
return plaintext, nil
}

func (s *Scheme) Encapsulate(keys []nike.PublicKey, payload []byte) (nike.PrivateKey, []byte) {
ephPub, ephPriv, err := s.nike.GenerateKeyPair()
if err != nil {
panic(err)
}

secrets := make([][hash.HashSize]byte, len(keys))
for i := 0; i < len(keys); i++ {
secrets[i] = hash.Sum256(s.nike.DeriveSecret(ephPriv, keys[i]))
}

msgKey := make([]byte, 32)
_, err = rand.Reader.Read(msgKey)
if err != nil {
panic(err)
}
ciphertext := s.encrypt(msgKey, payload)

outCiphertexts := make([][]byte, len(secrets))
for i := 0; i < len(secrets); i++ {
outCiphertexts[i] = s.encrypt(secrets[i][:], msgKey)
}

c := &Ciphertext{
EphemeralPublicKey: ephPub,
DEKCiphertexts: outCiphertexts,
Envelope: ciphertext,
}
return ephPriv, c.Marshal()
}

func (s *Scheme) Decapsulate(privkey nike.PrivateKey, ciphertext []byte) ([]byte, error) {
c, err := CiphertextFromBytes(s, ciphertext)
if err != nil {
return nil, err
}

ephSecret := hash.Sum256(s.nike.DeriveSecret(privkey, c.EphemeralPublicKey))
for i := 0; i < len(c.DEKCiphertexts); i++ {
msgKey, err := s.decrypt(ephSecret[:], c.DEKCiphertexts[i])
if err != nil {
continue
}
return s.decrypt(msgKey, c.Envelope)
}
return nil, errors.New("failed to trial decrypt")
}
97 changes: 97 additions & 0 deletions kem/mkem/mkem_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// SPDX-FileCopyrightText: Copyright (C) 2024 David Stainton
// SPDX-License-Identifier: AGPL-3.0-only

// Package mkem provides multiparty KEM construction.
package mkem

import (
"crypto/rand"
"testing"

"github.com/stretchr/testify/require"

"github.com/katzenpost/hpqc/nike"
)

func TestCiphertextMarshaling(t *testing.T) {
ic := &IntermediaryCiphertext{
EphemeralPublicKey: []byte("hello1"),
DEKCiphertexts: [][]byte{[]byte("yo123")},
Envelope: []byte("hello i am ciphertext"),
}
blob1 := ic.Bytes()

ic2 := &IntermediaryCiphertext{}
err := ic2.FromBytes(blob1)
require.NoError(t, err)
blob2 := ic2.Bytes()
require.Equal(t, blob1, blob2)

ic3 := &IntermediaryCiphertext{}
err = ic3.FromBytes(blob2)
require.NoError(t, err)
blob3 := ic3.Bytes()
require.Equal(t, blob1, blob3)
}

func TestMKEMCorrectness(t *testing.T) {
s := NewScheme()

replica1pub, replica1priv, err := s.GenerateKeyPair()
require.NoError(t, err)

replica2pub, replica2priv, err := s.GenerateKeyPair()
require.NoError(t, err)

secret := make([]byte, 32)
_, err = rand.Reader.Read(secret)
require.NoError(t, err)

_, ciphertext := s.Encapsulate([]nike.PublicKey{replica1pub, replica2pub}, secret)

ciphertext2, err := CiphertextFromBytes(s, ciphertext)
require.NoError(t, err)
blob2 := ciphertext2.Marshal()
require.Equal(t, ciphertext, blob2)

secret1, err := s.Decapsulate(replica1priv, blob2)
require.NoError(t, err)

require.Equal(t, secret, secret1)

secret2, err := s.Decapsulate(replica2priv, blob2)
require.NoError(t, err)

require.Equal(t, secret, secret2)
}

func TestMKEMProtocol(t *testing.T) {
s := NewScheme()

// replicas create their keys and publish them
replica1pub, replica1priv, err := s.GenerateKeyPair()
require.NoError(t, err)
replica2pub, _, err := s.GenerateKeyPair()
require.NoError(t, err)

// client to replica
request := make([]byte, 32)
_, err = rand.Reader.Read(request)
require.NoError(t, err)
privKey1, envelopeRaw := s.Encapsulate([]nike.PublicKey{replica1pub, replica2pub}, request)
envelope1, err := CiphertextFromBytes(s, envelopeRaw)
require.NoError(t, err)

// replica decrypts message from client
request1, err := s.Decapsulate(replica1priv, envelopeRaw)
require.NoError(t, err)
require.Equal(t, request1, request)
replyPayload := []byte("hello")
reply1 := s.EnvelopeReply(replica1priv, envelope1.EphemeralPublicKey, replyPayload)

// client decrypts reply from replica
plaintext, err := s.DecryptEnvelope(privKey1, replica1pub, reply1)
require.NoError(t, err)

require.Equal(t, replyPayload, plaintext)
}

0 comments on commit 5779fd0

Please sign in to comment.