Compare commits

...

4 commits

Author SHA1 Message Date
92ab51b13d
Add comprehensive tests for scramAuth error handling
Introduce new test cases to validate the error handling of the scramAuth methods. These tests cover scenarios such as invalid characters in usernames, empty inputs, and edge cases like broken rand.Reader.
2024-11-09 00:06:33 +01:00
c6d416f142
Fix typo in the tls-unique channel binding comment
Corrected "crypto/tl" to "crypto/tls" in the comment for better clarity and accuracy. This typo fix ensures that the code comments correctly reference the relevant Go package.
2024-11-08 23:05:31 +01:00
a1efa1a1ca
Remove redundant empty string check in SCRAM normalization
The existing check for an empty normalized string is unnecessary because the OpaqueString profile in precis already throws an error if an empty string is returned: https://cs.opensource.google/go/x/text/+/master:secure/precis/profiles.go;l=66
2024-11-08 22:44:10 +01:00
d6f256c29e
Fix typo in error message in normalizeString function
Corrected the spelling of "failed" in the error handling branch of the normalizeString function within smtp/auth_scram.go. This change addresses a minor typographical error to ensure the error message is clear and accurate.
2024-11-08 22:30:46 +01:00
2 changed files with 188 additions and 5 deletions

View file

@ -154,7 +154,7 @@ func (a *scramAuth) initialClientMessage() ([]byte, error) {
connState := a.tlsConnState connState := a.tlsConnState
bindData := connState.TLSUnique bindData := connState.TLSUnique
// crypto/tl: no tls-unique channel binding value for this tls connection, possibly due to missing // crypto/tls: no tls-unique channel binding value for this tls connection, possibly due to missing
// extended master key support and/or resumed connection // extended master key support and/or resumed connection
// RFC9266:122 tls-unique not defined for tls 1.3 and later // RFC9266:122 tls-unique not defined for tls 1.3 and later
if bindData == nil || connState.Version >= tls.VersionTLS13 { if bindData == nil || connState.Version >= tls.VersionTLS13 {
@ -308,10 +308,7 @@ func (a *scramAuth) normalizeUsername() (string, error) {
func (a *scramAuth) normalizeString(s string) (string, error) { func (a *scramAuth) normalizeString(s string) (string, error) {
s, err := precis.OpaqueString.String(s) s, err := precis.OpaqueString.String(s)
if err != nil { if err != nil {
return "", fmt.Errorf("failled to normalize string: %w", err) return "", fmt.Errorf("failed to normalize string: %w", err)
}
if s == "" {
return "", errors.New("normalized string is empty")
} }
return s, nil return s, nil
} }

View file

@ -18,6 +18,7 @@ import (
"bytes" "bytes"
"context" "context"
"crypto/hmac" "crypto/hmac"
"crypto/rand"
"crypto/sha1" "crypto/sha1"
"crypto/sha256" "crypto/sha256"
"crypto/tls" "crypto/tls"
@ -1217,6 +1218,182 @@ func TestScramAuth(t *testing.T) {
}) })
} }
func TestScramAuth_normalizeString(t *testing.T) {
t.Run("normalizeString with invalid input should fail", func(t *testing.T) {
auth := scramAuth{}
value := "\u0000example\uFFFEstring\u001F"
_, err := auth.normalizeString(value)
if err == nil {
t.Fatal("normalizeString should fail on disallowed runes")
}
if !strings.Contains(err.Error(), "precis: disallowed rune encountered") {
t.Errorf("expected error to be %q, got %q", "precis: disallowed rune encountered", err)
}
})
t.Run("normalizeString on empty string should fail", func(t *testing.T) {
auth := scramAuth{}
_, err := auth.normalizeString("")
if err == nil {
t.Error("normalizeString should fail on disallowed runes")
}
if !strings.Contains(err.Error(), "precis: transformation resulted in empty string") {
t.Errorf("expected error to be %q, got %q", "precis: transformation resulted in empty string", err)
}
})
t.Run("normalizeUsername with invalid input should fail", func(t *testing.T) {
auth := scramAuth{username: "\u0000example\uFFFEstring\u001F"}
_, err := auth.normalizeUsername()
if err == nil {
t.Error("normalizeUsername should fail on disallowed runes")
}
if !strings.Contains(err.Error(), "precis: disallowed rune encountered") {
t.Errorf("expected error to be %q, got %q", "precis: disallowed rune encountered", err)
}
})
t.Run("normalizeUsername with empty input should fail", func(t *testing.T) {
auth := scramAuth{username: ""}
_, err := auth.normalizeUsername()
if err == nil {
t.Error("normalizeUsername should fail on empty input")
}
if !strings.Contains(err.Error(), "precis: transformation resulted in empty string") {
t.Errorf("expected error to be %q, got %q", "precis: transformation resulted in empty string", err)
}
})
}
func TestScramAuth_initialClientMessage(t *testing.T) {
t.Run("initialClientMessage with invalid username should fail", func(t *testing.T) {
auth := scramAuth{username: "\u0000example\uFFFEstring\u001F"}
_, err := auth.initialClientMessage()
if err == nil {
t.Error("initialClientMessage should fail on disallowed runes")
}
if !strings.Contains(err.Error(), "precis: disallowed rune encountered") {
t.Errorf("expected error to be %q, got %q", "precis: disallowed rune encountered", err)
}
})
t.Run("initialClientMessage with empty username should fail", func(t *testing.T) {
auth := scramAuth{username: ""}
_, err := auth.initialClientMessage()
if err == nil {
t.Error("initialClientMessage should fail on empty username")
}
if !strings.Contains(err.Error(), "precis: transformation resulted in empty string") {
t.Errorf("expected error to be %q, got %q", "precis: transformation resulted in empty string", err)
}
})
t.Run("initialClientMessage fails on broken rand.Reader", func(t *testing.T) {
defaultRandReader := rand.Reader
t.Cleanup(func() { rand.Reader = defaultRandReader })
rand.Reader = &randReader{}
auth := scramAuth{username: "username"}
_, err := auth.initialClientMessage()
if err == nil {
t.Error("initialClientMessage should fail with broken rand.Reader")
}
if !strings.Contains(err.Error(), "unable to generate client secret: broken reader") {
t.Errorf("expected error to be %q, got %q", "unable to generate client secret: broken reader", err)
}
})
}
func TestScramAuth_handleServerFirstResponse(t *testing.T) {
t.Run("handleServerFirstResponse fails if not at least 3 parts", func(t *testing.T) {
auth := scramAuth{}
_, err := auth.handleServerFirstResponse([]byte("r=0"))
if err == nil {
t.Error("handleServerFirstResponse should fail on invalid response")
}
expectedErr := "not enough fields in the first server response"
if !strings.EqualFold(err.Error(), expectedErr) {
t.Errorf("expected error to be %q, got %q", expectedErr, err)
}
})
t.Run("handleServerFirstResponse fails with first part does not start with r=", func(t *testing.T) {
auth := scramAuth{}
_, err := auth.handleServerFirstResponse([]byte("x=0,y=0,z=0,r=0"))
if err == nil {
t.Error("handleServerFirstResponse should fail on invalid response")
}
expectedErr := "first part of the server response does not start with r="
if !strings.EqualFold(err.Error(), expectedErr) {
t.Errorf("expected error to be %q, got %q", expectedErr, err)
}
})
t.Run("handleServerFirstResponse fails with second part does not start with s=", func(t *testing.T) {
auth := scramAuth{}
_, err := auth.handleServerFirstResponse([]byte("r=0,x=0,y=0,z=0"))
if err == nil {
t.Error("handleServerFirstResponse should fail on invalid response")
}
expectedErr := "second part of the server response does not start with s="
if !strings.EqualFold(err.Error(), expectedErr) {
t.Errorf("expected error to be %q, got %q", expectedErr, err)
}
})
t.Run("handleServerFirstResponse fails with third part does not start with i=", func(t *testing.T) {
auth := scramAuth{}
_, err := auth.handleServerFirstResponse([]byte("r=0,s=0,y=0,z=0"))
if err == nil {
t.Error("handleServerFirstResponse should fail on invalid response")
}
expectedErr := "third part of the server response does not start with i="
if !strings.EqualFold(err.Error(), expectedErr) {
t.Errorf("expected error to be %q, got %q", expectedErr, err)
}
})
t.Run("handleServerFirstResponse fails with empty nonce", func(t *testing.T) {
auth := scramAuth{}
_, err := auth.handleServerFirstResponse([]byte("r=,s=0,i=0"))
if err == nil {
t.Error("handleServerFirstResponse should fail on invalid response")
}
expectedErr := "server nonce does not start with our nonce"
if !strings.EqualFold(err.Error(), expectedErr) {
t.Errorf("expected error to be %q, got %q", expectedErr, err)
}
})
t.Run("handleServerFirstResponse fails with non-base64 nonce", func(t *testing.T) {
auth := scramAuth{nonce: []byte("Test123")}
_, err := auth.handleServerFirstResponse([]byte("r=Test123,s=0,i=0"))
if err == nil {
t.Error("handleServerFirstResponse should fail on invalid response")
}
expectedErr := "illegal base64 data at input byte 0"
if !strings.Contains(err.Error(), expectedErr) {
t.Errorf("expected error to be %q, got %q", expectedErr, err)
}
})
t.Run("handleServerFirstResponse fails with non-number iterations", func(t *testing.T) {
auth := scramAuth{nonce: []byte("VGVzdDEyMw==")}
_, err := auth.handleServerFirstResponse([]byte("r=VGVzdDEyMw==,s=VGVzdDEyMw==,i=abc"))
if err == nil {
t.Error("handleServerFirstResponse should fail on invalid response")
}
expectedErr := `invalid iterations: strconv.Atoi: parsing "abc": invalid syntax`
if !strings.Contains(err.Error(), expectedErr) {
t.Errorf("expected error to be %q, got %q", expectedErr, err)
}
})
t.Run("handleServerFirstResponse fails with invalid password runes", func(t *testing.T) {
auth := scramAuth{
nonce: []byte("VGVzdDEyMw=="),
username: "username",
password: "\u0000example\uFFFEstring\u001F",
}
_, err := auth.handleServerFirstResponse([]byte("r=VGVzdDEyMw==,s=VGVzdDEyMw==,i=0"))
if err == nil {
t.Error("handleServerFirstResponse should fail on invalid response")
}
expectedErr := `unable to normalize password: failed to normalize string: precis: disallowed rune encountered`
if !strings.Contains(err.Error(), expectedErr) {
t.Errorf("expected error to be %q, got %q", expectedErr, err)
}
})
}
/* /*
@ -3700,3 +3877,12 @@ func (s *testSCRAMSMTP) extractNonce(message string) string {
} }
return "" return ""
} }
// randReader is type that satisfies the io.Reader interface. It can fail on a specific read
// operations and is therefore useful to test consecutive reads with errors
type randReader struct{}
// Read implements the io.Reader interface for the randReader type
func (r *randReader) Read([]byte) (int, error) {
return 0, errors.New("broken reader")
}