Skip to content

Commit

Permalink
crypto/olm: add tests comparing libolm and goolm, replace crypto/ed25…
Browse files Browse the repository at this point in the history
…519 -> maunium.net/go/mautrix/crypto/ed25519

The following tests were added that compare libolm and goolm against
each other.

* account: add (un)pickle tests
* groupsession: add test for (en|de)cryption for group sessions
* account: test IdentityKeysJSON and OneTimeKeys
* session: add test for encrypt/decrypt
* session: add test for private key format
* outboundsession: add differential fuzz test for encryption

Signed-off-by: Sumner Evans <[email protected]>
  • Loading branch information
sumnerevans committed Aug 23, 2024
1 parent 213b6ec commit 66c4178
Show file tree
Hide file tree
Showing 8 changed files with 396 additions and 22 deletions.
16 changes: 12 additions & 4 deletions crypto/ed25519/ed25519.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,19 @@
// Package ed25519 implements the Ed25519 signature algorithm. See
// https://ed25519.cr.yp.to/.
//
// This package stores the private key in a different format than the
// [crypto/ed25519] package in the standard library.
// This package stores the private key in the NaCl format, which is a different
// format than that used by the [crypto/ed25519] package in the standard
// library.
//
// This picture will help with the rest of the explanation:
// https://blog.mozilla.org/warner/files/2011/11/key-formats.png
//
// The private key in the [crypto/ed25519] package is a 64-byte value where the
// first 32-bytes are the seed and the last 32-bytes are the public key.
//
// The private key in this package is stored as a 64-byte value that results
// from the SHA512 of the seed.
// The private key in this package is stored in the NaCl format. That is, the
// left 32-bytes are the private scalar A and the right 32-bytes are the right
// half of the SHA512 result.
//
// The contents of this package are mostly copied from the standard library,
// and as such the source code is licensed under the BSD license of the
Expand Down Expand Up @@ -187,6 +189,12 @@ func newKeyFromSeed(privateKey, seed []byte) {
}

h := sha512.Sum512(seed)

// Apply clamping to get A in the left half, and leave the right half
// as-is. This gets the private key into the NaCl format.
h[0] &= 248
h[31] &= 63
h[31] |= 64
copy(privateKey, h[:])
}

Expand Down
10 changes: 7 additions & 3 deletions crypto/goolm/crypto/ed25519.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package crypto

