~bigbes/core-go

a9e49d17a1e63903135b9b3c9a9a69b24451226e — Drew DeVault 5 years ago 98806e7
Overhaul crypto module

Part 1 of merging gql.sr.ht with core-go
5 files changed, 198 insertions(+), 51 deletions(-)

A crypto/crypto.go
A crypto/crypto_test.go
D crypto/webhook.go
M go.mod
M go.sum
A crypto/crypto.go => crypto/crypto.go +99 -0
@@ 0,0 1,99 @@
package crypto

import (
	"crypto/ed25519"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/hex"
	"fmt"
	"log"
	"time"

	"github.com/fernet/fernet-go"
	"github.com/vaughan0/go-ini"
)

var (
	privateKey ed25519.PrivateKey
	publicKey  ed25519.PublicKey
	macKey     []byte
	fernetKey  *fernet.Key
)

func InitCrypto(config ini.File) {
	b64key, ok := config.Get("webhooks", "private-key")
	if !ok {
		log.Fatalf("No webhook key configured")
	}
	seed, err := base64.StdEncoding.DecodeString(b64key)
	if err != nil {
		log.Fatalf("base64 decode webhooks private key: %v", err)
	}
	privateKey = ed25519.NewKeyFromSeed(seed)
	publicKey, _ = privateKey.Public().(ed25519.PublicKey)

	b64fernet, ok := config.Get("sr.ht", "network-key")
	if !ok {
		log.Fatalf("No network key configured")
	}
	fernetKey, err = fernet.DecodeKey(b64fernet)
	if err != nil {
		log.Fatalf("Load Fernet network encryption key: %v", err)
	}
	mac := hmac.New(sha256.New, privateKey)
	mac.Write([]byte("sr.ht HMAC key"))
	macKey = mac.Sum(nil)
}

func Sign(payload []byte) []byte {
	return ed25519.Sign(privateKey, payload)
}

func Verify(payload, signature []byte) bool {
	return ed25519.Verify(publicKey, payload, signature)
}

func Encrypt(payload []byte) []byte {
	msg, err := fernet.EncryptAndSign(payload, fernetKey)
	if err != nil {
		log.Fatalf("Error encrypting payload: %v", err)
	}
	return msg
}

func Decrypt(payload []byte) []byte {
	return fernet.VerifyAndDecrypt(payload,
		time.Duration(0), []*fernet.Key{fernetKey})
}

func DecryptWithExpiration(payload []byte, expiry time.Duration) []byte {
	return fernet.VerifyAndDecrypt(payload, expiry, []*fernet.Key{fernetKey})
}

func HMAC(payload []byte) []byte {
	mac := hmac.New(sha256.New, macKey)
	mac.Write(payload)
	return mac.Sum(nil)
}

func HMACVerify(payload []byte, signature []byte) bool {
	mac := hmac.New(sha256.New, macKey)
	mac.Write(payload)
	expected := mac.Sum(nil)
	return hmac.Equal(expected, signature)
}

func SignWebhook(payload []byte) (string, string) {
	var nonceSeed [8]byte
	_, err := rand.Read(nonceSeed[:])
	if err != nil {
		panic(fmt.Errorf("Failed to generate nonce: %w", err))
	}

	nonce := hex.EncodeToString(nonceSeed[:])
	signature := base64.StdEncoding.EncodeToString(
		Sign(append(payload, []byte(nonce)...)))
	return nonce, signature
}

A crypto/crypto_test.go => crypto/crypto_test.go +85 -0
@@ 0,0 1,85 @@
package crypto

import (
	"encoding/base64"
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/vaughan0/go-ini"
)

func init() {
	config, err := ini.Load(strings.NewReader(`
[webhooks]
private-key=ebzsjPaN6E13ln/FeNWly1C92q6bVMVdOnDo1HPl5fc=

[sr.ht]
network-key=tbuG-7Vh44vrDq1L_HKWkHnWrDOtJhEkPKPiauaLeuk=`))
	if err != nil {
		panic(err)
	}
	InitCrypto(config)
}

