code.gitea.io/gitea@v1.22.3/routers/api/packages/chef/auth.go (about) 1 // Copyright 2023 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package chef 5 6 import ( 7 "context" 8 "crypto" 9 "crypto/rsa" 10 "crypto/sha1" 11 "crypto/sha256" 12 "crypto/x509" 13 "encoding/base64" 14 "encoding/pem" 15 "fmt" 16 "hash" 17 "math/big" 18 "net/http" 19 "path" 20 "regexp" 21 "slices" 22 "strconv" 23 "strings" 24 "time" 25 26 user_model "code.gitea.io/gitea/models/user" 27 chef_module "code.gitea.io/gitea/modules/packages/chef" 28 "code.gitea.io/gitea/modules/util" 29 "code.gitea.io/gitea/services/auth" 30 ) 31 32 const ( 33 maxTimeDifference = 10 * time.Minute 34 ) 35 36 var ( 37 algorithmPattern = regexp.MustCompile(`algorithm=(\w+)`) 38 versionPattern = regexp.MustCompile(`version=(\d+\.\d+)`) 39 authorizationPattern = regexp.MustCompile(`\AX-Ops-Authorization-(\d+)`) 40 41 _ auth.Method = &Auth{} 42 ) 43 44 // Documentation: 45 // https://docs.chef.io/server/api_chef_server/#required-headers 46 // https://github.com/chef-boneyard/chef-rfc/blob/master/rfc065-sign-v1.3.md 47 // https://github.com/chef/mixlib-authentication/blob/bc8adbef833d4be23dc78cb23e6fe44b51ebc34f/lib/mixlib/authentication/signedheaderauth.rb 48 49 type Auth struct{} 50 51 func (a *Auth) Name() string { 52 return "chef" 53 } 54 55 // Verify extracts the user from the signed request 56 // If the request is signed with the user private key the user is verified. 57 func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) { 58 u, err := getUserFromRequest(req) 59 if err != nil { 60 return nil, err 61 } 62 if u == nil { 63 return nil, nil 64 } 65 66 pub, err := getUserPublicKey(req.Context(), u) 67 if err != nil { 68 return nil, err 69 } 70 71 if err := verifyTimestamp(req); err != nil { 72 return nil, err 73 } 74 75 version, err := getSignVersion(req) 76 if err != nil { 77 return nil, err 78 } 79 80 if err := verifySignedHeaders(req, version, pub.(*rsa.PublicKey)); err != nil { 81 return nil, err 82 } 83 84 return u, nil 85 } 86 87 func getUserFromRequest(req *http.Request) (*user_model.User, error) { 88 username := req.Header.Get("X-Ops-Userid") 89 if username == "" { 90 return nil, nil 91 } 92 93 return user_model.GetUserByName(req.Context(), username) 94 } 95 96 func getUserPublicKey(ctx context.Context, u *user_model.User) (crypto.PublicKey, error) { 97 pubKey, err := user_model.GetSetting(ctx, u.ID, chef_module.SettingPublicPem) 98 if err != nil { 99 return nil, err 100 } 101 102 pubPem, _ := pem.Decode([]byte(pubKey)) 103 104 return x509.ParsePKIXPublicKey(pubPem.Bytes) 105 } 106 107 func verifyTimestamp(req *http.Request) error { 108 hdr := req.Header.Get("X-Ops-Timestamp") 109 if hdr == "" { 110 return util.NewInvalidArgumentErrorf("X-Ops-Timestamp header missing") 111 } 112 113 ts, err := time.Parse(time.RFC3339, hdr) 114 if err != nil { 115 return err 116 } 117 118 diff := time.Now().UTC().Sub(ts) 119 if diff < 0 { 120 diff = -diff 121 } 122 123 if diff > maxTimeDifference { 124 return fmt.Errorf("time difference") 125 } 126 127 return nil 128 } 129 130 func getSignVersion(req *http.Request) (string, error) { 131 hdr := req.Header.Get("X-Ops-Sign") 132 if hdr == "" { 133 return "", util.NewInvalidArgumentErrorf("X-Ops-Sign header missing") 134 } 135 136 m := versionPattern.FindStringSubmatch(hdr) 137 if len(m) != 2 { 138 return "", util.NewInvalidArgumentErrorf("invalid X-Ops-Sign header") 139 } 140 141 switch m[1] { 142 case "1.0", "1.1", "1.2", "1.3": 143 default: 144 return "", util.NewInvalidArgumentErrorf("unsupported version") 145 } 146 147 version := m[1] 148 149 m = algorithmPattern.FindStringSubmatch(hdr) 150 if len(m) == 2 && m[1] != "sha1" && !(m[1] == "sha256" && version == "1.3") { 151 return "", util.NewInvalidArgumentErrorf("unsupported algorithm") 152 } 153 154 return version, nil 155 } 156 157 func verifySignedHeaders(req *http.Request, version string, pub *rsa.PublicKey) error { 158 authorizationData, err := getAuthorizationData(req) 159 if err != nil { 160 return err 161 } 162 163 checkData := buildCheckData(req, version) 164 165 switch version { 166 case "1.3": 167 return verifyDataNew(authorizationData, checkData, pub, crypto.SHA256) 168 case "1.2": 169 return verifyDataNew(authorizationData, checkData, pub, crypto.SHA1) 170 default: 171 return verifyDataOld(authorizationData, checkData, pub) 172 } 173 } 174 175 func getAuthorizationData(req *http.Request) ([]byte, error) { 176 valueList := make(map[int]string) 177 for k, vs := range req.Header { 178 if m := authorizationPattern.FindStringSubmatch(k); m != nil { 179 index, _ := strconv.Atoi(m[1]) 180 var v string 181 if len(vs) == 0 { 182 v = "" 183 } else { 184 v = vs[0] 185 } 186 valueList[index] = v 187 } 188 } 189 190 tmp := make([]string, len(valueList)) 191 for k, v := range valueList { 192 if k > len(tmp) { 193 return nil, fmt.Errorf("invalid X-Ops-Authorization headers") 194 } 195 tmp[k-1] = v 196 } 197 198 return base64.StdEncoding.DecodeString(strings.Join(tmp, "")) 199 } 200 201 func buildCheckData(req *http.Request, version string) []byte { 202 username := req.Header.Get("X-Ops-Userid") 203 if version != "1.0" && version != "1.3" { 204 sum := sha1.Sum([]byte(username)) 205 username = base64.StdEncoding.EncodeToString(sum[:]) 206 } 207 208 var data string 209 if version == "1.3" { 210 data = fmt.Sprintf( 211 "Method:%s\nPath:%s\nX-Ops-Content-Hash:%s\nX-Ops-Sign:version=%s\nX-Ops-Timestamp:%s\nX-Ops-UserId:%s\nX-Ops-Server-API-Version:%s", 212 req.Method, 213 path.Clean(req.URL.Path), 214 req.Header.Get("X-Ops-Content-Hash"), 215 version, 216 req.Header.Get("X-Ops-Timestamp"), 217 username, 218 req.Header.Get("X-Ops-Server-Api-Version"), 219 ) 220 } else { 221 sum := sha1.Sum([]byte(path.Clean(req.URL.Path))) 222 data = fmt.Sprintf( 223 "Method:%s\nHashed Path:%s\nX-Ops-Content-Hash:%s\nX-Ops-Timestamp:%s\nX-Ops-UserId:%s", 224 req.Method, 225 base64.StdEncoding.EncodeToString(sum[:]), 226 req.Header.Get("X-Ops-Content-Hash"), 227 req.Header.Get("X-Ops-Timestamp"), 228 username, 229 ) 230 } 231 232 return []byte(data) 233 } 234 235 func verifyDataNew(signature, data []byte, pub *rsa.PublicKey, algo crypto.Hash) error { 236 var h hash.Hash 237 if algo == crypto.SHA256 { 238 h = sha256.New() 239 } else { 240 h = sha1.New() 241 } 242 if _, err := h.Write(data); err != nil { 243 return err 244 } 245 246 return rsa.VerifyPKCS1v15(pub, algo, h.Sum(nil), signature) 247 } 248 249 func verifyDataOld(signature, data []byte, pub *rsa.PublicKey) error { 250 c := new(big.Int) 251 m := new(big.Int) 252 m.SetBytes(signature) 253 e := big.NewInt(int64(pub.E)) 254 c.Exp(m, e, pub.N) 255 256 out := c.Bytes() 257 258 skip := 0 259 for i := 2; i < len(out); i++ { 260 if i+1 >= len(out) { 261 break 262 } 263 if out[i] == 0xFF && out[i+1] == 0 { 264 skip = i + 2 265 break 266 } 267 } 268 269 if !slices.Equal(out[skip:], data) { 270 return fmt.Errorf("could not verify signature") 271 } 272 273 return nil 274 }