import (
"crypto/ed25519"
"encoding/base64"
"fmt"
"io"

"maunium.net/go/mautrix/crypto/ed25519"
"maunium.net/go/mautrix/crypto/goolm/libolmpickle"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/id"
Expand Down Expand Up @@ -123,12 +123,16 @@ func (c Ed25519PrivateKey) Equal(x Ed25519PrivateKey) bool {
// PubKey returns the public key derived from the private key.
func (c Ed25519PrivateKey) PubKey() Ed25519PublicKey {
publicKey := ed25519.PrivateKey(c).Public()
return Ed25519PublicKey(publicKey.(ed25519.PublicKey))
return Ed25519PublicKey(publicKey.([]byte))
}

// Sign returns the signature for the message.
func (c Ed25519PrivateKey) Sign(message []byte) []byte {
return ed25519.Sign(ed25519.PrivateKey(c), message)
signature, err := ed25519.PrivateKey(c).Sign(nil, message, &ed25519.Options{})
if err != nil {
panic(err)
}
return signature
}

// Ed25519PublicKey represents the public key for ed25519 usage. This is just a wrapper.
Expand Down
9 changes: 3 additions & 6 deletions crypto/goolm/session/megolm_outbound_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"errors"
"fmt"

"maunium.net/go/mautrix/id"
"go.mau.fi/util/exerrors"

"maunium.net/go/mautrix/crypto/goolm/cipher"
"maunium.net/go/mautrix/crypto/goolm/crypto"
Expand All @@ -15,6 +15,7 @@ import (
"maunium.net/go/mautrix/crypto/goolm/megolm"
"maunium.net/go/mautrix/crypto/goolm/utilities"
"maunium.net/go/mautrix/crypto/olm"
"maunium.net/go/mautrix/id"
)

const (
Expand Down Expand Up @@ -187,9 +188,5 @@ func (s *MegolmOutboundSession) MessageIndex() uint {

// Key returns the base64-encoded current ratchet key for this session.
func (s *MegolmOutboundSession) Key() string {
message, err := s.SessionSharingMessage()
if err != nil {
panic(err)
}
return string(message)
return string(exerrors.Must(s.SessionSharingMessage()))
}
122 changes: 122 additions & 0 deletions crypto/olm/account_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) 2024 Sumner Evans
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package olm_test

import (
"bytes"
"encoding/base64"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mau.fi/util/exerrors"

"maunium.net/go/mautrix/crypto/ed25519"
"maunium.net/go/mautrix/crypto/goolm/account"
"maunium.net/go/mautrix/crypto/libolm"
"maunium.net/go/mautrix/crypto/olm"
)

func ensureAccountsEqual(t *testing.T, a, b olm.Account) {
t.Helper()

assert.Equal(t, a.MaxNumberOfOneTimeKeys(), b.MaxNumberOfOneTimeKeys())

aEd25519, aCurve25519, err := a.IdentityKeys()
require.NoError(t, err)
bEd25519, bCurve25519, err := b.IdentityKeys()
require.NoError(t, err)
assert.Equal(t, aEd25519, bEd25519)
assert.Equal(t, aCurve25519, bCurve25519)

aIdentityKeysJSON, err := a.IdentityKeysJSON()
require.NoError(t, err)
bIdentityKeysJSON, err := b.IdentityKeysJSON()
require.NoError(t, err)
assert.JSONEq(t, string(aIdentityKeysJSON), string(bIdentityKeysJSON))

aOTKs, err := a.OneTimeKeys()
require.NoError(t, err)
bOTKs, err := b.OneTimeKeys()
require.NoError(t, err)
assert.Equal(t, aOTKs, bOTKs)
}

// TestAccount_UnpickleLibolmToGoolm tests creating an account from libolm,
// pickling it, and importing it into goolm.
func TestAccount_UnpickleLibolmToGoolm(t *testing.T) {
libolmAccount, err := libolm.NewAccount(nil)
require.NoError(t, err)

require.NoError(t, libolmAccount.GenOneTimeKeys(nil, 50))

libolmPickled, err := libolmAccount.Pickle([]byte("test"))
require.NoError(t, err)

goolmAccount, err := account.AccountFromPickled(libolmPickled, []byte("test"))
require.NoError(t, err)

ensureAccountsEqual(t, libolmAccount, goolmAccount)

goolmPickled, err := goolmAccount.Pickle([]byte("test"))
require.NoError(t, err)
assert.Equal(t, libolmPickled, goolmPickled)
}

// TestAccount_UnpickleGoolmToLibolm tests creating an account from goolm,
// pickling it, and importing it into libolm.
func TestAccount_UnpickleGoolmToLibolm(t *testing.T) {
goolmAccount, err := account.NewAccount(nil)
require.NoError(t, err)

require.NoError(t, goolmAccount.GenOneTimeKeys(nil, 50))

goolmPickled, err := goolmAccount.Pickle([]byte("test"))
require.NoError(t, err)

libolmAccount, err := libolm.AccountFromPickled(bytes.Clone(goolmPickled), []byte("test"))
require.NoError(t, err)

ensureAccountsEqual(t, libolmAccount, goolmAccount)

libolmPickled, err := libolmAccount.Pickle([]byte("test"))
require.NoError(t, err)
assert.Equal(t, goolmPickled, libolmPickled)
}

func FuzzAccount_Sign(f *testing.F) {
f.Add([]byte("anything"))

libolmAccount := exerrors.Must(libolm.NewAccount(nil))
goolmAccount := exerrors.Must(account.AccountFromPickled(exerrors.Must(libolmAccount.Pickle([]byte("test"))), []byte("test")))

f.Fuzz(func(t *testing.T, message []byte) {
if len(message) == 0 {
t.Skip("empty message is not supported")
}

libolmSignature, err := libolmAccount.Sign(bytes.Clone(message))
require.NoError(t, err)
goolmSignature, err := goolmAccount.Sign(bytes.Clone(message))
require.NoError(t, err)
assert.Equal(t, goolmSignature, libolmSignature)

goolmSignatureBytes, err := base64.RawStdEncoding.DecodeString(string(goolmSignature))
require.NoError(t, err)
libolmSignatureBytes, err := base64.RawStdEncoding.DecodeString(string(libolmSignature))
require.NoError(t, err)

libolmEd25519, _, err := libolmAccount.IdentityKeys()
require.NoError(t, err)

assert.True(t, ed25519.Verify(ed25519.PublicKey(libolmEd25519.Bytes()), message, libolmSignatureBytes))
assert.True(t, ed25519.Verify(ed25519.PublicKey(libolmEd25519.Bytes()), message, goolmSignatureBytes))

assert.True(t, goolmAccount.IdKeys.Ed25519.Verify(bytes.Clone(message), libolmSignatureBytes))
assert.True(t, goolmAccount.IdKeys.Ed25519.Verify(bytes.Clone(message), goolmSignatureBytes))
})
}
6 changes: 6 additions & 0 deletions crypto/olm/errors.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
// Copyright (c) 2024 Sumner Evans
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package olm

import "errors"
Expand Down
47 changes: 47 additions & 0 deletions crypto/olm/groupsession_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package olm_test

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"maunium.net/go/mautrix/crypto/goolm/session"
"maunium.net/go/mautrix/crypto/libolm"
)

// TestEncryptDecrypt_GoolmToLibolm tests encryption where goolm encrypts and libolm decrypts
func TestEncryptDecrypt_GoolmToLibolm(t *testing.T) {
goolmOutbound, err := session.NewMegolmOutboundSession()
require.NoError(t, err)

libolmInbound, err := libolm.NewInboundGroupSession([]byte(goolmOutbound.Key()))
require.NoError(t, err)

for i := 0; i < 10; i++ {
ciphertext, err := goolmOutbound.Encrypt([]byte(fmt.Sprintf("message %d", i)))
require.NoError(t, err)

plaintext, msgIdx, err := libolmInbound.Decrypt(ciphertext)
assert.NoError(t, err)
assert.Equal(t, []byte(fmt.Sprintf("message %d", i)), plaintext)
assert.Equal(t, goolmOutbound.MessageIndex()-1, msgIdx)
}
}

func TestEncryptDecrypt_LibolmToGoolm(t *testing.T) {
libolmOutbound := libolm.NewOutboundGroupSession()
goolmInbound, err := session.NewMegolmInboundSession([]byte(libolmOutbound.Key()))
require.NoError(t, err)

for i := 0; i < 10; i++ {
ciphertext, err := libolmOutbound.Encrypt([]byte(fmt.Sprintf("message %d", i)))
require.NoError(t, err)

plaintext, msgIdx, err := goolmInbound.Decrypt(ciphertext)
assert.NoError(t, err)
assert.Equal(t, []byte(fmt.Sprintf("message %d", i)), plaintext)
assert.Equal(t, libolmOutbound.MessageIndex()-1, msgIdx)
}
}
Loading

0 comments on commit 66c4178

Please sign in to comment.