func TestSignWebhook(t *testing.T) {
	payload := []byte("Hello world!")
	nonce, signature := SignWebhook(payload)

	sig, err := base64.StdEncoding.DecodeString(signature)
	assert.Nil(t, err)
	valid := Verify(append(payload, []byte(nonce)...), sig)
	assert.True(t, valid)
}

func TestSign(t *testing.T) {
	payload := []byte("Hello world!")
	signature := Sign(payload)

	valid := Verify(payload, signature)
	assert.True(t, valid)

	valid = Verify([]byte("Something else"), signature)
	assert.False(t, valid)
}

func TestEncrypt(t *testing.T) {
	payload := []byte("Hello, world!")

	enc := Encrypt(payload)
	assert.NotNil(t, enc)
	assert.NotEqual(t, enc, []byte("Hello, world!"))

	dec := Decrypt(enc)
	assert.NotNil(t, dec)
	assert.Equal(t, dec, []byte("Hello, world!"))
}

func TestEncryptWithExpire(t *testing.T) {
	payload := []byte("Hello, world!")

	enc := Encrypt(payload)
	assert.NotNil(t, enc)
	assert.NotEqual(t, enc, []byte("Hello, world!"))

	dec := DecryptWithExpiration(enc, 30*time.Minute)
	assert.NotNil(t, dec)
	assert.Equal(t, dec, []byte("Hello, world!"))

	time.Sleep(time.Duration(1))

	dec = DecryptWithExpiration(enc, time.Duration(2))
	assert.Nil(t, dec)
}

func TestHMAC(t *testing.T) {
	payload := []byte("Hello, world!")
	mac := HMAC(payload)

	valid := HMACVerify(payload, mac)
	assert.True(t, valid)

	valid = HMACVerify([]byte("Something else"), mac)
	assert.False(t, valid)
}

D crypto/webhook.go => crypto/webhook.go +0 -51
@@ 1,51 0,0 @@
package crypto

import (
	"crypto/rand"
	"encoding/base64"
	"encoding/hex"
	"log"

	"github.com/vaughan0/go-ini"
	"golang.org/x/crypto/ed25519"
)

var (
	webhookKey ed25519.PrivateKey
)

func initWebhookKey(logger *log.Logger, config ini.File) {
	if webhookKey != nil {
		return
	}

	b64key, ok := config.Get("webhooks", "private-key")
	if !ok {
		logger.Fatalf("No webhook key configured")
	}
	seed, err := base64.StdEncoding.DecodeString(b64key)
	if err != nil {
		logger.Fatalf("base64 decode webhooks private key: %v", err)
	}
	webhookKey = ed25519.NewKeyFromSeed(seed)
}

func SignWebhookPayload(payload []byte, logger *log.Logger, config ini.File) (string, string) {
	var (
		nonceSeed [8]byte
		nonceHex  [16]byte
	)

	_, err := rand.Read(nonceSeed[:])
	if err != nil {
		logger.Fatalf("generate nonce: %v", err)
	}
	hex.Encode(nonceHex[:], nonceSeed[:])
	nonce := string(nonceHex[:])

	initWebhookKey(logger, config)
	signature := base64.StdEncoding.EncodeToString(
		ed25519.Sign(webhookKey, append(payload, nonceHex[:]...)))

	return nonce, signature
}

M go.mod => go.mod +2 -0
@@ 3,6 3,8 @@ module git.sr.ht/~sircmpwn/core-go
go 1.13

require (
	github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001
	github.com/stretchr/testify v1.6.1
	github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec
	golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
)

M go.sum => go.sum +12 -0
@@ 1,3 1,12 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001 h1:/UMxx5lGDg30aioUL9e7xJnbJfJeX7vhcm57fa5udaI=
github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001/go.mod h1:2H9hjfbpSMHwY503FclkV/lZTBh2YlOmLLSda12uL8c=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec h1:DGmKwyZwEB8dI7tbLt/I/gQuP559o/0FrAkHKlQM/Ks=
github.com/vaughan0/go-ini v0.0.0-20130923145212-a98ad7ee00ec/go.mod h1:owBmyHYMLkxyrugmfwE/DLJyW8Ro9mkphwuVErQ0iUw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=


@@ 7,3 16,6 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=