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