auth_digest.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. /*
  2. * Copyright (c) 2015, Psiphon Inc.
  3. * All rights reserved.
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. *
  18. */
  19. package upstreamproxy
  20. import (
  21. "crypto/md5"
  22. "crypto/rand"
  23. "encoding/base64"
  24. "fmt"
  25. "net/http"
  26. "strings"
  27. )
  28. type DigestHttpAuthState int
  29. const (
  30. DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED DigestHttpAuthState = iota
  31. DIGEST_HTTP_AUTH_STATE_RESPONSE_GENERATED
  32. )
  33. type DigestHttpAuthenticator struct {
  34. state DigestHttpAuthState
  35. username string
  36. password string
  37. digestHeaders *DigestHeaders
  38. }
  39. func newDigestAuthenticator(username, password string) *DigestHttpAuthenticator {
  40. return &DigestHttpAuthenticator{
  41. state: DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED,
  42. username: username,
  43. password: password,
  44. }
  45. }
  46. /* Adapted from https://github.com/ryanjdew/http-digest-auth-client */
  47. type DigestHeaders struct {
  48. Realm string
  49. Qop string
  50. Method string
  51. Nonce string
  52. Opaque string
  53. Algorithm string
  54. HA1 string
  55. HA2 string
  56. Cnonce string
  57. Uri string
  58. Nc int16
  59. Username string
  60. Password string
  61. }
  62. // ApplyAuth adds proper auth header to the passed request
  63. func (d *DigestHeaders) ApplyAuth(req *http.Request) {
  64. d.Nc += 0x1
  65. d.Method = req.Method
  66. d.digestChecksum()
  67. response := h(strings.Join([]string{d.HA1, d.Nonce, fmt.Sprintf("%08x", d.Nc),
  68. d.Cnonce, d.Qop, d.HA2}, ":"))
  69. AuthHeader := fmt.Sprintf(`Digest username="%s", realm="%s", nonce="%s", uri="%s", response="%s", qop=%s, nc=%08x, cnonce="%s", algorithm=%s`,
  70. d.Username, d.Realm, d.Nonce, d.Uri, response, d.Qop, d.Nc, d.Cnonce, d.Algorithm)
  71. if d.Opaque != "" {
  72. AuthHeader = fmt.Sprintf(`%s, opaque="%s"`, AuthHeader, d.Opaque)
  73. }
  74. req.Header.Set("Proxy-Authorization", AuthHeader)
  75. }
  76. func (d *DigestHeaders) digestChecksum() {
  77. var A1 string
  78. switch d.Algorithm {
  79. case "MD5":
  80. // HA1=MD5(username:realm:password)
  81. A1 = fmt.Sprintf("%s:%s:%s", d.Username, d.Realm, d.Password)
  82. case "MD5-sess":
  83. // HA1=MD5(MD5(username:realm:password):nonce:cnonce)
  84. str := fmt.Sprintf("%s:%s:%s", d.Username, d.Realm, d.Password)
  85. A1 = fmt.Sprintf("%s:%s:%s", h(str), d.Nonce, d.Cnonce)
  86. default:
  87. // Token
  88. }
  89. if A1 == "" {
  90. return
  91. }
  92. // HA1
  93. d.HA1 = h(A1)
  94. // HA2
  95. A2 := fmt.Sprintf("%s:%s", d.Method, d.Uri)
  96. d.HA2 = h(A2)
  97. }
  98. func randomKey() string {
  99. k := make([]byte, 12)
  100. for bytes := 0; bytes < len(k); {
  101. n, err := rand.Read(k[bytes:])
  102. if err != nil {
  103. panic("rand.Read() failed")
  104. }
  105. k[bytes] = byte(bytes)
  106. bytes += n
  107. }
  108. return base64.StdEncoding.EncodeToString(k)
  109. }
  110. /*
  111. H function for MD5 algorithm (returns a lower-case hex MD5 digest)
  112. */
  113. func h(data string) string {
  114. digest := md5.New()
  115. digest.Write([]byte(data))
  116. return fmt.Sprintf("%x", digest.Sum(nil))
  117. }
  118. func (a *DigestHttpAuthenticator) Authenticate(req *http.Request, resp *http.Response) error {
  119. if a.state != DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED {
  120. return proxyError(fmt.Errorf("authorization is not accepted by the proxy server"))
  121. }
  122. challenges, err := parseAuthChallenge(resp)
  123. if err != nil {
  124. // Already wrapped in proxyError
  125. return err
  126. }
  127. challenge := challenges["Digest"]
  128. if len(challenge) == 0 {
  129. return proxyError(fmt.Errorf("digest authentication challenge is empty"))
  130. }
  131. // Parse challenge
  132. digestParams := map[string]string{}
  133. for _, keyval := range strings.Split(challenge, ",") {
  134. param := strings.SplitN(keyval, "=", 2)
  135. if len(param) != 2 {
  136. continue
  137. }
  138. digestParams[strings.Trim(param[0], "\" ")] = strings.Trim(param[1], "\" ")
  139. }
  140. if len(digestParams) == 0 {
  141. return proxyError(fmt.Errorf("digest authentication challenge is malformed"))
  142. }
  143. algorithm := digestParams["algorithm"]
  144. if stale, ok := digestParams["stale"]; ok && stale == "true" {
  145. // Server indicated that the nonce is stale
  146. // Reset auth cache and state
  147. a.digestHeaders = nil
  148. a.state = DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED
  149. return nil
  150. }
  151. if a.digestHeaders == nil {
  152. d := &DigestHeaders{}
  153. if req.Method == "CONNECT" {
  154. d.Uri = req.URL.Host
  155. } else {
  156. d.Uri = req.URL.Scheme + "://" + req.URL.Host + req.URL.RequestURI()
  157. }
  158. d.Realm = digestParams["realm"]
  159. d.Qop = digestParams["qop"]
  160. d.Nonce = digestParams["nonce"]
  161. d.Opaque = digestParams["opaque"]
  162. if algorithm == "" {
  163. d.Algorithm = "MD5"
  164. } else {
  165. d.Algorithm = digestParams["algorithm"]
  166. }
  167. d.Nc = 0x0
  168. d.Cnonce = randomKey()
  169. d.Username = a.username
  170. d.Password = a.password
  171. a.digestHeaders = d
  172. }
  173. a.digestHeaders.ApplyAuth(req)
  174. a.state = DIGEST_HTTP_AUTH_STATE_RESPONSE_GENERATED
  175. return nil
  176. }
  177. func (a *DigestHttpAuthenticator) IsConnectionBased() bool {
  178. return false
  179. }
  180. func (a *DigestHttpAuthenticator) IsComplete() bool {
  181. return a.state == DIGEST_HTTP_AUTH_STATE_RESPONSE_GENERATED
  182. }
  183. func (a *DigestHttpAuthenticator) Reset() {
  184. a.state = DIGEST_HTTP_AUTH_STATE_CHALLENGE_RECEIVED
  185. }
  186. func (a *DigestHttpAuthenticator) PreAuthenticate(req *http.Request) error {
  187. if a.digestHeaders != nil {
  188. a.digestHeaders.ApplyAuth(req)
  189. }
  190. return nil
  191. }