mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-03 20:36:03 +02:00
hpke: compress query string (#4147)
* hpke: compress query string * only use v2 in authenticate if v2 was used for the initial request * fix comment
This commit is contained in:
parent
69713d38af
commit
facf9ab093
6 changed files with 184 additions and 52 deletions
|
@ -218,7 +218,12 @@ func (a *Authenticate) SignIn(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
|
||||||
a.logAuthenticateEvent(r, profile)
|
a.logAuthenticateEvent(r, profile)
|
||||||
|
|
||||||
redirectTo, err := urlutil.CallbackURL(state.hpkePrivateKey, proxyPublicKey, requestParams, profile)
|
encryptURLValues := hpke.EncryptURLValuesV1
|
||||||
|
if hpke.IsEncryptedURLV2(r.Form) {
|
||||||
|
encryptURLValues = hpke.EncryptURLValuesV2
|
||||||
|
}
|
||||||
|
|
||||||
|
redirectTo, err := urlutil.CallbackURL(state.hpkePrivateKey, proxyPublicKey, requestParams, profile, encryptURLValues)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return httputil.NewError(http.StatusInternalServerError, err)
|
return httputil.NewError(http.StatusInternalServerError, err)
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -156,7 +156,7 @@ require (
|
||||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgx/v5 v5.3.1
|
github.com/jackc/pgx/v5 v5.3.1
|
||||||
github.com/klauspost/compress v1.16.0 // indirect
|
github.com/klauspost/compress v1.16.0
|
||||||
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
github.com/libdns/libdns v0.2.1 // indirect
|
github.com/libdns/libdns v0.2.1 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||||
|
|
|
@ -40,6 +40,7 @@ func CallbackURL(
|
||||||
proxyPublicKey *hpke.PublicKey,
|
proxyPublicKey *hpke.PublicKey,
|
||||||
requestParams url.Values,
|
requestParams url.Values,
|
||||||
profile *identity.Profile,
|
profile *identity.Profile,
|
||||||
|
encryptURLValues hpke.EncryptURLValuesFunc,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
redirectURL, err := ParseAndValidateURL(requestParams.Get(QueryRedirectURI))
|
redirectURL, err := ParseAndValidateURL(requestParams.Get(QueryRedirectURI))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -76,7 +77,7 @@ func CallbackURL(
|
||||||
|
|
||||||
BuildTimeParameters(callbackParams, signInExpiry)
|
BuildTimeParameters(callbackParams, signInExpiry)
|
||||||
|
|
||||||
callbackParams, err = hpke.EncryptURLValues(authenticatePrivateKey, proxyPublicKey, callbackParams)
|
callbackParams, err = encryptURLValues(authenticatePrivateKey, proxyPublicKey, callbackParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("error encrypting callback params: %w", err)
|
return "", fmt.Errorf("error encrypting callback params: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -115,7 +116,7 @@ func SignInURL(
|
||||||
q.Set(QueryVersion, versionStr())
|
q.Set(QueryVersion, versionStr())
|
||||||
q.Set(QueryRequestUUID, uuid.NewString())
|
q.Set(QueryRequestUUID, uuid.NewString())
|
||||||
BuildTimeParameters(q, signInExpiry)
|
BuildTimeParameters(q, signInExpiry)
|
||||||
q, err := hpke.EncryptURLValues(senderPrivateKey, authenticatePublicKey, q)
|
q, err := hpke.EncryptURLValuesV2(senderPrivateKey, authenticatePublicKey, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ func TestCallbackURL(t *testing.T) {
|
||||||
QueryRedirectURI: {"https://redirect.example.com"},
|
QueryRedirectURI: {"https://redirect.example.com"},
|
||||||
}, &identity.Profile{
|
}, &identity.Profile{
|
||||||
ProviderId: "IDP-1",
|
ProviderId: "IDP-1",
|
||||||
})
|
}, hpke.EncryptURLValuesV1)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
signInURL, err := ParseAndValidateURL(rawSignInURL)
|
signInURL, err := ParseAndValidateURL(rawSignInURL)
|
||||||
|
|
122
pkg/hpke/url.go
122
pkg/hpke/url.go
|
@ -3,35 +3,76 @@ package hpke
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/klauspost/compress/zstd"
|
||||||
)
|
)
|
||||||
|
|
||||||
// URL Parameters
|
// URL Parameters
|
||||||
const (
|
const (
|
||||||
ParamSenderPublicKey = "pomerium_hpke_sender_pub"
|
paramSenderPublicKey = "pomerium_hpke_sender_pub"
|
||||||
ParamQuery = "pomerium_hpke_query"
|
paramQuery = "pomerium_hpke_query"
|
||||||
|
|
||||||
|
paramSenderPublicKeyV2 = "k"
|
||||||
|
paramQueryV2 = "q"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsEncryptedURL returns true if the url.Values contain an HPKE encrypted query.
|
// IsEncryptedURL returns true if the url.Values contain an HPKE encrypted query.
|
||||||
func IsEncryptedURL(values url.Values) bool {
|
func IsEncryptedURL(values url.Values) bool {
|
||||||
return values.Has(ParamSenderPublicKey) && values.Has(ParamQuery)
|
return IsEncryptedURLV1(values) || IsEncryptedURLV2(values)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncryptURLValues encrypts URL values using the Seal method.
|
// IsEncryptedURLV1 returns true if the url.Values contain a V1 HPKE encrypted query.
|
||||||
func EncryptURLValues(
|
func IsEncryptedURLV1(values url.Values) bool {
|
||||||
|
return values.Has(paramSenderPublicKey) && values.Has(paramQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEncryptedURLV2 returns true if the url.Values contains a V2 HPKE encrypted query.
|
||||||
|
func IsEncryptedURLV2(values url.Values) bool {
|
||||||
|
return values.Has(paramSenderPublicKeyV2) && values.Has(paramQueryV2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An EncryptURLValuesFunc is a function that encrypts url values.
|
||||||
|
type EncryptURLValuesFunc func(senderPrivateKey *PrivateKey, receiverPublicKey *PublicKey, values url.Values) (encrypted url.Values, err error)
|
||||||
|
|
||||||
|
// EncryptURLValuesV1 encrypts URL values using the Seal method.
|
||||||
|
func EncryptURLValuesV1(
|
||||||
senderPrivateKey *PrivateKey,
|
senderPrivateKey *PrivateKey,
|
||||||
receiverPublicKey *PublicKey,
|
receiverPublicKey *PublicKey,
|
||||||
values url.Values,
|
values url.Values,
|
||||||
) (encrypted url.Values, err error) {
|
) (encrypted url.Values, err error) {
|
||||||
values = withoutHPKEParams(values)
|
values = withoutHPKEParams(values)
|
||||||
|
|
||||||
sealed, err := Seal(senderPrivateKey, receiverPublicKey, []byte(values.Encode()))
|
encoded := encodeQueryStringV1(values)
|
||||||
|
|
||||||
|
sealed, err := Seal(senderPrivateKey, receiverPublicKey, encoded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("hpke: failed to seal URL values %w", err)
|
return nil, fmt.Errorf("hpke: failed to seal URL values %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return url.Values{
|
return url.Values{
|
||||||
ParamSenderPublicKey: {senderPrivateKey.PublicKey().String()},
|
paramSenderPublicKey: {senderPrivateKey.PublicKey().String()},
|
||||||
ParamQuery: {encode(sealed)},
|
paramQuery: {encode(sealed)},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptURLValuesV2 encrypts URL values using the Seal method and compresses the query string.
|
||||||
|
func EncryptURLValuesV2(
|
||||||
|
senderPrivateKey *PrivateKey,
|
||||||
|
receiverPublicKey *PublicKey,
|
||||||
|
values url.Values,
|
||||||
|
) (encrypted url.Values, err error) {
|
||||||
|
values = withoutHPKEParams(values)
|
||||||
|
|
||||||
|
encoded := encodeQueryStringV2(values)
|
||||||
|
|
||||||
|
sealed, err := Seal(senderPrivateKey, receiverPublicKey, encoded)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("hpke: failed to seal URL values %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.Values{
|
||||||
|
paramSenderPublicKeyV2: {senderPrivateKey.PublicKey().String()},
|
||||||
|
paramQueryV2: {encode(sealed)},
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,19 +81,15 @@ func DecryptURLValues(
|
||||||
receiverPrivateKey *PrivateKey,
|
receiverPrivateKey *PrivateKey,
|
||||||
encrypted url.Values,
|
encrypted url.Values,
|
||||||
) (senderPublicKey *PublicKey, values url.Values, err error) {
|
) (senderPublicKey *PublicKey, values url.Values, err error) {
|
||||||
if !encrypted.Has(ParamSenderPublicKey) {
|
var decrypted url.Values
|
||||||
return nil, nil, fmt.Errorf("hpke: missing sender public key in query parameters")
|
switch {
|
||||||
}
|
case IsEncryptedURLV1(encrypted):
|
||||||
if !encrypted.Has(ParamQuery) {
|
senderPublicKey, err = PublicKeyFromString(encrypted.Get(paramSenderPublicKey))
|
||||||
return nil, nil, fmt.Errorf("hpke: missing encrypted query in query parameters")
|
|
||||||
}
|
|
||||||
|
|
||||||
senderPublicKey, err = PublicKeyFromString(encrypted.Get(ParamSenderPublicKey))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("hpke: invalid sender public key parameter: %w", err)
|
return nil, nil, fmt.Errorf("hpke: invalid sender public key parameter: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed, err := decode(encrypted.Get(ParamQuery))
|
sealed, err := decode(encrypted.Get(paramQuery))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("hpke: failed decoding query parameter: %w", err)
|
return nil, nil, fmt.Errorf("hpke: failed decoding query parameter: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -62,10 +99,33 @@ func DecryptURLValues(
|
||||||
return nil, nil, fmt.Errorf("hpke: failed to open sealed message: %w", err)
|
return nil, nil, fmt.Errorf("hpke: failed to open sealed message: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
decrypted, err := url.ParseQuery(string(message))
|
decrypted, err = decodeQueryStringV1(message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("hpke: invalid query parameter: %w", err)
|
return nil, nil, fmt.Errorf("hpke: invalid query parameter: %w", err)
|
||||||
}
|
}
|
||||||
|
case IsEncryptedURLV2(encrypted):
|
||||||
|
senderPublicKey, err = PublicKeyFromString(encrypted.Get(paramSenderPublicKeyV2))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("hpke: invalid sender public key parameter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed, err := decode(encrypted.Get(paramQueryV2))
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("hpke: failed decoding query parameter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
message, err := Open(receiverPrivateKey, senderPublicKey, sealed)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("hpke: failed to open sealed message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypted, err = decodeQueryStringV2(message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("hpke: invalid query parameter: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("hpke: missing query parameters")
|
||||||
|
}
|
||||||
|
|
||||||
values = withoutHPKEParams(encrypted)
|
values = withoutHPKEParams(encrypted)
|
||||||
for k, vs := range decrypted {
|
for k, vs := range decrypted {
|
||||||
|
@ -78,9 +138,33 @@ func DecryptURLValues(
|
||||||
func withoutHPKEParams(values url.Values) url.Values {
|
func withoutHPKEParams(values url.Values) url.Values {
|
||||||
filtered := make(url.Values)
|
filtered := make(url.Values)
|
||||||
for k, vs := range values {
|
for k, vs := range values {
|
||||||
if k != ParamSenderPublicKey && k != ParamQuery {
|
if k != paramSenderPublicKey && k != paramQuery && k != paramSenderPublicKeyV2 && k != paramQueryV2 {
|
||||||
filtered[k] = vs
|
filtered[k] = vs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var zstdEncoder, _ = zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
|
||||||
|
|
||||||
|
func encodeQueryStringV1(values url.Values) []byte {
|
||||||
|
return []byte(values.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeQueryStringV2(values url.Values) []byte {
|
||||||
|
return zstdEncoder.EncodeAll([]byte(values.Encode()), nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zstdDecoder, _ = zstd.NewReader(nil)
|
||||||
|
|
||||||
|
func decodeQueryStringV1(raw []byte) (url.Values, error) {
|
||||||
|
return url.ParseQuery(string(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeQueryStringV2(raw []byte) (url.Values, error) {
|
||||||
|
bs, err := zstdDecoder.DecodeAll(raw, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return url.ParseQuery(string(bs))
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ package hpke
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -9,18 +10,23 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEncryptURLValues(t *testing.T) {
|
func TestEncryptURLValues(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
k1, err := GeneratePrivateKey()
|
k1, err := GeneratePrivateKey()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
k2, err := GeneratePrivateKey()
|
k2, err := GeneratePrivateKey()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
encrypted, err := EncryptURLValues(k1, k2.PublicKey(), url.Values{
|
t.Run("v1", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encrypted, err := EncryptURLValuesV1(k1, k2.PublicKey(), url.Values{
|
||||||
"a": {"b", "c"},
|
"a": {"b", "c"},
|
||||||
"x": {"y", "z"},
|
"x": {"y", "z"},
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.True(t, encrypted.Has(ParamSenderPublicKey))
|
assert.True(t, encrypted.Has(paramSenderPublicKey))
|
||||||
assert.True(t, encrypted.Has(ParamQuery))
|
assert.True(t, encrypted.Has(paramQuery))
|
||||||
|
|
||||||
assert.True(t, IsEncryptedURL(encrypted))
|
assert.True(t, IsEncryptedURL(encrypted))
|
||||||
|
|
||||||
|
@ -34,4 +40,40 @@ func TestEncryptURLValues(t *testing.T) {
|
||||||
"extra": {"value"},
|
"extra": {"value"},
|
||||||
}, decrypted)
|
}, decrypted)
|
||||||
assert.Equal(t, k1.PublicKey().String(), senderPublicKey.String())
|
assert.Equal(t, k1.PublicKey().String(), senderPublicKey.String())
|
||||||
|
})
|
||||||
|
t.Run("v2", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encrypted, err := EncryptURLValuesV2(k1, k2.PublicKey(), url.Values{
|
||||||
|
"a": {"b", "c"},
|
||||||
|
"x": {"y", "z"},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, encrypted.Has(paramSenderPublicKeyV2))
|
||||||
|
assert.True(t, encrypted.Has(paramQueryV2))
|
||||||
|
|
||||||
|
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())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("compresses", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
encrypted, err := EncryptURLValuesV2(k1, k2.PublicKey(), url.Values{
|
||||||
|
"a": {strings.Repeat("b", 1024*128)},
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Less(t, len(encrypted.Encode()), 1024)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue