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  }