Skip to content
Snippets Groups Projects
Commit 11413679 authored by Will McCutchen's avatar Will McCutchen
Browse files

Stricter digest algorithm & realm handling, more tests

parent 6feee4e6
No related branches found
No related tags found
No related merge requests found
......@@ -11,7 +11,6 @@
package digest
import (
"crypto"
"crypto/md5"
"crypto/sha256"
"crypto/subtle"
......@@ -22,6 +21,25 @@ import (
"time"
)
// digestAlgorithm is an algorithm used to hash digest payloads
type digestAlgorithm int
// Digest algorithms supported by this package
const (
MD5 digestAlgorithm = iota
SHA256
)
func (a digestAlgorithm) String() string {
switch a {
case MD5:
return "MD5"
case SHA256:
return "SHA-256"
}
return "UNKNOWN"
}
// Check returns a bool indicating whether the request is correctly
// authenticated for the given username and password.
func Check(req *http.Request, username, password string) bool {
......@@ -34,23 +52,32 @@ func Check(req *http.Request, username, password string) bool {
}
// Challenge returns a WWW-Authenticate header value for the given realm and
// algorithm.
func Challenge(realm, algorithm string) string {
// algorithm. If an invalid realm or an unsupported algorithm is given
func Challenge(realm string, algorithm digestAlgorithm) string {
entropy := make([]byte, 32)
rand.Read(entropy)
opaqueVal := entropy[:16]
nonceVal := fmt.Sprintf("%s:%x", time.Now(), entropy[16:31])
opaque := hash(opaqueVal, crypto.MD5)
nonce := hash([]byte(nonceVal), crypto.MD5)
// we use MD5 to hash nonces regardless of hash used for authentication
opaque := hash(opaqueVal, MD5)
nonce := hash([]byte(nonceVal), MD5)
return fmt.Sprintf("Digest qop=auth, realm=%#v, algorithm=%s, nonce=%s, opaque=%s", sanitizeRealm(realm), algorithm, nonce, opaque)
}
return fmt.Sprintf("Digest qop=auth, realm=%s, algorithm=%s, nonce=%s, opaque=%s", realm, algorithm, nonce, opaque)
// sanitizeRealm tries to ensure that a given realm does not include any
// characters that will trip up our extremely simplistic header parser.
func sanitizeRealm(realm string) string {
realm = strings.Replace(realm, `"`, "", -1)
realm = strings.Replace(realm, ",", "", -1)
return realm
}
// authorization is the result of parsing an Authorization header
type authorization struct {
algorithm crypto.Hash
algorithm digestAlgorithm
cnonce string
nc string
nonce string
......@@ -92,9 +119,9 @@ func parseAuthorizationHeader(value string) *authorization {
authInfo := parts[1]
auth := parseDictHeader(authInfo)
algo := crypto.MD5
algo := MD5
if strings.ToLower(auth["algorithm"]) == "sha-256" {
algo = crypto.SHA256
algo = SHA256
}
return &authorization{
......@@ -136,9 +163,9 @@ func parseDictHeader(value string) map[string]string {
// hash generates the hex digest of the given data using the given hashing
// algorithm, which must be one of MD5 or SHA256.
func hash(data []byte, algorithm crypto.Hash) string {
func hash(data []byte, algorithm digestAlgorithm) string {
switch algorithm {
case crypto.SHA256:
case SHA256:
return fmt.Sprintf("%x", sha256.Sum256(data))
default:
return fmt.Sprintf("%x", md5.Sum(data))
......@@ -150,7 +177,7 @@ func hash(data []byte, algorithm crypto.Hash) string {
// HA1 = H(A1) = H(username:realm:password)
//
// and H is one of MD5 or SHA256.
func makeHA1(realm, username, password string, algorithm crypto.Hash) string {
func makeHA1(realm, username, password string, algorithm digestAlgorithm) string {
A1 := fmt.Sprintf("%s:%s:%s", username, realm, password)
return hash([]byte(A1), algorithm)
}
......
package digest
import (
"crypto"
"fmt"
"net/http"
"reflect"
......@@ -75,6 +74,27 @@ func TestCheck(t *testing.T) {
})
}
func TestChallenge(t *testing.T) {
var tests = []struct {
realm string
expectedRealm string
algorithm digestAlgorithm
expectedAlgorithm string
}{
{"realm", "realm", MD5, "MD5"},
{"realm", "realm", SHA256, "SHA-256"},
{"realm with spaces", "realm with spaces", SHA256, "SHA-256"},
{`realm "with" "quotes"`, "realm with quotes", MD5, "MD5"},
{`spaces, "quotes," and commas`, "spaces quotes and commas", MD5, "MD5"},
}
for _, test := range tests {
challenge := Challenge(test.realm, test.algorithm)
result := parseDictHeader(challenge)
assertStringEquals(t, test.expectedRealm, result["realm"])
assertStringEquals(t, test.expectedAlgorithm, result["algorithm"])
}
}
func TestResponse(t *testing.T) {
auth := parseAuthorizationHeader(exampleAuthorization)
expected := auth.response
......@@ -84,16 +104,15 @@ func TestResponse(t *testing.T) {
func TestHash(t *testing.T) {
var tests = []struct {
algorithm crypto.Hash
algorithm digestAlgorithm
data []byte
expected string
}{
{crypto.SHA256, []byte("hello, world!\n"), "4dca0fd5f424a31b03ab807cbae77eb32bf2d089eed1cee154b3afed458de0dc"},
{crypto.MD5, []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"},
{SHA256, []byte("hello, world!\n"), "4dca0fd5f424a31b03ab807cbae77eb32bf2d089eed1cee154b3afed458de0dc"},
{MD5, []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"},
// Any unhandled hash results in MD5 being used
{crypto.MD4, []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"},
{crypto.SHA512, []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"},
{digestAlgorithm(10), []byte("hello, world!\n"), "910c8bc73110b0cd1bc5d2bcae782511"},
}
for _, test := range tests {
t.Run(fmt.Sprintf("hash/%v", test.algorithm), func(t *testing.T) {
......@@ -177,7 +196,7 @@ func TestParseAuthorizationHeader(t *testing.T) {
// incomplete headers are fine
{"Digest username=u, realm=r, nonce=n", &authorization{
algorithm: crypto.MD5,
algorithm: MD5,
username: "u",
realm: "r",
nonce: "n",
......@@ -185,51 +204,51 @@ func TestParseAuthorizationHeader(t *testing.T) {
// algorithm can be either MD5 or SHA-256, with MD5 as default
{"Digest username=u", &authorization{
algorithm: crypto.MD5,
algorithm: MD5,
username: "u",
}},
{"Digest algorithm=MD5, username=u", &authorization{
algorithm: crypto.MD5,
algorithm: MD5,
username: "u",
}},
{"Digest algorithm=md5, username=u", &authorization{
algorithm: crypto.MD5,
algorithm: MD5,
username: "u",
}},
{"Digest algorithm=SHA-256, username=u", &authorization{
algorithm: crypto.SHA256,
algorithm: SHA256,
username: "u",
}},
{"Digest algorithm=foo, username=u", &authorization{
algorithm: crypto.MD5,
algorithm: MD5,
username: "u",
}},
{"Digest algorithm=SHA-512, username=u", &authorization{
algorithm: crypto.MD5,
algorithm: MD5,
username: "u",
}},
// algorithm not case sensitive
{"Digest algorithm=sha-256, username=u", &authorization{
algorithm: crypto.SHA256,
algorithm: SHA256,
username: "u",
}},
// but dash is required in SHA-256 is not recognized
{"Digest algorithm=SHA256, username=u", &authorization{
algorithm: crypto.MD5,
algorithm: MD5,
username: "u",
}},
// session variants not recognized
{"Digest algorithm=SHA-256-sess, username=u", &authorization{
algorithm: crypto.MD5,
algorithm: MD5,
username: "u",
}},
{"Digest algorithm=MD5-sess, username=u", &authorization{
algorithm: crypto.MD5,
algorithm: MD5,
username: "u",
}},
{exampleAuthorization, &authorization{
algorithm: crypto.MD5,
algorithm: MD5,
cnonce: "0a4f113b",
nc: "00000001",
nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093",
......
......@@ -861,20 +861,25 @@ func (h *HTTPBin) DigestAuth(w http.ResponseWriter, r *http.Request) {
user := parts[3]
password := parts[4]
algorithm := "MD5"
algoName := "MD5"
if count == 6 {
algorithm = strings.ToUpper(parts[5])
algoName = strings.ToUpper(parts[5])
}
if qop != "auth" {
http.Error(w, "Invalid QOP directive", http.StatusBadRequest)
return
}
if algorithm != "MD5" && algorithm != "SHA-256" {
if algoName != "MD5" && algoName != "SHA-256" {
http.Error(w, "Invalid algorithm", http.StatusBadRequest)
return
}
algorithm := digest.MD5
if algoName == "SHA-256" {
algorithm = digest.SHA256
}
if !digest.Check(r, user, password) {
w.Header().Set("WWW-Authenticate", digest.Challenge("go-httpbin", algorithm))
w.WriteHeader(http.StatusUnauthorized)
......
......@@ -1123,9 +1123,16 @@ func TestDigestAuth(t *testing.T) {
{"/digest-auth", http.StatusNotFound},
{"/digest-auth/user", http.StatusNotFound},
{"/digest-auth/user/pass", http.StatusNotFound},
{"/digest-auth/auth/user/pass/MD5/foo", http.StatusNotFound},
// valid but unauthenticated requests
{"/digest-auth/auth/user/pass", http.StatusUnauthorized},
{"/digest-auth/auth/user/pass/MD5", http.StatusUnauthorized},
{"/digest-auth/auth/user/pass/SHA-256", http.StatusUnauthorized},
// invalid requests
{"/digest-auth/bad-qop/user/pass/MD5", http.StatusBadRequest},
{"/digest-auth/auth/user/pass/SHA-512", http.StatusBadRequest},
}
for _, test := range tests {
t.Run("ok"+test.url, func(t *testing.T) {
......@@ -1135,6 +1142,40 @@ func TestDigestAuth(t *testing.T) {
assertStatusCode(t, w, test.status)
})
}
t.Run("ok", func(t *testing.T) {
// Example captured from a successful login in a browser
authorization := `Digest username="user",
realm="go-httpbin",
nonce="6fb213c6593975c877bb1247370527ad",
uri="/digest-auth/auth/user/pass/MD5",
algorithm=MD5,
response="9b7a05d78051b4f668356eedf32f55d6",
opaque="fd1c386a015a2bb7c41585f54329ce91",
qop=auth,
nc=00000001,
cnonce="aaab705226af5bd4"`
url := "/digest-auth/auth/user/pass/MD5"
r, _ := http.NewRequest("GET", url, nil)
r.RequestURI = url
r.Header.Set("Authorization", authorization)
w := httptest.NewRecorder()
handler.ServeHTTP(w, r)
assertStatusCode(t, w, http.StatusOK)
resp := &authResponse{}
json.Unmarshal(w.Body.Bytes(), resp)
expectedResp := &authResponse{
Authorized: true,
User: "user",
}
if !reflect.DeepEqual(resp, expectedResp) {
t.Fatalf("expected response %#v, got %#v", expectedResp, resp)
}
})
}
func TestGzip(t *testing.T) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment