Repository: bluenviron/gortsplib
Pull request #523 (2024-02-22 11:57:07)

split headers.Authenticate and headers.Authorization

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/bluenviron/gortsplib/pull/523?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) Report
Attention: `3 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`c93d5c5`)](https://app.codecov.io/gh/bluenviron/gortsplib/commit/c93d5c54d9a786a0ebd34b2a284a90a940ff87a6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) 84.55% compared to head [(`087f840`)](https://app.codecov.io/gh/bluenviron/gortsplib/pull/523?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) 84.53%.

| [Files](https://app.codecov.io/gh/bluenviron/gortsplib/pull/523?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) | Patch % | Lines |
|---|---|---|
| [pkg/headers/authenticate.go](https://app.codecov.io/gh/bluenviron/gortsplib/pull/523?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron#diff-cGtnL2hlYWRlcnMvYXV0aGVudGljYXRlLmdv) | 93.75% | [2 Missing and 1 partial :warning: ](https://app.codecov.io/gh/bluenviron/gortsplib/pull/523?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #523      +/-   ##
==========================================
- Coverage   84.55%   84.53%   -0.03%     
==========================================
  Files         106      106              
  Lines       10996    10999       +3     
==========================================
  Hits         9298     9298              
- Misses       1297     1298       +1     
- Partials      401      403       +2     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/bluenviron/gortsplib/pull/523?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=bluenviron).


Patch:
diff --git a/pkg/auth/sender.go b/pkg/auth/sender.go
index e5d95004..7a662b64 100644
--- a/pkg/auth/sender.go
+++ b/pkg/auth/sender.go
@@ -19,11 +19,9 @@ func findHeader(v base.HeaderValue, prefix string) string {
 
 // Sender allows to send credentials.
 type Sender struct {
-	user   string
-	pass   string
-	method headers.AuthMethod
-	realm  string
-	nonce  string
+	user               string
+	pass               string
+	authenticateHeader *headers.Authenticate
 }
 
 // NewSender allocates a Sender.
@@ -38,20 +36,10 @@ func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) {
 			return nil, err
 		}
 
-		if auth.Realm == nil {
-			return nil, fmt.Errorf("realm is missing")
-		}
-
-		if auth.Nonce == nil {
-			return nil, fmt.Errorf("nonce is missing")
-		}
-
 		return &Sender{
-			user:   user,
-			pass:   pass,
-			method: headers.AuthDigest,
-			realm:  *auth.Realm,
-			nonce:  *auth.Nonce,
+			user:               user,
+			pass:               pass,
+			authenticateHeader: &auth,
 		}, nil
 	}
 
@@ -62,15 +50,10 @@ func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) {
 			return nil, err
 		}
 
-		if auth.Realm == nil {
-			return nil, fmt.Errorf("realm is missing")
-		}
-
 		return &Sender{
-			user:   user,
-			pass:   pass,
-			method: headers.AuthBasic,
-			realm:  *auth.Realm,
+			user:               user,
+			pass:               pass,
+			authenticateHeader: &auth,
 		}, nil
 	}
 
@@ -82,26 +65,19 @@ func (se *Sender) AddAuthorization(req *base.Request) {
 	urStr := req.URL.CloneWithoutCredentials().String()
 
 	h := headers.Authorization{
-		Method: se.method,
+		Method: se.authenticateHeader.Method,
 	}
 
-	switch se.method {
-	case headers.AuthBasic:
+	if se.authenticateHeader.Method == headers.AuthBasic {
 		h.BasicUser = se.user
 		h.BasicPass = se.pass
-
-	default: // headers.AuthDigest
-		response := md5Hex(md5Hex(se.user+":"+se.realm+":"+se.pass) + ":" +
-			se.nonce + ":" + md5Hex(string(req.Method)+":"+urStr))
-
-		h.DigestValues = headers.Authenticate{
-			Method:   headers.AuthDigest,
-			Username: &se.user,
-			Realm:    &se.realm,
-			Nonce:    &se.nonce,
-			URI:      &urStr,
-			Response: &response,
-		}
+	} else { // digest
+		h.Username = se.user
+		h.Realm = se.authenticateHeader.Realm
+		h.Nonce = se.authenticateHeader.Nonce
+		h.URI = urStr
+		h.Response = md5Hex(md5Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" +
+			se.authenticateHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr))
 	}
 
 	if req.Header == nil {
diff --git a/pkg/auth/validate.go b/pkg/auth/validate.go
index e43d52b9..3e20cd27 100644
--- a/pkg/auth/validate.go
+++ b/pkg/auth/validate.go
@@ -39,14 +39,14 @@ func GenerateWWWAuthenticate(methods []headers.AuthMethod, realm string, nonce s
 		case headers.AuthBasic:
 			ret = append(ret, (&headers.Authenticate{
 				Method: headers.AuthBasic,
-				Realm:  &realm,
+				Realm:  realm,
 			}).Marshal()...)
 
 		case headers.AuthDigest:
 			ret = append(ret, headers.Authenticate{
 				Method: headers.AuthDigest,
-				Realm:  &realm,
-				Nonce:  &nonce,
+				Realm:  realm,
+				Nonce:  nonce,
 			}.Marshal()...)
 		}
 	}
@@ -92,46 +92,26 @@ func Validate(
 			return fmt.Errorf("authentication failed")
 		}
 	case auth.Method == headers.AuthDigest && contains(methods, headers.AuthDigest):
-		if auth.DigestValues.Realm == nil {
-			return fmt.Errorf("realm is missing")
-		}
-
-		if auth.DigestValues.Nonce == nil {
-			return fmt.Errorf("nonce is missing")
-		}
-
-		if auth.DigestValues.Username == nil {
-			return fmt.Errorf("username is missing")
-		}
-
-		if auth.DigestValues.URI == nil {
-			return fmt.Errorf("uri is missing")
-		}
-
-		if auth.DigestValues.Response == nil {
-			return fmt.Errorf("response is missing")
-		}
-
-		if *auth.DigestValues.Nonce != nonce {
+		if auth.Nonce != nonce {
 			return fmt.Errorf("wrong nonce")
 		}
 
-		if *auth.DigestValues.Realm != realm {
+		if auth.Realm != realm {
 			return fmt.Errorf("wrong realm")
 		}
 
-		if *auth.DigestValues.Username != user {
+		if auth.Username != user {
 			return fmt.Errorf("authentication failed")
 		}
 
 		ur := req.URL
 
-		if *auth.DigestValues.URI != ur.String() {
+		if auth.URI != ur.String() {
 			// in SETUP requests, VLC strips the control attribute.
 			// try again with the base URL.
 			if baseURL != nil {
 				ur = baseURL
-				if *auth.DigestValues.URI != ur.String() {
+				if auth.URI != ur.String() {
 					return fmt.Errorf("wrong URL")
 				}
 			} else {
@@ -142,7 +122,7 @@ func Validate(
 		response := md5Hex(md5Hex(user+":"+realm+":"+pass) +
 			":" + nonce + ":" + md5Hex(string(req.Method)+":"+ur.String()))
 
-		if *auth.DigestValues.Response != response {
+		if auth.Response != response {
 			return fmt.Errorf("authentication failed")
 		}
 	default:
diff --git a/pkg/headers/authenticate.go b/pkg/headers/authenticate.go
index 5049b124..d4d37763 100644
--- a/pkg/headers/authenticate.go
+++ b/pkg/headers/authenticate.go
@@ -19,37 +19,32 @@ const (
 	AuthDigest
 )
 
-// Authenticate is an Authenticate or a WWW-Authenticate header.
+// Authenticate is a WWW-Authenticate header.
 type Authenticate struct {
 	// authentication method
 	Method AuthMethod
 
-	// (optional) username
-	Username *string
+	// realm
+	Realm string
 
-	// (optional) realm
-	Realm *string
+	//
+	// Digest authentication fields
+	//
 
-	// (optional) nonce
-	Nonce *string
+	// nonce
+	Nonce string
 
-	// (optional) uri
-	URI *string
-
-	// (optional) response
-	Response *string
-
-	// (optional) opaque
+	// opaque
 	Opaque *string
 
-	// (optional) stale
+	// stale
 	Stale *string
 
-	// (optional) algorithm
+	// algorithm
 	Algorithm *string
 }
 
-// Unmarshal decodes an Authenticate or a WWW-Authenticate header.
+// Unmarshal decodes a WWW-Authenticate header.
 func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
 	if len(v) == 0 {
 		return fmt.Errorf("value not provided")
@@ -78,93 +73,86 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
 		return fmt.Errorf("invalid method (%s)", method)
 	}
 
-	kvs, err := keyValParse(v0, ',')
-	if err != nil {
-		return err
-	}
-
-	for k, rv := range kvs {
-		v := rv
-
-		switch k {
-		case "username":
-			h.Username = &v
-
-		case "realm":
-			h.Realm = &v
-
-		case "nonce":
-			h.Nonce = &v
-
-		case "uri":
-			h.URI = &v
+	if h.Method == AuthBasic {
+		kvs, err := keyValParse(v0, ',')
+		if err != nil {
+			return err
+		}
 
-		case "response":
-			h.Response = &v
+		realmReceived := false
 
-		case "opaque":
-			h.Opaque = &v
+		for k, rv := range kvs {
+			v := rv
 
-		case "stale":
-			h.Stale = &v
+			if k == "realm" {
+				h.Realm = v
+				realmReceived = true
+			}
+		}
 
-		case "algorithm":
-			h.Algorithm = &v
+		if !realmReceived {
+			return fmt.Errorf("realm is missing")
+		}
+	} else { // digest
+		kvs, err := keyValParse(v0, ',')
+		if err != nil {
+			return err
 		}
-	}
 
-	return nil
-}
+		realmReceived := false
+		nonceReceived := false
 
-// Marshal encodes an Authenticate or a WWW-Authenticate header.
-func (h Authenticate) Marshal() base.HeaderValue {
-	ret := ""
+		for k, rv := range kvs {
+			v := rv
 
-	switch h.Method {
-	case AuthBasic:
-		ret += "Basic"
+			switch k {
+			case "realm":
+				h.Realm = v
+				realmReceived = true
 
-	case AuthDigest:
-		ret += "Digest"
-	}
+			case "nonce":
+				h.Nonce = v
+				nonceReceived = true
 
-	ret += " "
+			case "opaque":
+				h.Opaque = &v
 
-	var rets []string
+			case "stale":
+				h.Stale = &v
 
-	if h.Username != nil {
-		rets = append(rets, "username=\""+*h.Username+"\"")
-	}
+			case "algorithm":
+				h.Algorithm = &v
+			}
+		}
 
-	if h.Realm != nil {
-		rets = append(rets, "realm=\""+*h.Realm+"\"")
+		if !realmReceived || !nonceReceived {
+			return fmt.Errorf("one or more digest fields are missing")
+		}
 	}
 
-	if h.Nonce != nil {
-		rets = append(rets, "nonce=\""+*h.Nonce+"\"")
-	}
+	return nil
+}
 
-	if h.URI != nil {
-		rets = append(rets, "uri=\""+*h.URI+"\"")
+// Marshal encodes a WWW-Authenticate header.
+func (h Authenticate) Marshal() base.HeaderValue {
+	if h.Method == AuthBasic {
+		return base.HeaderValue{"Basic " +
+			"realm=\"" + h.Realm + "\""}
 	}
 
-	if h.Response != nil {
-		rets = append(rets, "response=\""+*h.Response+"\"")
-	}
+	ret := "Digest realm=\"" + h.Realm + "\", nonce=\"" + h.Nonce + "\""
 
 	if h.Opaque != nil {
-		rets = append(rets, "opaque=\""+*h.Opaque+"\"")
+		ret += ", opaque=\"" + *h.Opaque + "\""
 	}
 
 	if h.Stale != nil {
-		rets = append(rets, "stale=\""+*h.Stale+"\"")
+		ret += ", stale=\"" + *h.Stale + "\""
 	}
 
 	if h.Algorithm != nil {
-		rets = append(rets, "algorithm=\""+*h.Algorithm+"\"")
+		ret += ", algorithm=\"" + *h.Algorithm + "\""
 	}
 
-	ret += strings.Join(rets, ", ")
-
 	return base.HeaderValue{ret}
 }
diff --git a/pkg/headers/authorization.go b/pkg/headers/authorization.go
index 18e5e7e3..8fb18ccf 100644
--- a/pkg/headers/authorization.go
+++ b/pkg/headers/authorization.go
@@ -13,14 +13,37 @@ type Authorization struct {
 	// authentication method
 	Method AuthMethod
 
-	// basic user
+	//
+	// Basic authentication fields
+	//
+
+	// user
 	BasicUser string
 
-	// basic password
+	// password
 	BasicPass string
 
-	// digest values
-	DigestValues Authenticate
+	//
+	// Digest authentication fields
+	//
+
+	// username
+	Username string
+
+	// realm
+	Realm string
+
+	// nonce
+	Nonce string
+
+	// URI
+	URI string
+
+	// response
+	Response string
+
+	// response
+	Opaque *string
 }
 
 // Unmarshal decodes an Authorization header.
@@ -35,12 +58,24 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
 
 	v0 := v[0]
 
-	switch {
-	case strings.HasPrefix(v0, "Basic "):
+	i := strings.IndexByte(v0, ' ')
+	if i < 0 {
+		return fmt.Errorf("unable to split between method and keys (%v)", v0)
+	}
+	method, v0 := v0[:i], v0[i+1:]
+
+	switch method {
+	case "Basic":
 		h.Method = AuthBasic
 
-		v0 = v0[len("Basic "):]
+	case "Digest":
+		h.Method = AuthDigest
 
+	default:
+		return fmt.Errorf("invalid method (%s)", method)
+	}
+
+	if h.Method == AuthBasic {
 		tmp, err := base64.StdEncoding.DecodeString(v0)
 		if err != nil {
 			return fmt.Errorf("invalid value")
@@ -52,20 +87,50 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
 		}
 
 		h.BasicUser, h.BasicPass = tmp2[0], tmp2[1]
-
-	case strings.HasPrefix(v0, "Digest "):
-		h.Method = AuthDigest
-
-		var vals Authenticate
-		err := vals.Unmarshal(base.HeaderValue{v0})
+	} else { // digest
+		kvs, err := keyValParse(v0, ',')
 		if err != nil {
 			return err
 		}
 
-		h.DigestValues = vals
+		realmReceived := false
+		usernameReceived := false
+		nonceReceived := false
+		uriReceived := false
+		responseReceived := false
 
-	default:
-		return fmt.Errorf("invalid authorization header")
+		for k, rv := range kvs {
+			v := rv
+
+			switch k {
+			case "realm":
+				h.Realm = v
+				realmReceived = true
+
+			case "username":
+				h.Username = v
+				usernameReceived = true
+
+			case "nonce":
+				h.Nonce = v
+				nonceReceived = true
+
+			case "uri":
+				h.URI = v
+				uriReceived = true
+
+			case "response":
+				h.Response = v
+				responseReceived = true
+
+			case "opaque":
+				h.Opaque = &v
+			}
+		}
+
+		if !realmReceived || !usernameReceived || !nonceReceived || !uriReceived || !responseReceived {
+			return fmt.Errorf("one or more digest fields are missing")
+		}
 	}
 
 	return nil
@@ -73,13 +138,18 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
 
 // Marshal encodes an Authorization header.
 func (h Authorization) Marshal() base.HeaderValue {
-	switch h.Method {
-	case AuthBasic:
-		response := base64.StdEncoding.EncodeToString([]byte(h.BasicUser + ":" + h.BasicPass))
+	if h.Method == AuthBasic {
+		return base.HeaderValue{"Basic " +
+			base64.StdEncoding.EncodeToString([]byte(h.BasicUser+":"+h.BasicPass))}
+	}
 
-		return base.HeaderValue{"Basic " + response}
+	ret := "Digest " +
+		"username=\"" + h.Username + "\", realm=\"" + h.Realm + "\", " +
+		"nonce=\"" + h.Nonce + "\", uri=\"" + h.URI + "\", response=\"" + h.Response + "\""
 
-	default: // AuthDigest
-		return h.DigestValues.Marshal()
+	if h.Opaque != nil {
+		ret += ", opaque=\"" + *h.Opaque + "\""
 	}
+
+	return base.HeaderValue{ret}
 }


Test patch:
diff --git a/client_play_test.go b/client_play_test.go
index cbd5c20a..889d49c6 100644
--- a/client_play_test.go
+++ b/client_play_test.go
@@ -1691,12 +1691,13 @@ func TestClientPlayRedirect(t *testing.T) {
 							authOpaque := "exampleOpaque"
 							authStale := "FALSE"
 							authAlg := "MD5"
+
 							err = conn.WriteResponse(&base.Response{
 								Header: base.Header{
 									"WWW-Authenticate": headers.Authenticate{
 										Method:    headers.AuthDigest,
-										Realm:     &authRealm,
-										Nonce:     &authNonce,
+										Realm:     authRealm,
+										Nonce:     authNonce,
 										Opaque:    &authOpaque,
 										Stale:     &authStale,
 										Algorithm: &authAlg,
@@ -1706,13 +1707,16 @@ func TestClientPlayRedirect(t *testing.T) {
 							})
 							require.NoError(t, err)
 						}
+
 						req, err = conn.ReadRequest()
 						require.NoError(t, err)
+
 						authHeaderVal, exists := req.Header["Authorization"]
 						require.True(t, exists)
-						var authHeader headers.Authenticate
+
+						var authHeader headers.Authorization
 						require.NoError(t, authHeader.Unmarshal(authHeaderVal))
-						require.Equal(t, *authHeader.Username, "testusr")
+						require.Equal(t, authHeader.Username, "testusr")
 						require.Equal(t, base.Describe, req.Method)
 					}
 
diff --git a/pkg/auth/sender_test.go b/pkg/auth/sender_test.go
index b0506877..c4eca4fb 100644
--- a/pkg/auth/sender_test.go
+++ b/pkg/auth/sender_test.go
@@ -3,51 +3,18 @@ package auth
 import (
 	"testing"
 
-	"github.com/stretchr/testify/require"
-
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
 )
 
-func TestSenderErrors(t *testing.T) {
-	for _, ca := range []struct {
-		name string
-		hv   base.HeaderValue
-		err  string
-	}{
-		{
-			"invalid method",
-			base.HeaderValue{`Invalid`},
-			"no authentication methods available",
-		},
-		{
-			"digest invalid",
-			base.HeaderValue{`Digest`},
-			"unable to split between method and keys (Digest)",
-		},
-		{
-			"digest, missing realm",
-			base.HeaderValue{`Digest nonce=123`},
-			"realm is missing",
-		},
-		{
-			"digest, missing nonce",
-			base.HeaderValue{`Digest realm=123`},
-			"nonce is missing",
-		},
-		{
-			"basic invalid",
-			base.HeaderValue{`Basic`},
-			"unable to split between method and keys (Basic)",
-		},
-		{
-			"basic, missing realm",
-			base.HeaderValue{`Basic nonce=123`},
-			"realm is missing",
-		},
-	} {
-		t.Run(ca.name, func(t *testing.T) {
-			_, err := NewSender(ca.hv, "myuser", "mypass")
-			require.EqualError(t, err, ca.err)
-		})
-	}
+func FuzzSender(f *testing.F) {
+	f.Add(`Invalid`)
+	f.Add(`Digest`)
+	f.Add(`Digest nonce=123`)
+	f.Add(`Digest realm=123`)
+	f.Add(`Basic`)
+	f.Add(`Basic nonce=123`)
+
+	f.Fuzz(func(t *testing.T, a string) {
+		NewSender(base.HeaderValue{a}, "myuser", "mypass") //nolint:errcheck
+	})
 }
diff --git a/pkg/auth/validate_test.go b/pkg/auth/validate_test.go
index 2abb255e..956fa5fb 100644
--- a/pkg/auth/validate_test.go
+++ b/pkg/auth/validate_test.go
@@ -3,75 +3,55 @@ package auth
 import (
 	"testing"
 
-	"github.com/stretchr/testify/require"
-
 	"github.com/bluenviron/gortsplib/v4/pkg/base"
+	"github.com/bluenviron/gortsplib/v4/pkg/headers"
+	"github.com/stretchr/testify/require"
 )
 
-func TestValidateErrors(t *testing.T) {
-	for _, ca := range []struct {
-		name string
-		hv   base.HeaderValue
-		err  string
-	}{
-		{
-			"invalid auth",
-			base.HeaderValue{`Invalid`},
-			"invalid authorization header",
-		},
-		{
-			"digest missing realm",
-			base.HeaderValue{`Digest `},
-			"realm is missing",
-		},
-		{
-			"digest missing nonce",
-			base.HeaderValue{`Digest realm=123`},
-			"nonce is missing",
-		},
-		{
-			"digest missing username",
-			base.HeaderValue{`Digest realm=123,nonce=123`},
-			"username is missing",
-		},
-		{
-			"digest missing uri",
-			base.HeaderValue{`Digest realm=123,nonce=123,username=123`},
-			"uri is missing",
-		},
-		{
-			"digest missing response",
-			base.HeaderValue{`Digest realm=123,nonce=123,username=123,uri=123`},
-			"response is missing",
-		},
-		{
-			"digest wrong nonce",
-			base.HeaderValue{`Digest realm=123,nonce=123,username=123,uri=123,response=123`},
-			"wrong nonce",
-		},
-		{
-			"digest wrong realm",
-			base.HeaderValue{`Digest realm=123,nonce=abcde,username=123,uri=123,response=123`},
-			"wrong realm",
-		},
-	} {
-		t.Run(ca.name, func(t *testing.T) {
-			err := Validate(
-				&base.Request{
-					Method: base.Describe,
-					URL:    nil,
-					Header: base.Header{
-						"Authorization": ca.hv,
-					},
+func FuzzValidate(f *testing.F) {
+	f.Add(`Invalid`)
+	f.Add(`Digest `)
+	f.Add(`Digest realm=123`)
+	f.Add(`Digest realm=123,nonce=123`)
+	f.Add(`Digest realm=123,nonce=123,username=123`)
+	f.Add(`Digest realm=123,nonce=123,username=123,uri=123`)
+	f.Add(`Digest realm=123,nonce=123,username=123,uri=123,response=123`)
+	f.Add(`Digest realm=123,nonce=abcde,username=123,uri=123,response=123`)
+
+	f.Fuzz(func(t *testing.T, a string) {
+		Validate( //nolint:errcheck
+			&base.Request{
+				Method: base.Describe,
+				URL:    nil,
+				Header: base.Header{
+					"Authorization": base.HeaderValue{a},
 				},
-				"myuser",
-				"mypass",
-				nil,
-				nil,
-				"IPCAM",
-				"abcde",
-			)
-			require.EqualError(t, err, ca.err)
-		})
-	}
+			},
+			"myuser",
+			"mypass",
+			nil,
+			nil,
+			"IPCAM",
+			"abcde",
+		)
+	})
+}
+
+func TestValidateAdditionalErrors(t *testing.T) {
+	err := Validate(
+		&base.Request{
+			Method: base.Describe,
+			URL:    nil,
+			Header: base.Header{
+				"Authorization": base.HeaderValue{"Basic bXl1c2VyOm15cGFzcw=="},
+			},
+		},
+		"myuser",
+		"mypass",
+		nil,
+		[]headers.AuthMethod{headers.AuthDigest},
+		"IPCAM",
+		"abcde",
+	)
+	require.Error(t, err)
 }
diff --git a/pkg/headers/authenticate_test.go b/pkg/headers/authenticate_test.go
index 07c8469d..6d82eab2 100644
--- a/pkg/headers/authenticate_test.go
+++ b/pkg/headers/authenticate_test.go
@@ -24,82 +24,52 @@ var casesAuthenticate = []struct {
 		base.HeaderValue{`Basic realm="4419b63f5e51"`},
 		Authenticate{
 			Method: AuthBasic,
-			Realm:  stringPtr("4419b63f5e51"),
+			Realm:  "4419b63f5e51",
 		},
 	},
 	{
-		"digest request 1",
+		"digest 1",
 		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
 		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
 		Authenticate{
 			Method: AuthDigest,
-			Realm:  stringPtr("4419b63f5e51"),
-			Nonce:  stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"),
+			Realm:  "4419b63f5e51",
+			Nonce:  "8b84a3b789283a8bea8da7fa7d41f08b",
 			Stale:  stringPtr("FALSE"),
 		},
 	},
 	{
-		"digest request 2",
+		"digest 2",
 		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale=FALSE`},
 		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
 		Authenticate{
 			Method: AuthDigest,
-			Realm:  stringPtr("4419b63f5e51"),
-			Nonce:  stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"),
+			Realm:  "4419b63f5e51",
+			Nonce:  "8b84a3b789283a8bea8da7fa7d41f08b",
 			Stale:  stringPtr("FALSE"),
 		},
 	},
 	{
-		"digest request 3",
+		"digest 3",
 		base.HeaderValue{`Digest realm="4419b63f5e51",nonce="133767111917411116111311118211673010032",  stale="FALSE"`},
 		base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", stale="FALSE"`},
 		Authenticate{
 			Method: AuthDigest,
-			Realm:  stringPtr("4419b63f5e51"),
-			Nonce:  stringPtr("133767111917411116111311118211673010032"),
+			Realm:  "4419b63f5e51",
+			Nonce:  "133767111917411116111311118211673010032",
 			Stale:  stringPtr("FALSE"),
 		},
 	},
 	{
-		"digest response generic",
-		base.HeaderValue{`Digest username="aa", realm="bb", nonce="cc", uri="dd", response="ee"`},
-		base.HeaderValue{`Digest username="aa", realm="bb", nonce="cc", uri="dd", response="ee"`},
-		Authenticate{
-			Method:   AuthDigest,
-			Username: stringPtr("aa"),
-			Realm:    stringPtr("bb"),
-			Nonce:    stringPtr("cc"),
-			URI:      stringPtr("dd"),
-			Response: stringPtr("ee"),
-		},
-	},
-	{
-		"digest response with empty field",
-		base.HeaderValue{`Digest username="", realm="IPCAM", ` +
-			`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
-			`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
-		base.HeaderValue{`Digest username="", realm="IPCAM", ` +
-			`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
-			`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
-		Authenticate{
-			Method:   AuthDigest,
-			Username: stringPtr(""),
-			Realm:    stringPtr("IPCAM"),
-			Nonce:    stringPtr("5d17cd12b9fa8a85ac5ceef0926ea5a6"),
-			URI:      stringPtr("rtsp://localhost:8554/mystream"),
-			Response: stringPtr("c072ae90eb4a27f4cdcb90d62266b2a1"),
-		},
-	},
-	{
-		"digest response with no spaces and additional fields",
+		"digest after failed auth",
 		base.HeaderValue{`Digest realm="Please log in with a valid username",` +
 			`nonce="752a62306daf32b401a41004555c7663",opaque="",stale=FALSE,algorithm=MD5`},
 		base.HeaderValue{`Digest realm="Please log in with a valid username", ` +
 			`nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE", algorithm="MD5"`},
 		Authenticate{
 			Method:    AuthDigest,
-			Realm:     stringPtr("Please log in with a valid username"),
-			Nonce:     stringPtr("752a62306daf32b401a41004555c7663"),
+			Realm:     "Please log in with a valid username",
+			Nonce:     "752a62306daf32b401a41004555c7663",
 			Opaque:    stringPtr(""),
 			Stale:     stringPtr("FALSE"),
 			Algorithm: stringPtr("MD5"),
@@ -118,46 +88,6 @@ func TestAuthenticateUnmarshal(t *testing.T) {
 	}
 }
 
-func TestAutenticatehUnmarshalErrors(t *testing.T) {
-	for _, ca := range []struct {
-		name string
-		hv   base.HeaderValue
-		err  string
-	}{
-		{
-			"empty",
-			base.HeaderValue{},
-			"value not provided",
-		},
-		{
-			"2 values",
-			base.HeaderValue{"a", "b"},
-			"value provided multiple times ([a b])",
-		},
-		{
-			"no keys",
-			base.HeaderValue{"Basic"},
-			"unable to split between method and keys (Basic)",
-		},
-		{
-			"invalid keys",
-			base.HeaderValue{`Basic key1="k`},
-			"apexes not closed (key1=\"k)",
-		},
-		{
-			"invalid method",
-			base.HeaderValue{"Testing key1=val1"},
-			"invalid method (Testing)",
-		},
-	} {
-		t.Run(ca.name, func(t *testing.T) {
-			var h Authenticate
-			err := h.Unmarshal(ca.hv)
-			require.EqualError(t, err, ca.err)
-		})
-	}
-}
-
 func TestAuthenticateMarshal(t *testing.T) {
 	for _, ca := range casesAuthenticate {
 		t.Run(ca.name, func(t *testing.T) {
@@ -166,3 +96,28 @@ func TestAuthenticateMarshal(t *testing.T) {
 		})
 	}
 }
+
+func FuzzAuthenticateUnmarshal(f *testing.F) {
+	for _, ca := range casesAuthenticate {
+		f.Add(ca.vin[0])
+	}
+
+	f.Fuzz(func(t *testing.T, b string) {
+		var h Authenticate
+		h.Unmarshal(base.HeaderValue{b}) //nolint:errcheck
+	})
+}
+
+func TestAuthenticateAdditionalErrors(t *testing.T) {
+	func() {
+		var h Authenticate
+		err := h.Unmarshal(base.HeaderValue{})
+		require.Error(t, err)
+	}()
+
+	func() {
+		var h Authenticate
+		err := h.Unmarshal(base.HeaderValue{"a", "b"})
+		require.Error(t, err)
+	}()
+}
diff --git a/pkg/headers/authorization_test.go b/pkg/headers/authorization_test.go
index 0f8a9cad..e86ea8e0 100644
--- a/pkg/headers/authorization_test.go
+++ b/pkg/headers/authorization_test.go
@@ -26,16 +26,37 @@ var casesAuthorization = []struct {
 	},
 	{
 		"digest",
-		base.HeaderValue{"Digest realm=\"4419b63f5e51\", nonce=\"8b84a3b789283a8bea8da7fa7d41f08b\", stale=\"FALSE\""},
-		base.HeaderValue{"Digest realm=\"4419b63f5e51\", nonce=\"8b84a3b789283a8bea8da7fa7d41f08b\", stale=\"FALSE\""},
+		base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` +
+			`nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", ` +
+			`uri="/dir/index.html", response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41"`},
+		base.HeaderValue{`Digest username="Mufasa", realm="testrealm@host.com", ` +
+			`nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", ` +
+			`uri="/dir/index.html", response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41"`},
 		Authorization{
-			Method: AuthDigest,
-			DigestValues: Authenticate{
-				Method: AuthDigest,
-				Realm:  stringPtr("4419b63f5e51"),
-				Nonce:  stringPtr("8b84a3b789283a8bea8da7fa7d41f08b"),
-				Stale:  stringPtr("FALSE"),
-			},
+			Method:   AuthDigest,
+			Username: "Mufasa",
+			Realm:    "testrealm@host.com",
+			Nonce:    "dcd98b7102dd2f0e8b11d0f600bfb0c093",
+			URI:      "/dir/index.html",
+			Response: "e966c932a9242554e42c8ee200cec7f6",
+			Opaque:   stringPtr("5ccc069c403ebaf9f0171e9517f40e41"),
+		},
+	},
+	{
+		"digest with empty field",
+		base.HeaderValue{`Digest username="", realm="IPCAM", ` +
+			`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
+			`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
+		base.HeaderValue{`Digest username="", realm="IPCAM", ` +
+			`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
+			`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
+		Authorization{
+			Method:   AuthDigest,
+			Username: "",
+			Realm:    "IPCAM",
+			Nonce:    "5d17cd12b9fa8a85ac5ceef0926ea5a6",
+			URI:      "rtsp://localhost:8554/mystream",
+			Response: "c072ae90eb4a27f4cdcb90d62266b2a1",
 		},
 	},
 }
@@ -51,51 +72,6 @@ func TestAuthorizationUnmarshal(t *testing.T) {
 	}
 }
 
-func TestAuthorizationUnmarshalErrors(t *testing.T) {
-	for _, ca := range []struct {
-		name string
-		hv   base.HeaderValue
-		err  string
-	}{
-		{
-			"empty",
-			base.HeaderValue{},
-			"value not provided",
-		},
-		{
-			"2 values",
-			base.HeaderValue{"a", "b"},
-			"value provided multiple times ([a b])",
-		},
-		{
-			"invalid",
-			base.HeaderValue{`Invalid`},
-			"invalid authorization header",
-		},
-		{
-			"basic invalid 1",
-			base.HeaderValue{`Basic aaa`},
-			"invalid value",
-		},
-		{
-			"basic invalid 2",
-			base.HeaderValue{`Basic aW52YWxpZA==`},
-			"invalid value",
-		},
-		{
-			"digest invalid",
-			base.HeaderValue{`Digest test="v`},
-			"apexes not closed (test=\"v)",
-		},
-	} {
-		t.Run(ca.name, func(t *testing.T) {
-			var h Authorization
-			err := h.Unmarshal(ca.hv)
-			require.EqualError(t, err, ca.err)
-		})
-	}
-}
-
 func TestAuthorizationMarshal(t *testing.T) {
 	for _, ca := range casesAuthorization {
 		t.Run(ca.name, func(t *testing.T) {
@@ -104,3 +80,28 @@ func TestAuthorizationMarshal(t *testing.T) {
 		})
 	}
 }
+
+func FuzzAuthorizationUnmarshal(f *testing.F) {
+	for _, ca := range casesAuthorization {
+		f.Add(ca.vin[0])
+	}
+
+	f.Fuzz(func(t *testing.T, b string) {
+		var h Authorization
+		h.Unmarshal(base.HeaderValue{b}) //nolint:errcheck
+	})
+}
+
+func TestAuthorizationAdditionalErrors(t *testing.T) {
+	func() {
+		var h Authorization
+		err := h.Unmarshal(base.HeaderValue{})
+		require.Error(t, err)
+	}()
+
+	func() {
+		var h Authorization
+		err := h.Unmarshal(base.HeaderValue{"a", "b"})
+		require.Error(t, err)
+	}()
+}
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/593a6affc7d1fcee b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/593a6affc7d1fcee
new file mode 100644
index 00000000..21449f99
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/593a6affc7d1fcee
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("Basic =\"")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/5fb42e6dcbfb41cb b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/5fb42e6dcbfb41cb
new file mode 100644
index 00000000..c098300d
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/5fb42e6dcbfb41cb
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("Digest ")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/771e938e4458e983 b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/771e938e4458e983
new file mode 100644
index 00000000..ee3f3399
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/771e938e4458e983
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("0")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/8643a79bab153d1b b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/8643a79bab153d1b
new file mode 100644
index 00000000..037b4727
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/8643a79bab153d1b
@@ -0,0 +1,2 @@
+go test fuzz v1
+string(" ")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/9a1c51bbe1216ed5 b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/9a1c51bbe1216ed5
new file mode 100644
index 00000000..c460e4b4
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/9a1c51bbe1216ed5
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("Digest =\"")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/c44cf6083afc355b b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/c44cf6083afc355b
new file mode 100644
index 00000000..228e628d
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthenticateUnmarshal/c44cf6083afc355b
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("Basic ")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/5fb42e6dcbfb41cb b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/5fb42e6dcbfb41cb
new file mode 100644
index 00000000..c098300d
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/5fb42e6dcbfb41cb
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("Digest ")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/771e938e4458e983 b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/771e938e4458e983
new file mode 100644
index 00000000..ee3f3399
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/771e938e4458e983
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("0")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/8643a79bab153d1b b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/8643a79bab153d1b
new file mode 100644
index 00000000..037b4727
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/8643a79bab153d1b
@@ -0,0 +1,2 @@
+go test fuzz v1
+string(" ")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/9a1c51bbe1216ed5 b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/9a1c51bbe1216ed5
new file mode 100644
index 00000000..c460e4b4
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/9a1c51bbe1216ed5
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("Digest =\"")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/c44cf6083afc355b b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/c44cf6083afc355b
new file mode 100644
index 00000000..228e628d
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/c44cf6083afc355b
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("Basic ")
diff --git a/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/e77c46eef8802889 b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/e77c46eef8802889
new file mode 100644
index 00000000..74941108
--- /dev/null
+++ b/pkg/headers/testdata/fuzz/FuzzAuthorizationUnmarshal/e77c46eef8802889
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("Basic 0")