digest.go 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  1. package ims
  2. import (
  3. "context"
  4. "crypto/md5"
  5. "crypto/rand"
  6. "encoding/base64"
  7. "encoding/hex"
  8. "errors"
  9. "fmt"
  10. "strings"
  11. "vocat/internal/vowifi"
  12. )
  13. type digestChallenge struct {
  14. Realm string
  15. Nonce string
  16. Opaque string
  17. Algorithm string
  18. QOP string
  19. Stale bool
  20. Proxy bool
  21. }
  22. type digestCredentials struct {
  23. Username string
  24. Password []byte
  25. AUTS string
  26. URI string
  27. Method string
  28. CNonce string
  29. NC uint32
  30. }
  31. func parseDigestChallenge(value string, proxy bool) (digestChallenge, error) {
  32. scheme, parameters, found := strings.Cut(strings.TrimSpace(value), " ")
  33. if !found || !strings.EqualFold(scheme, "Digest") {
  34. return digestChallenge{}, errors.New("ims: unsupported SIP authentication scheme")
  35. }
  36. directives, err := parseAuthDirectives(parameters)
  37. if err != nil {
  38. return digestChallenge{}, err
  39. }
  40. challenge := digestChallenge{
  41. Realm: directives["realm"],
  42. Nonce: directives["nonce"],
  43. Opaque: directives["opaque"],
  44. Algorithm: directives["algorithm"],
  45. Proxy: proxy,
  46. Stale: strings.EqualFold(directives["stale"], "true"),
  47. }
  48. if challenge.Realm == "" || challenge.Nonce == "" {
  49. return digestChallenge{}, errors.New("ims: incomplete SIP digest challenge")
  50. }
  51. if challenge.Algorithm == "" {
  52. // RFC 3310 inherits the HTTP Digest default: an omitted algorithm is
  53. // plain MD5, not AKA. This provider has no subscriber password and
  54. // must not misinterpret an ordinary nonce as RAND || AUTN.
  55. return digestChallenge{}, errors.New("ims: digest challenge omitted the AKA algorithm")
  56. }
  57. if !strings.EqualFold(challenge.Algorithm, "AKAv1-MD5") {
  58. return digestChallenge{}, fmt.Errorf("ims: unsupported digest algorithm %q", challenge.Algorithm)
  59. }
  60. if qop := directives["qop"]; qop != "" {
  61. for _, candidate := range strings.Split(qop, ",") {
  62. if strings.EqualFold(strings.TrimSpace(candidate), "auth") {
  63. challenge.QOP = "auth"
  64. break
  65. }
  66. }
  67. if challenge.QOP == "" {
  68. return digestChallenge{}, errors.New("ims: digest challenge does not offer qop=auth")
  69. }
  70. }
  71. return challenge, nil
  72. }
  73. func parseAuthDirectives(value string) (map[string]string, error) {
  74. directives := make(map[string]string)
  75. for index := 0; index < len(value); {
  76. for index < len(value) && (value[index] == ' ' || value[index] == '\t' || value[index] == ',') {
  77. index++
  78. }
  79. if index == len(value) {
  80. break
  81. }
  82. keyStart := index
  83. for index < len(value) && value[index] != '=' && value[index] != ',' {
  84. index++
  85. }
  86. if index == len(value) || value[index] != '=' {
  87. return nil, errors.New("ims: malformed digest directive")
  88. }
  89. key := strings.ToLower(strings.TrimSpace(value[keyStart:index]))
  90. index++
  91. for index < len(value) && (value[index] == ' ' || value[index] == '\t') {
  92. index++
  93. }
  94. var directiveValue strings.Builder
  95. if index < len(value) && value[index] == '"' {
  96. index++
  97. closed := false
  98. for index < len(value) {
  99. switch value[index] {
  100. case '\\':
  101. index++
  102. if index == len(value) {
  103. return nil, errors.New("ims: malformed quoted digest directive")
  104. }
  105. directiveValue.WriteByte(value[index])
  106. index++
  107. case '"':
  108. index++
  109. closed = true
  110. default:
  111. directiveValue.WriteByte(value[index])
  112. index++
  113. }
  114. if closed {
  115. break
  116. }
  117. }
  118. if !closed {
  119. return nil, errors.New("ims: unterminated quoted digest directive")
  120. }
  121. } else {
  122. start := index
  123. for index < len(value) && value[index] != ',' {
  124. index++
  125. }
  126. directiveValue.WriteString(strings.TrimSpace(value[start:index]))
  127. }
  128. if key == "" {
  129. return nil, errors.New("ims: empty digest directive name")
  130. }
  131. directives[key] = directiveValue.String()
  132. for index < len(value) && value[index] != ',' {
  133. if value[index] != ' ' && value[index] != '\t' {
  134. return nil, errors.New("ims: malformed digest directive separator")
  135. }
  136. index++
  137. }
  138. }
  139. return directives, nil
  140. }
  141. type akaMaterial struct {
  142. password []byte
  143. auts []byte
  144. ck []byte
  145. ik []byte
  146. }
  147. func clearAKAMaterial(material *akaMaterial) {
  148. if material == nil {
  149. return
  150. }
  151. zeroBytes(material.password)
  152. zeroBytes(material.auts)
  153. zeroBytes(material.ck)
  154. zeroBytes(material.ik)
  155. *material = akaMaterial{}
  156. }
  157. func authenticateAKA(
  158. ctx context.Context,
  159. provider vowifi.AKAProvider,
  160. identity vowifi.SIMIdentity,
  161. challenge digestChallenge,
  162. ) (akaMaterial, error) {
  163. nonce, err := decodeAKANonce(challenge.Nonce)
  164. if err != nil {
  165. return akaMaterial{}, err
  166. }
  167. // 3GPP HTTP Digest AKA encodes RAND || AUTN as the first 32 nonce octets.
  168. // Following server data remains in the digest nonce and never enters USIM.
  169. var akaChallenge vowifi.AKAChallenge
  170. copy(akaChallenge.RAND[:], nonce[:16])
  171. copy(akaChallenge.AUTN[:], nonce[16:32])
  172. result, err := provider.Authenticate(ctx, identity, akaChallenge)
  173. if err != nil {
  174. return akaMaterial{}, fmt.Errorf("ims: USIM AKA authentication failed: %w", err)
  175. }
  176. if result.SynchronizationFailure || len(result.AUTS) > 0 {
  177. if !result.SynchronizationFailure || len(result.AUTS) != 14 {
  178. return akaMaterial{}, errors.New("ims: USIM returned malformed AKA synchronization evidence")
  179. }
  180. return akaMaterial{auts: append([]byte(nil), result.AUTS...)}, nil
  181. }
  182. res, err := extractRES(result)
  183. if err != nil {
  184. return akaMaterial{}, err
  185. }
  186. return akaMaterial{
  187. password: res,
  188. ck: append([]byte(nil), result.CK...),
  189. ik: append([]byte(nil), result.IK...),
  190. }, nil
  191. }
  192. func decodeAKANonce(value string) ([]byte, error) {
  193. var decoded []byte
  194. var err error
  195. for _, encoding := range []*base64.Encoding{
  196. base64.StdEncoding,
  197. base64.RawStdEncoding,
  198. base64.URLEncoding,
  199. base64.RawURLEncoding,
  200. } {
  201. decoded, err = encoding.DecodeString(strings.TrimSpace(value))
  202. if err == nil {
  203. break
  204. }
  205. }
  206. if err != nil || len(decoded) < 32 {
  207. return nil, errors.New("ims: invalid AKA nonce")
  208. }
  209. return decoded, nil
  210. }
  211. func extractRES(result vowifi.AKAResult) ([]byte, error) {
  212. if len(result.RES) == 0 {
  213. return nil, errors.New("ims: USIM returned an empty AKA result")
  214. }
  215. if len(result.RES) < 4 || len(result.RES) > 16 {
  216. return nil, errors.New("ims: USIM returned an invalid RES length")
  217. }
  218. return append([]byte(nil), result.RES...), nil
  219. }
  220. func newDigestCredentials(
  221. username string,
  222. password []byte,
  223. uri string,
  224. method string,
  225. nc uint32,
  226. ) (digestCredentials, error) {
  227. cnonceBytes := make([]byte, 16)
  228. if _, err := rand.Read(cnonceBytes); err != nil {
  229. return digestCredentials{}, fmt.Errorf("ims: create digest cnonce: %w", err)
  230. }
  231. return digestCredentials{
  232. Username: username,
  233. Password: password,
  234. URI: uri,
  235. Method: method,
  236. CNonce: hex.EncodeToString(cnonceBytes),
  237. NC: nc,
  238. }, nil
  239. }
  240. func buildDigestAuthorization(challenge digestChallenge, credentials digestCredentials) string {
  241. nc := fmt.Sprintf("%08x", credentials.NC)
  242. response := digestResponse(
  243. credentials.Username,
  244. challenge.Realm,
  245. credentials.Password,
  246. credentials.Method,
  247. credentials.URI,
  248. challenge.Nonce,
  249. nc,
  250. credentials.CNonce,
  251. challenge.QOP,
  252. )
  253. parts := []string{
  254. `username="` + quoteDigest(credentials.Username) + `"`,
  255. `realm="` + quoteDigest(challenge.Realm) + `"`,
  256. `nonce="` + quoteDigest(challenge.Nonce) + `"`,
  257. `uri="` + quoteDigest(credentials.URI) + `"`,
  258. `response="` + response + `"`,
  259. "algorithm=AKAv1-MD5",
  260. }
  261. if challenge.Opaque != "" {
  262. parts = append(parts, `opaque="`+quoteDigest(challenge.Opaque)+`"`)
  263. }
  264. if challenge.QOP != "" {
  265. parts = append(parts,
  266. "qop="+challenge.QOP,
  267. "nc="+nc,
  268. `cnonce="`+quoteDigest(credentials.CNonce)+`"`,
  269. )
  270. }
  271. if credentials.AUTS != "" {
  272. parts = append(parts, `auts="`+quoteDigest(credentials.AUTS)+`"`)
  273. }
  274. return "Digest " + strings.Join(parts, ", ")
  275. }
  276. func digestResponse(
  277. username string,
  278. realm string,
  279. password []byte,
  280. method string,
  281. uri string,
  282. nonce string,
  283. nc string,
  284. cnonce string,
  285. qop string,
  286. ) string {
  287. ha1Hash := md5.New()
  288. _, _ = ha1Hash.Write([]byte(username + ":" + realm + ":"))
  289. _, _ = ha1Hash.Write(password)
  290. ha1 := hex.EncodeToString(ha1Hash.Sum(nil))
  291. ha2 := md5Hex(method + ":" + uri)
  292. if qop == "" {
  293. return md5Hex(ha1 + ":" + nonce + ":" + ha2)
  294. }
  295. return md5Hex(ha1 + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + ha2)
  296. }
  297. func md5Hex(value string) string {
  298. sum := md5.Sum([]byte(value))
  299. return hex.EncodeToString(sum[:])
  300. }
  301. func quoteDigest(value string) string {
  302. value = strings.ReplaceAll(value, `\`, `\\`)
  303. return strings.ReplaceAll(value, `"`, `\"`)
  304. }