github.com/htcondor/osdf-client/v6@v6.13.0-rc1.0.20231009141709-766e7b4d1dc8/get_stashserver_caches.go (about)

     1  package stashcp
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/rsa"
     6  	"crypto/sha1"
     7  	"crypto/x509"
     8  	_ "embed"
     9  	"encoding/hex"
    10  	"encoding/pem"
    11  	"errors"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"strings"
    16  
    17  	log "github.com/sirupsen/logrus"
    18  )
    19  
    20  //go:embed resources/opensciencegrid.org.pub
    21  var osgpubkey []byte
    22  
    23  func get_stashservers_caches(responselines_b [][]byte) (map[string][]string, error) {
    24  
    25  	/**
    26  		 After the geo order of the selected server list on line zero,
    27  	      the rest of the response is in .cvmfswhitelist format.
    28  	     This is done to avoid using https for every request on the
    29  	      wlcg-wpad servers and takes advantage of conveniently
    30  	      existing infrastructure.
    31  	     The format contains the following lines:
    32  	     1. Creation date stamp, e.g. 20200414170005.  For debugging
    33  	        only.
    34  	     2. Expiration date stamp, e.g. E20200421170005.  cvmfs clients
    35  	        check this to avoid replay attacks, but for this api that
    36  	        is not much of a risk so it is ignored.
    37  	     3. "Repository" name, e.g. Nstash-servers.  cvmfs clients
    38  	        also check this but it is not important here.
    39  	     4. With cvmfs the 4th line has a repository fingerprint, but
    40  	        for this api it instead contains a semi-colon separated list
    41  	        of named server lists.  Each server list is of the form
    42  	        name=servers where servers is comma-separated.  Ends with
    43  	        "hash=-sha1" because cvmfs_server expects the hash name
    44  	        to be there.  e.g.
    45  	        xroot=stashcache.t2.ucsd.edu,sg-gftp.pace.gatech.edu;xroots=xrootd-local.unl.edu,stashcache.t2.ucsd.edu;hash=-sha1
    46  	     5. A two-dash separator, i.e "--"
    47  	     6. The sha1 hash of lines 1 through 4.
    48  	     7. The signature, i.e. an RSA encryption of the hash that can
    49  	        be decrypted by the OSG cvmfs public key.  Contains binary
    50  	        information so it may contain a variable number of newlines
    51  	        which would have caused it to have been split into multiple
    52  		    response "lines".
    53  		**/
    54  
    55  	if len(responselines_b) < 8 {
    56  
    57  		log.Errorln("stashservers response too short, less than 8 lines:", len(responselines_b))
    58  		return nil, errors.New("stashservers response too short, less than 8 lines")
    59  	}
    60  
    61  	// Get the 5th row (4th index), the last 5 characters
    62  	hashname_b := string(responselines_b[4][len(responselines_b[4])-5:])
    63  
    64  	if hashname_b != "-sha1" {
    65  
    66  		log.Error("stashservers response does not have sha1 hash:", string(hashname_b))
    67  		return nil, errors.New("stashservers response does not have sha1 hash")
    68  	}
    69  
    70  	sha1Hash := sha1.New()
    71  	sha1Hash.Write(bytes.Join(responselines_b[1:5], []byte("\n")))
    72  	sha1Hash.Write([]byte("\n"))
    73  	hashed := sha1Hash.Sum(nil)
    74  	hashStr := hex.EncodeToString(hashed)
    75  
    76  	log.Debugln("Hashed:", hashStr, "From CVMFS:", string(responselines_b[6]))
    77  	if string(responselines_b[6]) != hashStr {
    78  		log.Debugln("stashservers hash", string(responselines_b[6]), "does not match expected hash ", hashname_b)
    79  		log.Debugln("hashed text:\n", string(hashname_b))
    80  		log.Errorln("stashservers response hash does not match expected hash")
    81  		return nil, errors.New("stashservers response hash does not match expected hash")
    82  	}
    83  
    84  	var pubKey *rsa.PublicKey
    85  	var err error
    86  	if pubKey, err = readPublicKey(); err != nil {
    87  		// The signature check isn't critical to be done everywhere;
    88  		// any tampering will likely to be caught somewhere and
    89  		// investigated.
    90  		log.Warnln("Public Key not found, will not verify caches")
    91  	} else {
    92  		sig := bytes.Join(responselines_b[7:], []byte("\n"))
    93  		err = rsa.VerifyPKCS1v15(pubKey, 0, []byte(hashStr), sig)
    94  		if err != nil {
    95  			log.Errorln("Error from public key verification of cache list:", err)
    96  			//return nil, err
    97  		} else {
    98  			log.Debugln("Signature Matched")
    99  		}
   100  
   101  	}
   102  
   103  	// Split the caches by type (xroot, xroots) in the returned by the GeoIP service
   104  	var toReturn = make(map[string][]string)
   105  	log.Debugf("Cache list: %s", string(responselines_b[4]))
   106  	for _, transferType := range strings.Split(string(responselines_b[4]), ";") {
   107  		splitType := strings.Split(transferType, "=")
   108  		toReturn[splitType[0]] = strings.Split(splitType[1], ",")
   109  	}
   110  
   111  	return toReturn, nil
   112  
   113  }
   114  
   115  func getKeyLocation() string {
   116  	osgpub := "opensciencegrid.org.pub"
   117  	var checkedLocation string = path.Join("/etc/cvmfs/keys/opensciencegrid.org/", osgpub)
   118  	if _, err := os.Stat(checkedLocation); err == nil {
   119  		return checkedLocation
   120  	}
   121  	prefix := os.Getenv("OSG_LOCATION")
   122  	if prefix != "" {
   123  		checkedLocation = path.Join(prefix, "etc/stashcache", osgpub)
   124  		if _, err := os.Stat(checkedLocation); err == nil {
   125  			return checkedLocation
   126  		}
   127  		checkedLocation = path.Join(prefix, "usr/share/stashcache", osgpub)
   128  		if _, err := os.Stat(checkedLocation); err == nil {
   129  			return checkedLocation
   130  		}
   131  
   132  	}
   133  
   134  	// Try the current directory
   135  	checkedLocation, _ = filepath.Abs(osgpub)
   136  	if _, err := os.Stat(checkedLocation); err == nil {
   137  		return checkedLocation
   138  	}
   139  
   140  	return ""
   141  
   142  }
   143  
   144  // Largely adapted from https://gist.github.com/jshap70/259a87a7146393aab5819873a193b88c
   145  func readPublicKey() (*rsa.PublicKey, error) {
   146  	var err error
   147  	publicKeyPath := getKeyLocation()
   148  	var pubkeyContents []byte
   149  	if publicKeyPath == "" {
   150  		pubkeyContents = osgpubkey
   151  	} else {
   152  		var err error
   153  		pubkeyContents, err = os.ReadFile(publicKeyPath)
   154  		if err != nil {
   155  			log.Errorln("Error reading public key:", err)
   156  			return nil, err
   157  		}
   158  	}
   159  
   160  	pubPem, rest := pem.Decode(pubkeyContents)
   161  	if pubPem.Type != "PUBLIC KEY" {
   162  		log.WithFields(log.Fields{"PEM Type": pubPem.Type}).Error("RSA public key is of the wrong type")
   163  		return nil, errors.New("RSA public key is of the wrong type")
   164  	}
   165  	var parsedKey interface{}
   166  	if parsedKey, err = x509.ParsePKIXPublicKey(pubPem.Bytes); err != nil {
   167  		log.Errorln("Unable to parse RSA public key:", err)
   168  		return nil, errors.New("Unable to parse RSA public key")
   169  	}
   170  	log.Debugf("Got a %T, with remaining data: %q", parsedKey, rest)
   171  
   172  	var pubKey *rsa.PublicKey
   173  	var ok bool
   174  	if pubKey, ok = parsedKey.(*rsa.PublicKey); !ok {
   175  		log.Errorln("Failed to convert RSA public key")
   176  	}
   177  
   178  	return pubKey, nil
   179  
   180  }