-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #40 from katzenpost/add_mkem
Add mkem scheme for pigeonhole mixnet protocols
- Loading branch information
Showing
5 changed files
with
326 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |