| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- // Copyright 2023 The Gitea Authors. All rights reserved.
- // SPDX-License-Identifier: MIT
-
- package chef
-
- import (
- "context"
- "crypto"
- "crypto/rsa"
- "crypto/sha1"
- "crypto/sha256"
- "crypto/x509"
- "encoding/base64"
- "encoding/pem"
- "errors"
- "fmt"
- "hash"
- "math/big"
- "net/http"
- "path"
- "regexp"
- "slices"
- "strconv"
- "strings"
- "time"
-
- user_model "code.gitea.io/gitea/models/user"
- chef_module "code.gitea.io/gitea/modules/packages/chef"
- "code.gitea.io/gitea/modules/util"
- "code.gitea.io/gitea/services/auth"
- )
-
- const (
- maxTimeDifference = 10 * time.Minute
- )
-
- var (
- algorithmPattern = regexp.MustCompile(`algorithm=(\w+)`)
- versionPattern = regexp.MustCompile(`version=(\d+\.\d+)`)
- authorizationPattern = regexp.MustCompile(`\AX-Ops-Authorization-(\d+)`)
-
- _ auth.Method = &Auth{}
- )
-
- // Documentation:
- // https://docs.chef.io/server/api_chef_server/#required-headers
- // https://github.com/chef-boneyard/chef-rfc/blob/master/rfc065-sign-v1.3.md
- // https://github.com/chef/mixlib-authentication/blob/bc8adbef833d4be23dc78cb23e6fe44b51ebc34f/lib/mixlib/authentication/signedheaderauth.rb
-
- type Auth struct{}
-
- func (a *Auth) Name() string {
- return "chef"
- }
-
- // Verify extracts the user from the signed request
- // If the request is signed with the user private key the user is verified.
- func (a *Auth) Verify(req *http.Request, w http.ResponseWriter, store auth.DataStore, sess auth.SessionStore) (*user_model.User, error) {
- u, err := getUserFromRequest(req)
- if err != nil {
- return nil, err
- }
- if u == nil {
- return nil, nil
- }
-
- pub, err := getUserPublicKey(req.Context(), u)
- if err != nil {
- return nil, err
- }
-
- if err := verifyTimestamp(req); err != nil {
- return nil, err
- }
-
- version, err := getSignVersion(req)
- if err != nil {
- return nil, err
- }
-
- if err := verifySignedHeaders(req, version, pub.(*rsa.PublicKey)); err != nil {
- return nil, err
- }
-
- return u, nil
- }
-
- func getUserFromRequest(req *http.Request) (*user_model.User, error) {
- username := req.Header.Get("X-Ops-Userid")
- if username == "" {
- return nil, nil
- }
-
- return user_model.GetUserByName(req.Context(), username)
- }
-
- func getUserPublicKey(ctx context.Context, u *user_model.User) (crypto.PublicKey, error) {
- pubKey, err := user_model.GetSetting(ctx, u.ID, chef_module.SettingPublicPem)
- if err != nil {
- return nil, err
- }
-
- pubPem, _ := pem.Decode([]byte(pubKey))
-
- return x509.ParsePKIXPublicKey(pubPem.Bytes)
- }
-
- func verifyTimestamp(req *http.Request) error {
- hdr := req.Header.Get("X-Ops-Timestamp")
- if hdr == "" {
- return util.NewInvalidArgumentErrorf("X-Ops-Timestamp header missing")
- }
-
- ts, err := time.Parse(time.RFC3339, hdr)
- if err != nil {
- return err
- }
-
- diff := time.Now().UTC().Sub(ts)
- if diff < 0 {
- diff = -diff
- }
-
- if diff > maxTimeDifference {
- return errors.New("time difference")
- }
-
- return nil
- }
-
- func getSignVersion(req *http.Request) (string, error) {
- hdr := req.Header.Get("X-Ops-Sign")
- if hdr == "" {
- return "", util.NewInvalidArgumentErrorf("X-Ops-Sign header missing")
- }
-
- m := versionPattern.FindStringSubmatch(hdr)
- if len(m) != 2 {
- return "", util.NewInvalidArgumentErrorf("invalid X-Ops-Sign header")
- }
-
- switch m[1] {
- case "1.0", "1.1", "1.2", "1.3":
- default:
- return "", util.NewInvalidArgumentErrorf("unsupported version")
- }
-
- version := m[1]
-
- m = algorithmPattern.FindStringSubmatch(hdr)
- if len(m) == 2 && m[1] != "sha1" && !(m[1] == "sha256" && version == "1.3") {
- return "", util.NewInvalidArgumentErrorf("unsupported algorithm")
- }
-
- return version, nil
- }
-
- func verifySignedHeaders(req *http.Request, version string, pub *rsa.PublicKey) error {
- authorizationData, err := getAuthorizationData(req)
- if err != nil {
- return err
- }
-
- checkData := buildCheckData(req, version)
-
- switch version {
- case "1.3":
- return verifyDataNew(authorizationData, checkData, pub, crypto.SHA256)
- case "1.2":
- return verifyDataNew(authorizationData, checkData, pub, crypto.SHA1)
- default:
- return verifyDataOld(authorizationData, checkData, pub)
- }
- }
-
- func getAuthorizationData(req *http.Request) ([]byte, error) {
- valueList := make(map[int]string)
- for k, vs := range req.Header {
- if m := authorizationPattern.FindStringSubmatch(k); m != nil {
- index, _ := strconv.Atoi(m[1])
- var v string
- if len(vs) == 0 {
- v = ""
- } else {
- v = vs[0]
- }
- valueList[index] = v
- }
- }
-
- tmp := make([]string, len(valueList))
- for k, v := range valueList {
- if k > len(tmp) {
- return nil, errors.New("invalid X-Ops-Authorization headers")
- }
- tmp[k-1] = v
- }
-
- return base64.StdEncoding.DecodeString(strings.Join(tmp, ""))
- }
-
- func buildCheckData(req *http.Request, version string) []byte {
- username := req.Header.Get("X-Ops-Userid")
- if version != "1.0" && version != "1.3" {
- sum := sha1.Sum([]byte(username))
- username = base64.StdEncoding.EncodeToString(sum[:])
- }
-
- var data string
- if version == "1.3" {
- data = fmt.Sprintf(
- "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",
- req.Method,
- path.Clean(req.URL.Path),
- req.Header.Get("X-Ops-Content-Hash"),
- version,
- req.Header.Get("X-Ops-Timestamp"),
- username,
- req.Header.Get("X-Ops-Server-Api-Version"),
- )
- } else {
- sum := sha1.Sum([]byte(path.Clean(req.URL.Path)))
- data = fmt.Sprintf(
- "Method:%s\nHashed Path:%s\nX-Ops-Content-Hash:%s\nX-Ops-Timestamp:%s\nX-Ops-UserId:%s",
- req.Method,
- base64.StdEncoding.EncodeToString(sum[:]),
- req.Header.Get("X-Ops-Content-Hash"),
- req.Header.Get("X-Ops-Timestamp"),
- username,
- )
- }
-
- return []byte(data)
- }
-
- func verifyDataNew(signature, data []byte, pub *rsa.PublicKey, algo crypto.Hash) error {
- var h hash.Hash
- if algo == crypto.SHA256 {
- h = sha256.New()
- } else {
- h = sha1.New()
- }
- if _, err := h.Write(data); err != nil {
- return err
- }
-
- return rsa.VerifyPKCS1v15(pub, algo, h.Sum(nil), signature)
- }
-
- func verifyDataOld(signature, data []byte, pub *rsa.PublicKey) error {
- c := new(big.Int)
- m := new(big.Int)
- m.SetBytes(signature)
- e := big.NewInt(int64(pub.E))
- c.Exp(m, e, pub.N)
-
- out := c.Bytes()
-
- skip := 0
- for i := 2; i < len(out); i++ {
- if i+1 >= len(out) {
- break
- }
- if out[i] == 0xFF && out[i+1] == 0 {
- skip = i + 2
- break
- }
- }
-
- if !slices.Equal(out[skip:], data) {
- return errors.New("could not verify signature")
- }
-
- return nil
- }
|