mirror of
https://github.com/pomerium/pomerium.git
synced 2025-08-03 00:40:25 +02:00
hpke: add hpke package
This commit is contained in:
parent
81053ac8ef
commit
dc0c97e749
6 changed files with 324 additions and 0 deletions
1
go.mod
1
go.mod
|
@ -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
|
||||
|
|
2
go.sum
2
go.sum
|
@ -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=
|
||||
|
|
166
pkg/hpke/hpke.go
Normal file
166
pkg/hpke/hpke.go
Normal file
|
@ -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
pkg/hpke/hpke_test.go
Normal file
32
pkg/hpke/hpke_test.go
Normal file
|
@ -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
pkg/hpke/url.go
Normal file
86
pkg/hpke/url.go
Normal file
|
@ -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, 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: invalid query parameter: %w", err)
|
||||
}
|
||||
|
||||
message, err := Open(receiverPrivateKey, senderPublicKey, sealed)
|
||||
if err != nil {
|
||||
return senderPublicKey, nil, fmt.Errorf("hpke: invalid query parameter: %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
pkg/hpke/url_test.go
Normal file
37
pkg/hpke/url_test.go
Normal file
|
@ -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())
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue