Skip to content

Commit

Permalink
Merge 9ecb792 into 81053ac
Browse files Browse the repository at this point in the history
  • Loading branch information
calebdoxsey committed Nov 22, 2022
2 parents 81053ac + 9ecb792 commit d036f3f
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -109,6 +109,7 @@ require (
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/charithe/durationcheck v0.0.9 // indirect
github.com/chavacava/garif v0.0.0-20220630083739-93517212f375 // indirect
github.com/cloudflare/circl v1.3.0
github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc // indirect
github.com/containerd/continuity v0.3.0 // indirect
github.com/curioswitch/go-reassign v0.2.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -174,6 +174,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.0 h1:Anq00jxDtoyX3+aCaYUZ0vXC5r4k4epberfWGDXV1zE=
github.com/cloudflare/circl v1.3.0/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
Expand Down
166 changes: 166 additions & 0 deletions pkg/hpke/hpke.go
@@ -0,0 +1,166 @@
// Package hpke contains functions for working with Hybrid Public Key Encryption.
package hpke

import (
"crypto/rand"
"encoding/base64"
"fmt"

"github.com/cloudflare/circl/hpke"
"github.com/cloudflare/circl/kem"
)

var (
kemID = hpke.KEM_X25519_HKDF_SHA256
kdfID = hpke.KDF_HKDF_SHA256
aeadID = hpke.AEAD_ChaCha20Poly1305
suite = hpke.NewSuite(kemID, kdfID, aeadID)

kdfExpandInfo = []byte("pomerium/hpke")
)

// PrivateKey is an HPKE private key.
type PrivateKey struct {
key kem.PrivateKey
}

// DerivePrivateKey derives a private key from a seed. The same seed will always result in the same private key.
func DerivePrivateKey(seed []byte) PrivateKey {
pk := kdfID.Extract(seed, nil)
data := kdfID.Expand(pk, kdfExpandInfo, uint(kemID.Scheme().SeedSize()))
_, key := kemID.Scheme().DeriveKeyPair(data)
return PrivateKey{key: key}
}

// GeneratePrivateKey generates an HPKE private key.
func GeneratePrivateKey() (PrivateKey, error) {
_, privateKey, err := kemID.Scheme().GenerateKeyPair()
if err != nil {
return PrivateKey{}, err
}
return PrivateKey{key: privateKey}, nil
}

// PrivateKeyFromString takes a string and returns a PrivateKey.
func PrivateKeyFromString(raw string) (PrivateKey, error) {
bs, err := decode(raw)
if err != nil {
return PrivateKey{}, err
}

key, err := kemID.Scheme().UnmarshalBinaryPrivateKey(bs)
if err != nil {
return PrivateKey{}, err
}

return PrivateKey{key: key}, nil
}

// PublicKey returns the public key for the private key.
func (key PrivateKey) PublicKey() PublicKey {
return PublicKey{key: key.key.Public()}
}

// String converts the private key into a string.
func (key PrivateKey) String() string {
bs, err := key.key.MarshalBinary()
if err != nil {
// this should not happen
panic(fmt.Sprintf("failed to marshal private HPKE key: %v", err))
}

return base64.RawStdEncoding.EncodeToString(bs)
}

// PublicKey is an HPKE public key.
type PublicKey struct {
key kem.PublicKey
}

// PublicKeyFromString converts a string into a public key.
func PublicKeyFromString(raw string) (PublicKey, error) {
bs, err := decode(raw)
if err != nil {
return PublicKey{}, err
}

key, err := kemID.Scheme().UnmarshalBinaryPublicKey(bs)
if err != nil {
return PublicKey{}, err
}

return PublicKey{key: key}, nil
}

// String converts a public key into a string.
func (key PublicKey) String() string {
bs, err := key.key.MarshalBinary()
if err != nil {
// this should not happen
panic(fmt.Sprintf("failed to marshal public HPKE key: %v", err))
}

return encode(bs)
}

// Seal seales a message using HPKE.
func Seal(
senderPrivateKey PrivateKey,
receiverPublicKey PublicKey,
message []byte,
) (sealed []byte, err error) {
sender, err := suite.NewSender(receiverPublicKey.key, nil)
if err != nil {
return nil, fmt.Errorf("hpke: error creating sender: %w", err)
}

enc, sealer, err := sender.SetupAuth(rand.Reader, senderPrivateKey.key)
if err != nil {
return nil, fmt.Errorf("hpke: error creating sealer: %w", err)
}

ct, err := sealer.Seal(message, nil)
if err != nil {
return nil, fmt.Errorf("hpke: error sealing message: %w", err)
}

return append(enc, ct...), nil
}

// Open opens a message using HPKE.
func Open(
receiverPrivateKey PrivateKey,
senderPublicKey PublicKey,
sealed []byte,
) (message []byte, err error) {
encSize := kemID.Scheme().SharedKeySize()
if len(sealed) < encSize {
return nil, fmt.Errorf("hpke: invalid sealed message")
}
enc, sealed := sealed[:encSize], sealed[encSize:]

receiver, err := suite.NewReceiver(receiverPrivateKey.key, nil)
if err != nil {
return nil, fmt.Errorf("hpke: error creating receiver: %w", err)
}

opener, err := receiver.SetupAuth(enc, senderPublicKey.key)
if err != nil {
return nil, fmt.Errorf("hpke: error creating opener: %w", err)
}

message, err = opener.Open(sealed, nil)
if err != nil {
return nil, fmt.Errorf("hpke: error opening sealed message: %w", err)
}

return message, nil
}

func decode(raw string) ([]byte, error) {
return base64.RawURLEncoding.DecodeString(raw)
}

func encode(data []byte) string {
return base64.RawURLEncoding.EncodeToString(data)
}
32 changes: 32 additions & 0 deletions pkg/hpke/hpke_test.go
@@ -0,0 +1,32 @@
package hpke

import (
"testing"

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

func TestSeal(t *testing.T) {
k1, err := GeneratePrivateKey()
require.NoError(t, err)
k2, err := GeneratePrivateKey()
require.NoError(t, err)

sealed, err := Seal(k1, k2.PublicKey(), []byte("HELLO WORLD"))
assert.NoError(t, err)
assert.NotEmpty(t, sealed)

message, err := Open(k2, k1.PublicKey(), sealed)
assert.NoError(t, err)
assert.Equal(t, []byte("HELLO WORLD"), message)
}

func TestDerivePrivateKey(t *testing.T) {
k1a := DerivePrivateKey([]byte("KEY 1"))
k1b := DerivePrivateKey([]byte("KEY 1"))
k2 := DerivePrivateKey([]byte("KEY 2"))

assert.Equal(t, k1a.String(), k1b.String())
assert.NotEqual(t, k1a.String(), k2.String())
}
86 changes: 86 additions & 0 deletions pkg/hpke/url.go
@@ -0,0 +1,86 @@
package hpke

import (
"fmt"
"net/url"
)

// URL Parameters
const (
ParamSenderPublicKey = "pomerium_hpke_sender_pub"
ParamQuery = "pomerium_hpke_query"
)

// IsEncryptedURL returns true if the url.Values contain an HPKE encrypted query.
func IsEncryptedURL(values url.Values) bool {
return values.Has(ParamSenderPublicKey) && values.Has(ParamQuery)
}

// EncryptURLValues encrypts URL values using the Seal method.
func EncryptURLValues(
senderPrivateKey PrivateKey,
receiverPublicKey PublicKey,
values url.Values,
) (encrypted url.Values, err error) {
values = withoutHPKEParams(values)

sealed, err := Seal(senderPrivateKey, receiverPublicKey, []byte(values.Encode()))
if err != nil {
return nil, fmt.Errorf("hpke: failed to seal URL values %w", err)
}

return url.Values{
ParamSenderPublicKey: {senderPrivateKey.PublicKey().String()},
ParamQuery: {encode(sealed)},
}, nil
}

// DecryptURLValues decrypts URL values using the Open method.
func DecryptURLValues(
receiverPrivateKey PrivateKey,
encrypted url.Values,
) (senderPublicKey PublicKey, values url.Values, err error) {
if !encrypted.Has(ParamSenderPublicKey) {
return senderPublicKey, nil, fmt.Errorf("hpke: missing sender public key in query parameters")
}
if !encrypted.Has(ParamQuery) {
return senderPublicKey, nil, fmt.Errorf("hpke: missing encrypted query in query parameters")
}

senderPublicKey, err = PublicKeyFromString(encrypted.Get(ParamSenderPublicKey))
if err != nil {
return senderPublicKey, nil, fmt.Errorf("hpke: invalid sender public key parameter: %w", err)
}

sealed, err := decode(encrypted.Get(ParamQuery))
if err != nil {
return senderPublicKey, nil, fmt.Errorf("hpke: failed decoding query parameter: %w", err)
}

message, err := Open(receiverPrivateKey, senderPublicKey, sealed)
if err != nil {
return senderPublicKey, nil, fmt.Errorf("hpke: failed to open sealed message: %w", err)
}

decrypted, err := url.ParseQuery(string(message))
if err != nil {
return senderPublicKey, nil, fmt.Errorf("hpke: invalid query parameter: %w", err)
}

values = withoutHPKEParams(encrypted)
for k, vs := range decrypted {
values[k] = vs
}

return senderPublicKey, values, err
}

func withoutHPKEParams(values url.Values) url.Values {
filtered := make(url.Values)
for k, vs := range values {
if k != ParamSenderPublicKey && k != ParamQuery {
filtered[k] = vs
}
}
return filtered
}
37 changes: 37 additions & 0 deletions pkg/hpke/url_test.go
@@ -0,0 +1,37 @@
package hpke

import (
"net/url"
"testing"

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

func TestEncryptURLValues(t *testing.T) {
k1, err := GeneratePrivateKey()
require.NoError(t, err)
k2, err := GeneratePrivateKey()
require.NoError(t, err)

encrypted, err := EncryptURLValues(k1, k2.PublicKey(), url.Values{
"a": {"b", "c"},
"x": {"y", "z"},
})
assert.NoError(t, err)
assert.True(t, encrypted.Has(ParamSenderPublicKey))
assert.True(t, encrypted.Has(ParamQuery))

assert.True(t, IsEncryptedURL(encrypted))

encrypted.Set("extra", "value")
encrypted.Set("a", "notb")
senderPublicKey, decrypted, err := DecryptURLValues(k2, encrypted)
assert.NoError(t, err)
assert.Equal(t, url.Values{
"a": {"b", "c"},
"x": {"y", "z"},
"extra": {"value"},
}, decrypted)
assert.Equal(t, k1.PublicKey().String(), senderPublicKey.String())
}

0 comments on commit d036f3f

Please sign in to comment.