internal/aead : replace miscreant with xchacha20poly1305

This commit is contained in:
Bobby DeSimone 2019-01-16 12:14:51 -08:00
parent d24516f6e4
commit 72922b8ee8
No known key found for this signature in database
GPG key ID: AEE4CF12FE86D07E
9 changed files with 64 additions and 63 deletions

View file

@ -149,7 +149,7 @@ func NewAuthenticator(opts *Options, optionFuncs ...func(*Authenticator) error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cipher, err := aead.NewMiscreantCipher([]byte(decodedAuthCodeSecret)) cipher, err := aead.New([]byte(decodedAuthCodeSecret))
if err != nil { if err != nil {
return nil, err return nil, err
} }

2
go.mod
View file

@ -3,7 +3,6 @@ module github.com/pomerium/pomerium
require ( require (
github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3 github.com/benbjohnson/clock v0.0.0-20161215174838-7dc76406b6d3
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/miscreant/miscreant-go v0.0.0-20181010193435-325cbd69228b
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31
github.com/pomerium/go-oidc v2.0.0+incompatible github.com/pomerium/go-oidc v2.0.0+incompatible
@ -14,6 +13,7 @@ require (
golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 // indirect
google.golang.org/appengine v1.4.0 // indirect google.golang.org/appengine v1.4.0 // indirect
gopkg.in/square/go-jose.v2 v2.2.1 // indirect gopkg.in/square/go-jose.v2 v2.2.1 // indirect
) )

6
go.sum
View file

@ -4,12 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/miscreant/miscreant-go v0.0.0-20181010193435-325cbd69228b h1:VPhrxAgvd0d0xSZP4P8zzWZntso2NE0m69dmDEXM53I=
github.com/miscreant/miscreant-go v0.0.0-20181010193435-325cbd69228b/go.mod h1:Vj6lPE3LxPymcFxg7hm9aDIJWCyhJMnxSNC/y9ZHtN8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pomerium/envconfig v1.3.1-0.20180517194557-dd1402a4d99d h1:iAavGhspmmDa8LCcQLpHV/JBPWKL7EfvMN7YrGHBN3o=
github.com/pomerium/envconfig v1.3.1-0.20180517194557-dd1402a4d99d/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 h1:bNqUesLWa+RUxQvSaV3//dEFviXdCSvMF9GKDOopFLU= github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31 h1:bNqUesLWa+RUxQvSaV3//dEFviXdCSvMF9GKDOopFLU=
github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc= github.com/pomerium/envconfig v1.3.1-0.20190112072701-14cbcf832d31/go.mod h1:1Kz8Ca8PhJDtLYqgvbDZGn6GsJCvrT52SxQ3sPNJkDc=
github.com/pomerium/go-oidc v2.0.0+incompatible h1:gVvG/ExWsHQqatV+uceROnGmbVYF44mDNx5nayBhC0o= github.com/pomerium/go-oidc v2.0.0+incompatible h1:gVvG/ExWsHQqatV+uceROnGmbVYF44mDNx5nayBhC0o=
@ -29,6 +25,8 @@ golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcp
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339 h1:g/Jesu8+QLnA0CPzF3E1pURg0Byr7i6jLoX5sqjcAh0=
golang.org/x/sys v0.0.0-20190116161447-11f53e031339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=

View file

@ -4,19 +4,16 @@ import (
"bytes" "bytes"
"compress/gzip" "compress/gzip"
"crypto/cipher" "crypto/cipher"
"crypto/rand"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"sync" "sync"
miscreant "github.com/miscreant/miscreant-go" "golang.org/x/crypto/chacha20poly1305"
) )
const miscreantNonceSize = 16
var algorithmType = "AES-CMAC-SIV"
// Cipher provides methods to encrypt and decrypt values. // Cipher provides methods to encrypt and decrypt values.
type Cipher interface { type Cipher interface {
Encrypt([]byte) ([]byte, error) Encrypt([]byte) ([]byte, error)
@ -25,42 +22,58 @@ type Cipher interface {
Unmarshal(string, interface{}) error Unmarshal(string, interface{}) error
} }
// MiscreantCipher provides methods to encrypt and decrypt values. // XChaCha20Cipher provides methods to encrypt and decrypt values.
// Using an AEAD is a cipher providing authenticated encryption with associated data. // Using an AEAD is a cipher providing authenticated encryption with associated data.
// For a description of the methodology, see https://en.wikipedia.org/wiki/Authenticated_encryption // For a description of the methodology, see https://en.wikipedia.org/wiki/Authenticated_encryption
type MiscreantCipher struct { type XChaCha20Cipher struct {
aead cipher.AEAD aead cipher.AEAD
mux sync.Mutex mu sync.Mutex
} }
// NewMiscreantCipher returns a new AES Cipher for encrypting values // New returns a new AES Cipher for encrypting values
func NewMiscreantCipher(secret []byte) (*MiscreantCipher, error) { func New(secret []byte) (*XChaCha20Cipher, error) {
aead, err := miscreant.NewAEAD(algorithmType, secret, miscreantNonceSize) aead, err := chacha20poly1305.NewX(secret)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &MiscreantCipher{ return &XChaCha20Cipher{
aead: aead, aead: aead,
}, nil }, nil
} }
// GenerateKey wraps miscreant's GenerateKey function // GenerateKey generates a random 32-byte encryption key.
// Panics if the key size is unsupported or source of randomness fails.
func GenerateKey() []byte { func GenerateKey() []byte {
return miscreant.GenerateKey(32) nonce := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(nonce); err != nil {
panic(err)
}
return nonce
}
// GenerateNonce generates a random 24-byte nonce for XChaCha20-Poly1305.
// Panics if the key size is unsupported or source of randomness fails.
func GenerateNonce() []byte {
nonce := make([]byte, chacha20poly1305.NonceSizeX)
if _, err := rand.Read(nonce); err != nil {
panic(err)
}
return nonce
} }
// Encrypt a value using AES-CMAC-SIV // Encrypt a value using AES-CMAC-SIV
func (c *MiscreantCipher) Encrypt(plaintext []byte) (joined []byte, err error) { func (c *XChaCha20Cipher) Encrypt(plaintext []byte) (joined []byte, err error) {
c.mux.Lock() c.mu.Lock()
defer c.mux.Unlock() defer c.mu.Unlock()
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err = fmt.Errorf("miscreant error encrypting bytes: %v", r) err = fmt.Errorf("internal/aead: error encrypting bytes: %v", r)
} }
}() }()
nonce := miscreant.GenerateNonce(c.aead) nonce := GenerateNonce()
ciphertext := c.aead.Seal(nil, nonce, plaintext, nil) ciphertext := c.aead.Seal(nil, nonce, plaintext, nil)
// we return the nonce as part of the returned value // we return the nonce as part of the returned value
@ -69,15 +82,15 @@ func (c *MiscreantCipher) Encrypt(plaintext []byte) (joined []byte, err error) {
} }
// Decrypt a value using AES-CMAC-SIV // Decrypt a value using AES-CMAC-SIV
func (c *MiscreantCipher) Decrypt(joined []byte) ([]byte, error) { func (c *XChaCha20Cipher) Decrypt(joined []byte) ([]byte, error) {
c.mux.Lock() c.mu.Lock()
defer c.mux.Unlock() defer c.mu.Unlock()
if len(joined) <= miscreantNonceSize { if len(joined) <= chacha20poly1305.NonceSizeX {
return nil, fmt.Errorf("invalid input size: %d", len(joined)) return nil, fmt.Errorf("internal/aead: invalid input size: %d", len(joined))
} }
// grab out the nonce // grab out the nonce
pivot := len(joined) - miscreantNonceSize pivot := len(joined) - chacha20poly1305.NonceSizeX
ciphertext := joined[:pivot] ciphertext := joined[:pivot]
nonce := joined[pivot:] nonce := joined[pivot:]
@ -91,22 +104,22 @@ func (c *MiscreantCipher) Decrypt(joined []byte) ([]byte, error) {
// Marshal marshals the interface state as JSON, encrypts the JSON using the cipher // Marshal marshals the interface state as JSON, encrypts the JSON using the cipher
// and base64 encodes the binary value as a string and returns the result // and base64 encodes the binary value as a string and returns the result
func (c *MiscreantCipher) Marshal(s interface{}) (string, error) { func (c *XChaCha20Cipher) Marshal(s interface{}) (string, error) {
// encode json value // encode json value
plaintext, err := json.Marshal(s) plaintext, err := json.Marshal(s)
if err != nil { if err != nil {
return "", err return "", err
} }
// compress the plaintext bytes
compressed, err := compress(plaintext) compressed, err := compress(plaintext)
if err != nil { if err != nil {
return "", err return "", err
} }
// encrypt the JSON // encrypt the compressed JSON bytes
ciphertext, err := c.Encrypt(compressed) ciphertext, err := c.Encrypt(compressed)
if err != nil { if err != nil {
return "", err return "", err
} }
// base64-encode the result // base64-encode the result
encoded := base64.RawURLEncoding.EncodeToString(ciphertext) encoded := base64.RawURLEncoding.EncodeToString(ciphertext)
return encoded, nil return encoded, nil
@ -114,24 +127,23 @@ func (c *MiscreantCipher) Marshal(s interface{}) (string, error) {
// Unmarshal takes the marshaled string, base64-decodes into a byte slice, decrypts the // Unmarshal takes the marshaled string, base64-decodes into a byte slice, decrypts the
// byte slice the passed cipher, and unmarshals the resulting JSON into the struct pointer passed // byte slice the passed cipher, and unmarshals the resulting JSON into the struct pointer passed
func (c *MiscreantCipher) Unmarshal(value string, s interface{}) error { func (c *XChaCha20Cipher) Unmarshal(value string, s interface{}) error {
// convert base64 string value to bytes // convert base64 string value to bytes
ciphertext, err := base64.RawURLEncoding.DecodeString(value) ciphertext, err := base64.RawURLEncoding.DecodeString(value)
if err != nil { if err != nil {
return err return err
} }
// decrypt the bytes // decrypt the bytes
compressed, err := c.Decrypt(ciphertext) compressed, err := c.Decrypt(ciphertext)
if err != nil { if err != nil {
return err return err
} }
// decompress // decompress the unencrypted bytes
plaintext, err := decompress(compressed) plaintext, err := decompress(compressed)
if err != nil { if err != nil {
return err return err
} }
// unmarshal bytes // unmarshal the unencrypted bytes
err = json.Unmarshal(plaintext, s) err = json.Unmarshal(plaintext, s)
if err != nil { if err != nil {
return err return err
@ -144,13 +156,13 @@ func compress(data []byte) ([]byte, error) {
var buf bytes.Buffer var buf bytes.Buffer
writer, err := gzip.NewWriterLevel(&buf, gzip.DefaultCompression) writer, err := gzip.NewWriterLevel(&buf, gzip.DefaultCompression)
if err != nil { if err != nil {
return nil, fmt.Errorf("aead/compress: failed to create a gzip writer: %q", err) return nil, fmt.Errorf("internal/aead: failed to create a gzip writer: %q", err)
} }
if writer == nil { if writer == nil {
return nil, fmt.Errorf("aead/compress: failed to create a gzip writer") return nil, fmt.Errorf("internal/aead: failed to create a gzip writer")
} }
if _, err = writer.Write(data); err != nil { if _, err = writer.Write(data); err != nil {
return nil, fmt.Errorf("aead/compress: failed to compress data with err: %q", err) return nil, fmt.Errorf("internal/aead: failed to compress data with err: %q", err)
} }
if err = writer.Close(); err != nil { if err = writer.Close(); err != nil {
return nil, err return nil, err
@ -161,7 +173,7 @@ func compress(data []byte) ([]byte, error) {
func decompress(data []byte) ([]byte, error) { func decompress(data []byte) ([]byte, error) {
reader, err := gzip.NewReader(bytes.NewReader(data)) reader, err := gzip.NewReader(bytes.NewReader(data))
if err != nil { if err != nil {
return nil, fmt.Errorf("aead/compress: failed to create a gzip reader: %q", err) return nil, fmt.Errorf("internal/aead: failed to create a gzip reader: %q", err)
} }
defer reader.Close() defer reader.Close()
var buf bytes.Buffer var buf bytes.Buffer

View file

@ -13,7 +13,7 @@ func TestEncodeAndDecodeAccessToken(t *testing.T) {
plaintext := []byte("my plain text value") plaintext := []byte("my plain text value")
key := GenerateKey() key := GenerateKey()
c, err := NewMiscreantCipher([]byte(key)) c, err := New([]byte(key))
if err != nil { if err != nil {
t.Fatalf("unexpected err: %v", err) t.Fatalf("unexpected err: %v", err)
} }
@ -42,7 +42,7 @@ func TestEncodeAndDecodeAccessToken(t *testing.T) {
func TestMarshalAndUnmarshalStruct(t *testing.T) { func TestMarshalAndUnmarshalStruct(t *testing.T) {
key := GenerateKey() key := GenerateKey()
c, err := NewMiscreantCipher([]byte(key)) c, err := New([]byte(key))
if err != nil { if err != nil {
t.Fatalf("unexpected err: %v", err) t.Fatalf("unexpected err: %v", err)
} }
@ -94,17 +94,8 @@ func TestMarshalAndUnmarshalStruct(t *testing.T) {
} }
} }
// TestCipherDataRace exercises a simple concurrency test for the MicscreantCipher.
// In https://github.com/pomerium/pomerium/pull/75 we investigated why, on random occasion,
// unmarshalling session states would fail, triggering users to get kicked out of
// authenticated states. We narrowed our investigation to a data race we uncovered
// from our misuse of the underlying miscreant library which makes no attempt
// at thread-safety.
//
// In https://github.com/pomerium/pomerium/pull/77 we added this test to exercise the
// data race condition and resolved said race by introducing a simple mutex.
func TestCipherDataRace(t *testing.T) { func TestCipherDataRace(t *testing.T) {
miscreantCipher, err := NewMiscreantCipher(GenerateKey()) miscreantCipher, err := New(GenerateKey())
if err != nil { if err != nil {
t.Fatalf("unexpected generating cipher err: %v", err) t.Fatalf("unexpected generating cipher err: %v", err)
} }
@ -116,7 +107,7 @@ func TestCipherDataRace(t *testing.T) {
wg := &sync.WaitGroup{} wg := &sync.WaitGroup{}
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
wg.Add(1) wg.Add(1)
go func(c *MiscreantCipher, wg *sync.WaitGroup) { go func(c *XChaCha20Cipher, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
b := make([]byte, 32) b := make([]byte, 32)
_, err := rand.Read(b) _, err := rand.Read(b)

View file

@ -43,7 +43,7 @@ type CookieStore struct {
// CreateMiscreantCookieCipher creates a new miscreant cipher with the cookie secret // CreateMiscreantCookieCipher creates a new miscreant cipher with the cookie secret
func CreateMiscreantCookieCipher(cookieSecret []byte) func(s *CookieStore) error { func CreateMiscreantCookieCipher(cookieSecret []byte) func(s *CookieStore) error {
return func(s *CookieStore) error { return func(s *CookieStore) error {
cipher, err := aead.NewMiscreantCipher(cookieSecret) cipher, err := aead.New(cookieSecret)
if err != nil { if err != nil {
return fmt.Errorf("miscreant cookie-secret error: %s", err.Error()) return fmt.Errorf("miscreant cookie-secret error: %s", err.Error())
} }

View file

@ -10,7 +10,7 @@ import (
func TestSessionStateSerialization(t *testing.T) { func TestSessionStateSerialization(t *testing.T) {
secret := aead.GenerateKey() secret := aead.GenerateKey()
c, err := aead.NewMiscreantCipher([]byte(secret)) c, err := aead.New([]byte(secret))
if err != nil { if err != nil {
t.Fatalf("expected to be able to create cipher: %v", err) t.Fatalf("expected to be able to create cipher: %v", err)
} }

View file

@ -144,7 +144,7 @@ func NewProxy(opts *Options) (*Proxy, error) {
// error explicitly handled by validate // error explicitly handled by validate
decodedSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret) decodedSecret, _ := base64.StdEncoding.DecodeString(opts.CookieSecret)
cipher, err := aead.NewMiscreantCipher(decodedSecret) cipher, err := aead.New(decodedSecret)
if err != nil { if err != nil {
return nil, fmt.Errorf("cookie-secret error: %s", err.Error()) return nil, fmt.Errorf("cookie-secret error: %s", err.Error())
} }