support authenticating with SHA-256 digest (#524)

This commit is contained in:
Alessandro Ros
2024-02-22 19:12:17 +01:00
committed by GitHub
parent c10f7aaedb
commit f040e20ac4
13 changed files with 241 additions and 120 deletions

View File

@@ -1690,17 +1690,15 @@ func TestClientPlayRedirect(t *testing.T) {
authNonce := "exampleNonce"
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,
Opaque: &authOpaque,
Stale: &authStale,
Algorithm: &authAlg,
Method: headers.AuthDigestMD5,
Realm: authRealm,
Nonce: authNonce,
Opaque: &authOpaque,
Stale: &authStale,
}.Marshal(),
},
StatusCode: base.StatusUnauthorized,

View File

@@ -27,11 +27,15 @@ func TestAuth(t *testing.T) {
[]headers.AuthMethod{headers.AuthBasic},
},
{
"digest",
[]headers.AuthMethod{headers.AuthDigest},
"digest md5",
[]headers.AuthMethod{headers.AuthDigestMD5},
},
{
"both",
"digest sha256",
[]headers.AuthMethod{headers.AuthDigestSHA256},
},
{
"all",
nil,
},
} {

17
pkg/auth/nonce.go Normal file
View File

@@ -0,0 +1,17 @@
package auth
import (
"crypto/rand"
"encoding/hex"
)
// GenerateNonce generates a nonce that can be used in Validate().
func GenerateNonce() (string, error) {
byts := make([]byte, 16)
_, err := rand.Read(byts)
if err != nil {
return "", err
}
return hex.EncodeToString(byts), nil
}

View File

@@ -2,19 +2,34 @@ package auth
import (
"fmt"
"strings"
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
)
func findHeader(v base.HeaderValue, prefix string) string {
for _, vi := range v {
if strings.HasPrefix(vi, prefix) {
return vi
func findAuthenticateHeader(auths []headers.Authenticate, method headers.AuthMethod) *headers.Authenticate {
for _, auth := range auths {
if auth.Method == method {
return &auth
}
}
return ""
return nil
}
func pickAuthenticateHeader(auths []headers.Authenticate) (*headers.Authenticate, error) {
if auth := findAuthenticateHeader(auths, headers.AuthDigestSHA256); auth != nil {
return auth, nil
}
if auth := findAuthenticateHeader(auths, headers.AuthDigestMD5); auth != nil {
return auth, nil
}
if auth := findAuthenticateHeader(auths, headers.AuthBasic); auth != nil {
return auth, nil
}
return nil, fmt.Errorf("no authentication methods available")
}
// Sender allows to send credentials.
@@ -27,37 +42,29 @@ type Sender struct {
// NewSender allocates a Sender.
// It requires a WWW-Authenticate header (provided by the server)
// and a set of credentials.
func NewSender(v base.HeaderValue, user string, pass string) (*Sender, error) {
// prefer digest
if v0 := findHeader(v, "Digest"); v0 != "" {
func NewSender(vals base.HeaderValue, user string, pass string) (*Sender, error) {
var auths []headers.Authenticate //nolint:prealloc
for _, v := range vals {
var auth headers.Authenticate
err := auth.Unmarshal(base.HeaderValue{v0})
err := auth.Unmarshal(base.HeaderValue{v})
if err != nil {
return nil, err
continue // ignore unrecognized headers
}
return &Sender{
user: user,
pass: pass,
authenticateHeader: &auth,
}, nil
auths = append(auths, auth)
}
if v0 := findHeader(v, "Basic"); v0 != "" {
var auth headers.Authenticate
err := auth.Unmarshal(base.HeaderValue{v0})
if err != nil {
return nil, err
}
return &Sender{
user: user,
pass: pass,
authenticateHeader: &auth,
}, nil
auth, err := pickAuthenticateHeader(auths)
if err != nil {
return nil, err
}
return nil, fmt.Errorf("no authentication methods available")
return &Sender{
user: user,
pass: pass,
authenticateHeader: auth,
}, nil
}
// AddAuthorization adds the Authorization header to a Request.
@@ -68,16 +75,26 @@ func (se *Sender) AddAuthorization(req *base.Request) {
Method: se.authenticateHeader.Method,
}
if se.authenticateHeader.Method == headers.AuthBasic {
switch se.authenticateHeader.Method {
case headers.AuthBasic:
h.BasicUser = se.user
h.BasicPass = se.pass
} else { // digest
case headers.AuthDigestMD5:
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))
default: // digest SHA-256
h.Username = se.user
h.Realm = se.authenticateHeader.Realm
h.Nonce = se.authenticateHeader.Nonce
h.URI = urStr
h.Response = sha256Hex(sha256Hex(se.user+":"+se.authenticateHeader.Realm+":"+se.pass) + ":" +
se.authenticateHeader.Nonce + ":" + sha256Hex(string(req.Method)+":"+urStr))
}
if req.Header == nil {

View File

@@ -2,7 +2,7 @@ package auth
import (
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
@@ -16,41 +16,10 @@ func md5Hex(in string) string {
return hex.EncodeToString(h.Sum(nil))
}
// GenerateNonce generates a nonce that can be used in Validate().
func GenerateNonce() (string, error) {
byts := make([]byte, 16)
_, err := rand.Read(byts)
if err != nil {
return "", err
}
return hex.EncodeToString(byts), nil
}
// GenerateWWWAuthenticate generates a WWW-Authenticate header.
func GenerateWWWAuthenticate(methods []headers.AuthMethod, realm string, nonce string) base.HeaderValue {
if methods == nil {
methods = []headers.AuthMethod{headers.AuthBasic, headers.AuthDigest}
}
var ret base.HeaderValue
for _, m := range methods {
switch m {
case headers.AuthBasic:
ret = append(ret, (&headers.Authenticate{
Method: headers.AuthBasic,
Realm: realm,
}).Marshal()...)
case headers.AuthDigest:
ret = append(ret, headers.Authenticate{
Method: headers.AuthDigest,
Realm: realm,
Nonce: nonce,
}.Marshal()...)
}
}
return ret
func sha256Hex(in string) string {
h := sha256.New()
h.Write([]byte(in))
return hex.EncodeToString(h.Sum(nil))
}
func contains(list []headers.AuthMethod, item headers.AuthMethod) bool {
@@ -73,7 +42,7 @@ func Validate(
nonce string,
) error {
if methods == nil {
methods = []headers.AuthMethod{headers.AuthBasic, headers.AuthDigest}
methods = []headers.AuthMethod{headers.AuthDigestSHA256, headers.AuthDigestMD5, headers.AuthBasic}
}
var auth headers.Authorization
@@ -83,15 +52,8 @@ func Validate(
}
switch {
case auth.Method == headers.AuthBasic && contains(methods, headers.AuthBasic):
if auth.BasicUser != user {
return fmt.Errorf("authentication failed")
}
if auth.BasicPass != pass {
return fmt.Errorf("authentication failed")
}
case auth.Method == headers.AuthDigest && contains(methods, headers.AuthDigest):
case (auth.Method == headers.AuthDigestSHA256 && contains(methods, headers.AuthDigestSHA256)) ||
(auth.Method == headers.AuthDigestMD5 && contains(methods, headers.AuthDigestMD5)):
if auth.Nonce != nonce {
return fmt.Errorf("wrong nonce")
}
@@ -119,12 +81,29 @@ func Validate(
}
}
response := md5Hex(md5Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + md5Hex(string(req.Method)+":"+ur.String()))
var response string
if auth.Method == headers.AuthDigestSHA256 {
response = sha256Hex(sha256Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + sha256Hex(string(req.Method)+":"+ur.String()))
} else {
response = md5Hex(md5Hex(user+":"+realm+":"+pass) +
":" + nonce + ":" + md5Hex(string(req.Method)+":"+ur.String()))
}
if auth.Response != response {
return fmt.Errorf("authentication failed")
}
case auth.Method == headers.AuthBasic && contains(methods, headers.AuthBasic):
if auth.BasicUser != user {
return fmt.Errorf("authentication failed")
}
if auth.BasicPass != pass {
return fmt.Errorf("authentication failed")
}
default:
return fmt.Errorf("no supported authentication methods found")
}

View File

@@ -49,7 +49,7 @@ func TestValidateAdditionalErrors(t *testing.T) {
"myuser",
"mypass",
nil,
[]headers.AuthMethod{headers.AuthDigest},
[]headers.AuthMethod{headers.AuthDigestMD5},
"IPCAM",
"abcde",
)

View File

@@ -0,0 +1,23 @@
package auth
import (
"github.com/bluenviron/gortsplib/v4/pkg/base"
"github.com/bluenviron/gortsplib/v4/pkg/headers"
)
// GenerateWWWAuthenticate generates a WWW-Authenticate header.
func GenerateWWWAuthenticate(methods []headers.AuthMethod, realm string, nonce string) base.HeaderValue {
if methods == nil {
methods = []headers.AuthMethod{headers.AuthDigestSHA256, headers.AuthDigestMD5, headers.AuthBasic}
}
var ret base.HeaderValue
for _, m := range methods {
ret = append(ret, headers.Authenticate{
Method: m,
Realm: realm,
Nonce: nonce, // used only by digest
}.Marshal()...)
}
return ret
}

View File

@@ -15,10 +15,33 @@ const (
// AuthBasic is the Basic authentication method
AuthBasic AuthMethod = iota
// AuthDigest is the Digest authentication method
AuthDigest
// AuthDigestMD5 is the Digest authentication method with the MD5 hash
AuthDigestMD5
// AuthDigestSHA256 is the Digest authentication method with the SHA-256 hash
AuthDigestSHA256
)
const (
// AuthDigest is an alias for AuthDigestMD5
//
// Deprecated: replaced by AuthDigestMD5
AuthDigest = AuthDigestMD5
)
func algorithmToMethod(v *string) (AuthMethod, error) {
switch {
case v == nil, strings.ToLower(*v) == "md5":
return AuthDigestMD5, nil
case strings.ToLower(*v) == "sha-256":
return AuthDigestSHA256, nil
default:
return 0, fmt.Errorf("unrecognized algorithm: %v", *v)
}
}
// Authenticate is a WWW-Authenticate header.
type Authenticate struct {
// authentication method
@@ -39,9 +62,6 @@ type Authenticate struct {
// stale
Stale *string
// algorithm
Algorithm *string
}
// Unmarshal decodes a WWW-Authenticate header.
@@ -62,18 +82,20 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
}
method, v0 := v0[:i], v0[i+1:]
isDigest := false
switch method {
case "Basic":
h.Method = AuthBasic
case "Digest":
h.Method = AuthDigest
isDigest = true
default:
return fmt.Errorf("invalid method (%s)", method)
}
if h.Method == AuthBasic {
if !isDigest {
kvs, err := keyValParse(v0, ',')
if err != nil {
return err
@@ -101,6 +123,7 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
realmReceived := false
nonceReceived := false
var algorithm *string
for k, rv := range kvs {
v := rv
@@ -121,13 +144,18 @@ func (h *Authenticate) Unmarshal(v base.HeaderValue) error {
h.Stale = &v
case "algorithm":
h.Algorithm = &v
algorithm = &v
}
}
if !realmReceived || !nonceReceived {
return fmt.Errorf("one or more digest fields are missing")
}
h.Method, err = algorithmToMethod(algorithm)
if err != nil {
return err
}
}
return nil
@@ -150,8 +178,10 @@ func (h Authenticate) Marshal() base.HeaderValue {
ret += ", stale=\"" + *h.Stale + "\""
}
if h.Algorithm != nil {
ret += ", algorithm=\"" + *h.Algorithm + "\""
if h.Method == AuthDigestMD5 {
ret += ", algorithm=\"MD5\""
} else {
ret += ", algorithm=\"SHA-256\""
}
return base.HeaderValue{ret}

View File

@@ -30,9 +30,10 @@ var casesAuthenticate = []struct {
{
"digest 1",
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` +
`stale="FALSE", algorithm="MD5"`},
Authenticate{
Method: AuthDigest,
Method: AuthDigestMD5,
Realm: "4419b63f5e51",
Nonce: "8b84a3b789283a8bea8da7fa7d41f08b",
Stale: stringPtr("FALSE"),
@@ -41,9 +42,10 @@ var casesAuthenticate = []struct {
{
"digest 2",
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale=FALSE`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", stale="FALSE"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="8b84a3b789283a8bea8da7fa7d41f08b", ` +
`stale="FALSE", algorithm="MD5"`},
Authenticate{
Method: AuthDigest,
Method: AuthDigestMD5,
Realm: "4419b63f5e51",
Nonce: "8b84a3b789283a8bea8da7fa7d41f08b",
Stale: stringPtr("FALSE"),
@@ -52,9 +54,10 @@ var casesAuthenticate = []struct {
{
"digest 3",
base.HeaderValue{`Digest realm="4419b63f5e51",nonce="133767111917411116111311118211673010032", stale="FALSE"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", stale="FALSE"`},
base.HeaderValue{`Digest realm="4419b63f5e51", nonce="133767111917411116111311118211673010032", ` +
`stale="FALSE", algorithm="MD5"`},
Authenticate{
Method: AuthDigest,
Method: AuthDigestMD5,
Realm: "4419b63f5e51",
Nonce: "133767111917411116111311118211673010032",
Stale: stringPtr("FALSE"),
@@ -67,12 +70,24 @@ var casesAuthenticate = []struct {
base.HeaderValue{`Digest realm="Please log in with a valid username", ` +
`nonce="752a62306daf32b401a41004555c7663", opaque="", stale="FALSE", algorithm="MD5"`},
Authenticate{
Method: AuthDigest,
Realm: "Please log in with a valid username",
Nonce: "752a62306daf32b401a41004555c7663",
Opaque: stringPtr(""),
Stale: stringPtr("FALSE"),
Algorithm: stringPtr("MD5"),
Method: AuthDigestMD5,
Realm: "Please log in with a valid username",
Nonce: "752a62306daf32b401a41004555c7663",
Opaque: stringPtr(""),
Stale: stringPtr("FALSE"),
},
},
{
"digest sha256",
base.HeaderValue{`Digest realm="IP Camera(AB705)", ` +
`nonce="fcc86deace979a488b2bfb89f4d0812c", algorithm="SHA-256", stale="FALSE"`},
base.HeaderValue{`Digest realm="IP Camera(AB705)", ` +
`nonce="fcc86deace979a488b2bfb89f4d0812c", stale="FALSE", algorithm="SHA-256"`},
Authenticate{
Method: AuthDigestSHA256,
Realm: "IP Camera(AB705)",
Nonce: "fcc86deace979a488b2bfb89f4d0812c",
Stale: stringPtr("FALSE"),
},
},
}

View File

@@ -42,7 +42,7 @@ type Authorization struct {
// response
Response string
// response
// opaque
Opaque *string
}
@@ -64,18 +64,20 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
}
method, v0 := v0[:i], v0[i+1:]
isDigest := false
switch method {
case "Basic":
h.Method = AuthBasic
case "Digest":
h.Method = AuthDigest
isDigest = true
default:
return fmt.Errorf("invalid method (%s)", method)
}
if h.Method == AuthBasic {
if !isDigest {
tmp, err := base64.StdEncoding.DecodeString(v0)
if err != nil {
return fmt.Errorf("invalid value")
@@ -98,6 +100,7 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
nonceReceived := false
uriReceived := false
responseReceived := false
var algorithm *string
for k, rv := range kvs {
v := rv
@@ -125,12 +128,20 @@ func (h *Authorization) Unmarshal(v base.HeaderValue) error {
case "opaque":
h.Opaque = &v
case "algorithm":
algorithm = &v
}
}
if !realmReceived || !usernameReceived || !nonceReceived || !uriReceived || !responseReceived {
return fmt.Errorf("one or more digest fields are missing")
}
h.Method, err = algorithmToMethod(algorithm)
if err != nil {
return err
}
}
return nil
@@ -151,5 +162,11 @@ func (h Authorization) Marshal() base.HeaderValue {
ret += ", opaque=\"" + *h.Opaque + "\""
}
if h.Method == AuthDigestMD5 {
ret += ", algorithm=\"MD5\""
} else {
ret += ", algorithm=\"SHA-256\""
}
return base.HeaderValue{ret}
}

View File

@@ -30,10 +30,10 @@ var casesAuthorization = []struct {
`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"`},
`nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", ` +
`response="e966c932a9242554e42c8ee200cec7f6", opaque="5ccc069c403ebaf9f0171e9517f40e41", algorithm="MD5"`},
Authorization{
Method: AuthDigest,
Method: AuthDigestMD5,
Username: "Mufasa",
Realm: "testrealm@host.com",
Nonce: "dcd98b7102dd2f0e8b11d0f600bfb0c093",
@@ -49,9 +49,9 @@ var casesAuthorization = []struct {
`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
base.HeaderValue{`Digest username="", realm="IPCAM", ` +
`nonce="5d17cd12b9fa8a85ac5ceef0926ea5a6", uri="rtsp://localhost:8554/mystream", ` +
`response="c072ae90eb4a27f4cdcb90d62266b2a1"`},
`response="c072ae90eb4a27f4cdcb90d62266b2a1", algorithm="MD5"`},
Authorization{
Method: AuthDigest,
Method: AuthDigestMD5,
Username: "",
Realm: "IPCAM",
Nonce: "5d17cd12b9fa8a85ac5ceef0926ea5a6",
@@ -59,6 +59,23 @@ var casesAuthorization = []struct {
Response: "c072ae90eb4a27f4cdcb90d62266b2a1",
},
},
{
"digest sha256",
base.HeaderValue{`Digest username="admin", realm="IP Camera(AB705)", ` +
`nonce="1ad195c2b2ca5a03784e53f88e16f579", uri="rtsp://192.168.80.76/", ` +
`response="9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913", algorithm="SHA-256"`},
base.HeaderValue{`Digest username="admin", realm="IP Camera(AB705)", ` +
`nonce="1ad195c2b2ca5a03784e53f88e16f579", uri="rtsp://192.168.80.76/", ` +
`response="9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913", algorithm="SHA-256"`},
Authorization{
Method: AuthDigestSHA256,
Username: "admin",
Realm: "IP Camera(AB705)",
Nonce: "1ad195c2b2ca5a03784e53f88e16f579",
URI: "rtsp://192.168.80.76/",
Response: "9e2324f104f3ce507d17e44a78fc1293001fe84805bde65d2aaa9be97a5a8913",
},
},
}
func TestAuthorizationUnmarshal(t *testing.T) {

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Digest realm,nonce=\"\"algorithm")

View File

@@ -0,0 +1,2 @@
go test fuzz v1
string("Digest username,realm,nonce,uri,response,algorithm")