github.com/google/fleetspeak@v0.1.15-0.20240426164851-4f31f62c1aea/fleetspeak/src/server/https/client_certificate.go (about)

     1  package https
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"crypto/x509"
     6  	"encoding/base64"
     7  	"encoding/hex"
     8  	"encoding/pem"
     9  	"errors"
    10  	"fmt"
    11  	"net/http"
    12  	"net/url"
    13  	"strings"
    14  
    15  	log "github.com/golang/glog"
    16  	cpb "github.com/google/fleetspeak/fleetspeak/src/server/components/proto/fleetspeak_components"
    17  )
    18  
    19  // GetClientCert returns the client certificate from either the request header or TLS connection state.
    20  func GetClientCert(req *http.Request, frontendConfig *cpb.FrontendConfig) (*x509.Certificate, error) {
    21  	// Default to using mTLS if frontend_config or frontend_mode have not been set
    22  	if frontendConfig.GetFrontendMode() == nil {
    23  		return getCertFromTLS(req)
    24  	}
    25  
    26  	switch {
    27  	case frontendConfig.GetMtlsConfig() != nil:
    28  		return getCertFromTLS(req)
    29  	case frontendConfig.GetCleartextHeaderConfig() != nil:
    30  		return getCertFromHeader(frontendConfig.GetCleartextHeaderConfig().GetClientCertificateHeader(), req.Header)
    31  	case frontendConfig.GetHttpsHeaderConfig() != nil:
    32  		return getCertFromHeader(frontendConfig.GetHttpsHeaderConfig().GetClientCertificateHeader(), req.Header)
    33  	case frontendConfig.GetCleartextHeaderChecksumConfig() != nil:
    34  		cert, err := getCertFromHeader(frontendConfig.GetCleartextHeaderChecksumConfig().GetClientCertificateHeader(), req.Header)
    35  		if err != nil {
    36  			return nil, err
    37  		}
    38  		err = verifyCertSha256Checksum(req.Header.Get(frontendConfig.GetCleartextHeaderChecksumConfig().GetClientCertificateHeader()),
    39  			req.Header.Get(frontendConfig.GetCleartextHeaderChecksumConfig().GetClientCertificateChecksumHeader()))
    40  		if err != nil {
    41  			return nil, err
    42  		}
    43  		return cert, nil
    44  	case frontendConfig.GetHttpsHeaderChecksumConfig() != nil:
    45  		cert, err := getCertFromHeader(frontendConfig.GetHttpsHeaderChecksumConfig().GetClientCertificateHeader(), req.Header)
    46  		if err != nil {
    47  			return nil, err
    48  		}
    49  		err = verifyCertSha256Checksum(req.Header.Get(frontendConfig.GetHttpsHeaderChecksumConfig().GetClientCertificateHeader()),
    50  			req.Header.Get(frontendConfig.GetHttpsHeaderChecksumConfig().GetClientCertificateChecksumHeader()))
    51  		if err != nil {
    52  			return nil, err
    53  		}
    54  		return cert, nil
    55  	case frontendConfig.GetCleartextXfccConfig() != nil:
    56  		cert, err := getCertFromXfcc(frontendConfig.GetCleartextXfccConfig().GetClientCertificateHeader(), req.Header)
    57  		if err != nil {
    58  			return nil, err
    59  		}
    60  		return cert, nil
    61  	}
    62  
    63  	// Given the above switch statement is exhaustive, this error should never be reached
    64  	return nil, errors.New("invalid frontend_config")
    65  }
    66  
    67  // This function is calculating the client certificate checksum in the same fashion the GLB7 does.
    68  // We can also do so on the command line using openssl to calculate the certificate checksum.
    69  // openssl x509 -in mclient.crt -outform DER | openssl dgst -sha256 | cut -d ' ' -f2 | xxd -r -p - | openssl enc -a
    70  // For more info check out: https://gist.github.com/salrashid123/6e2a1eb9be95fb49506f1554e2d3d392
    71  func calculateClientCertificateChecksum(clientCert string) string {
    72  	// Most certificates are URL PEM encoded
    73  	if decodedCert, err := url.PathUnescape(clientCert); err != nil {
    74  		return ""
    75  	} else {
    76  		clientCert = decodedCert
    77  	}
    78  	// Decode the PEM string
    79  	block, rest := pem.Decode([]byte(clientCert))
    80  	if block == nil || len(rest) != 0 {
    81  		log.Warningln("Failed to decode PEM certificate")
    82  		return ""
    83  	}
    84  	// Calculate the SHA-256 digest of the DER certificate
    85  	sha256Digest := sha256.Sum256(block.Bytes)
    86  
    87  	// Convert the SHA-256 digest to a hexadecimal string
    88  	// sha256HexStr equivalent to: openssl x509 -n mclient.crt -outform DER | openssl dgst -sha256
    89  	sha256HexStr := fmt.Sprintf("%x", sha256Digest)
    90  
    91  	// sha256Binaryequivalent to: openssl x509 -n mclient.crt -outform DER | openssl dgst -sha256 | xxd -r -p -
    92  	sha256Binary, err := hex.DecodeString(sha256HexStr)
    93  	if err != nil {
    94  		log.Warningf("error decoding hexdump: %v\n", err)
    95  		return ""
    96  	}
    97  
    98  	// Convert the hexadecimal string to a base64 encoded string
    99  	// base64EncodedStr equivalent to: openssl x509 -n mclient.crt -outform DER | openssl dgst -sha256 | xxd -r -p - | openssl enc -a
   100  	// It also removes trailing "=" padding characters
   101  	base64EncodedStr := strings.TrimRight(base64.StdEncoding.EncodeToString(sha256Binary), "=")
   102  
   103  	// Return the base64 encoded string
   104  	return base64EncodedStr
   105  }
   106  
   107  func verifyCertSha256Checksum(headerCert string, clientCertSha256Checksum string) error {
   108  	if clientCertSha256Checksum == "" {
   109  		return errors.New("no client certificate checksum received in header")
   110  	}
   111  
   112  	calculatedClientCertSha256 := calculateClientCertificateChecksum(headerCert)
   113  	if calculatedClientCertSha256 != clientCertSha256Checksum {
   114  		return errors.New("received client certificate checksum is invalid")
   115  	}
   116  
   117  	return nil
   118  }
   119  
   120  const (
   121  	key int = iota
   122  	valueStart
   123  	value
   124  	quotedValue
   125  )
   126  
   127  type xfccParser struct {
   128  	header string
   129  }
   130  
   131  func (x *xfccParser) Next() (string, string) {
   132  	var keyStr, valueStr strings.Builder
   133  	state := key
   134  	var i int
   135  L:
   136  	for i = 0; i < len(x.header); i++ {
   137  		switch state {
   138  		case key:
   139  			if string(x.header[i]) == "=" {
   140  				state = valueStart
   141  			} else {
   142  				keyStr.Write([]byte{x.header[i]})
   143  			}
   144  		case valueStart:
   145  			if string(x.header[i]) == `"` {
   146  				state = quotedValue
   147  				continue L
   148  			} else {
   149  				state = value
   150  			}
   151  			fallthrough
   152  		case value:
   153  			if string(x.header[i]) == ";" {
   154  				break L
   155  			} else if string(x.header[i]) == "," {
   156  				break L
   157  			}
   158  			if string(x.header[i]) == `\` {
   159  				if len(x.header) == i+1 {
   160  					return "", ""
   161  				}
   162  				i++
   163  			}
   164  			valueStr.Write([]byte{x.header[i]})
   165  		case quotedValue:
   166  			if string(x.header[i]) == `"` {
   167  				state = value
   168  				continue L
   169  			}
   170  			if string(x.header[i]) == `\` {
   171  				if len(x.header) == i+1 {
   172  					return "", ""
   173  				}
   174  				i++
   175  			}
   176  			valueStr.Write([]byte{x.header[i]})
   177  		}
   178  	}
   179  	if len(x.header) > i {
   180  		x.header = x.header[i+1:]
   181  	} else {
   182  		x.header = ""
   183  	}
   184  	return keyStr.String(), valueStr.String()
   185  }
   186  
   187  // parses the X-Forwarded-Client-Cert header as defined by envoy
   188  // see: https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert
   189  // if multiple client certs are found, takes the first one
   190  func extractField(fieldName, headerCert string) string {
   191  	headerReader := &xfccParser{
   192  		header: headerCert,
   193  	}
   194  	for {
   195  		keyStr, valueStr := headerReader.Next()
   196  		if keyStr == "" {
   197  			return ""
   198  		}
   199  		if keyStr == fieldName {
   200  			return valueStr
   201  		}
   202  	}
   203  }
   204  
   205  func getCertFromXfcc(hn string, rh http.Header) (*x509.Certificate, error) {
   206  	headerCert := rh.Get(hn)
   207  	if headerCert == "" {
   208  		return nil, errors.New("no certificate found in header")
   209  	}
   210  	// support for envoy encoded xfcc header:
   211  	if certField := extractField("Cert", headerCert); certField != "" {
   212  		headerCert = certField
   213  	}
   214  	// Most certificates are URL PEM encoded
   215  	if decodedCert, err := url.PathUnescape(headerCert); err != nil {
   216  		return nil, err
   217  	} else {
   218  		headerCert = decodedCert
   219  	}
   220  	block, rest := pem.Decode([]byte(headerCert))
   221  	if block == nil || block.Type != "CERTIFICATE" {
   222  		return nil, errors.New("failed to decode PEM block containing certificate")
   223  	}
   224  	if len(rest) != 0 {
   225  		return nil, errors.New("received more than 1 client cert")
   226  	}
   227  	cert, err := x509.ParseCertificate(block.Bytes)
   228  	return cert, err
   229  }
   230  
   231  func getCertFromHeader(hn string, rh http.Header) (*x509.Certificate, error) {
   232  	headerCert := rh.Get(hn)
   233  	if headerCert == "" {
   234  		return nil, fmt.Errorf("no certificate found in header with name %q", hn)
   235  	}
   236  
   237  	decodedCert, err := url.PathUnescape(headerCert)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	// Most certificates are URL PEM encoded
   243  	block, rest := pem.Decode([]byte(decodedCert))
   244  	if block == nil {
   245  		return nil, errors.New("failed to decode PEM block")
   246  	}
   247  	if block.Type != "CERTIFICATE" {
   248  		return nil, errors.New("PEM block is not a certificate")
   249  	}
   250  	if len(rest) != 0 {
   251  		return nil, errors.New("received more than 1 client cert")
   252  	}
   253  	return x509.ParseCertificate(block.Bytes)
   254  }
   255  
   256  func getCertFromTLS(req *http.Request) (*x509.Certificate, error) {
   257  	if req.TLS == nil {
   258  		return nil, errors.New("TLS information not found")
   259  	}
   260  	if len(req.TLS.PeerCertificates) != 1 {
   261  		return nil, fmt.Errorf("expected 1 client cert, received %v", len(req.TLS.PeerCertificates))
   262  	}
   263  	cert := req.TLS.PeerCertificates[0]
   264  	return cert, nil
   265  }