github.com/blend/go-sdk@v1.20220411.3/envoyutil/xfcc.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package envoyutil
     9  
    10  import (
    11  	"crypto/x509"
    12  	"encoding/hex"
    13  	"fmt"
    14  	"net/url"
    15  	"strings"
    16  
    17  	"github.com/blend/go-sdk/certutil"
    18  	"github.com/blend/go-sdk/ex"
    19  )
    20  
    21  // XFCC represents a proxy header containing certificate information for the client
    22  // that is sending the request to the proxy.
    23  // See https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-client-cert
    24  type XFCC []XFCCElement
    25  
    26  // XFCCElement is an element in an XFCC header (see `XFCC`).
    27  type XFCCElement struct {
    28  	// By contains Subject Alternative Name (URI type) of the current proxy's
    29  	// certificate.	This can be decoded as a `*url.URL` via `xe.DecodeBy()`.
    30  	By string
    31  	// Hash contains the SHA 256 digest of the current client certificate; this
    32  	// is a string of 64 hexadecimal characters. This can be converted to the raw
    33  	// bytes underlying the hex string via `xe.DecodeHash()`.
    34  	Hash string
    35  	// Cert contains the entire client certificate in URL encoded PEM format.
    36  	// This can be decoded as a `*x509.Certificate` via `xe.DecodeCert()`.
    37  	Cert string
    38  	// Chain contains entire client certificate chain (including the leaf certificate)
    39  	// in URL encoded PEM format. This can be decoded as a `[]*x509.Certificate` via
    40  	// `xe.DecodeChain()`.
    41  	Chain string
    42  	// Subject contains the `Subject` field of the current client certificate.
    43  	Subject string
    44  	// URI contains the URI SAN of the current client certificate (assumes only
    45  	// one URI SAN). This can be decoded as a `*url.URL` via `xe.DecodeURI()`.
    46  	URI string
    47  	// DNS contains the DNS SANs of the current client certificate. A client
    48  	// certificate may contain multiple DNS SANs, each will be a separate
    49  	// key-value pair in the XFCC element.
    50  	DNS []string
    51  }
    52  
    53  // DecodeBy decodes the `By` element from a URI string to a `*url.URL`.
    54  func (xe XFCCElement) DecodeBy() (*url.URL, error) {
    55  	u, err := url.Parse(xe.By)
    56  	if err != nil {
    57  		return nil, ex.New(ErrXFCCParsing).WithInner(err)
    58  	}
    59  
    60  	return u, nil
    61  }
    62  
    63  // DecodeHash decodes the `Hash` element from a hex string to raw bytes.
    64  func (xe XFCCElement) DecodeHash() ([]byte, error) {
    65  	bs, err := hex.DecodeString(xe.Hash)
    66  	if err != nil {
    67  		return nil, ex.New(ErrXFCCParsing).WithInner(err)
    68  	}
    69  
    70  	return bs, nil
    71  }
    72  
    73  // DecodeCert decodes the `Cert` element from a URL encoded PEM to a
    74  // single `x509.Certificate`.
    75  func (xe XFCCElement) DecodeCert() (*x509.Certificate, error) {
    76  	if xe.Cert == "" {
    77  		return nil, nil
    78  	}
    79  
    80  	value, err := url.QueryUnescape(xe.Cert)
    81  	if err != nil {
    82  		return nil, ex.New(ErrXFCCParsing).WithInner(err)
    83  	}
    84  
    85  	parsed, err := certutil.ParseCertPEM([]byte(value))
    86  	if err != nil {
    87  		return nil, ex.New(ErrXFCCParsing).WithInner(err)
    88  	}
    89  
    90  	if len(parsed) != 1 {
    91  		err = ex.New(
    92  			ErrXFCCParsing,
    93  			ex.OptMessagef("Incorrect number of certificates; expected 1 got %d", len(parsed)),
    94  		)
    95  		return nil, err
    96  	}
    97  
    98  	return parsed[0], nil
    99  }
   100  
   101  // DecodeChain decodes the `Chain` element from a URL encoded PEM to a
   102  // `[]x509.Certificate`.
   103  func (xe XFCCElement) DecodeChain() ([]*x509.Certificate, error) {
   104  	if xe.Chain == "" {
   105  		return nil, nil
   106  	}
   107  
   108  	value, err := url.QueryUnescape(xe.Chain)
   109  	if err != nil {
   110  		return nil, ex.New(ErrXFCCParsing).WithInner(err)
   111  	}
   112  
   113  	parsed, err := certutil.ParseCertPEM([]byte(value))
   114  	if err != nil {
   115  		return nil, ex.New(ErrXFCCParsing).WithInner(err)
   116  	}
   117  
   118  	return parsed, nil
   119  
   120  }
   121  
   122  // DecodeURI decodes the `URI` element from a URI string to a `*url.URL`.
   123  func (xe XFCCElement) DecodeURI() (*url.URL, error) {
   124  	u, err := url.Parse(xe.URI)
   125  	if err != nil {
   126  		return nil, ex.New(ErrXFCCParsing).WithInner(err)
   127  	}
   128  
   129  	return u, nil
   130  }
   131  
   132  // maybeQuoted quotes a string value that may need to be quoted to be part of an
   133  // XFCC header. It will use `%q` formatting to quote the value if it contains any
   134  // of `,` (comma), `;` (semi-colon), `=` (equals) or `"` (double quote).
   135  func maybeQuoted(value string) string {
   136  	if strings.ContainsAny(value, `,;="`) {
   137  		return fmt.Sprintf("%q", value)
   138  	}
   139  	return value
   140  }
   141  
   142  // String converts the parsed XFCC element **back** to a string. This is intended
   143  // for debugging purposes and is not particularly
   144  func (xe XFCCElement) String() string {
   145  	parts := []string{}
   146  	if xe.By != "" {
   147  		parts = append(parts, fmt.Sprintf("By=%s", maybeQuoted(xe.By)))
   148  	}
   149  	if xe.Hash != "" {
   150  		parts = append(parts, fmt.Sprintf("Hash=%s", maybeQuoted(xe.Hash)))
   151  	}
   152  	if xe.Cert != "" {
   153  		parts = append(parts, fmt.Sprintf("Cert=%s", maybeQuoted(xe.Cert)))
   154  	}
   155  	if xe.Chain != "" {
   156  		parts = append(parts, fmt.Sprintf("Chain=%s", maybeQuoted(xe.Chain)))
   157  	}
   158  	if xe.Subject != "" {
   159  		parts = append(parts, fmt.Sprintf("Subject=%q", xe.Subject))
   160  	}
   161  	if xe.URI != "" {
   162  		parts = append(parts, fmt.Sprintf("URI=%s", maybeQuoted(xe.URI)))
   163  	}
   164  	for _, dnsSAN := range xe.DNS {
   165  		parts = append(parts, fmt.Sprintf("DNS=%s", maybeQuoted(dnsSAN)))
   166  	}
   167  
   168  	return strings.Join(parts, ";")
   169  }
   170  
   171  const (
   172  	// HeaderXFCC is the header key for forwarded client cert
   173  	HeaderXFCC = "x-forwarded-client-cert"
   174  )
   175  
   176  const (
   177  	// ErrXFCCParsing is the class of error returned when parsing XFCC fails
   178  	ErrXFCCParsing = ex.Class("Error Parsing X-Forwarded-Client-Cert")
   179  
   180  	// initialValueCapacity is the capacity used for a key in a key-value
   181  	// pair from an XFCC header.
   182  	initialKeyCapacity = 4
   183  	// initialValueCapacity is the capacity used for a value in a key-value
   184  	// pair from an XFCC header.
   185  	initialValueCapacity = 8
   186  )
   187  
   188  type parseXFCCState int
   189  
   190  const (
   191  	parseXFCCKey parseXFCCState = iota
   192  	parseXFCCValueStart
   193  	parseXFCCValue
   194  	parseXFCCValueQuoted
   195  )
   196  
   197  // xfccParser holds state while an XFCC header is being parsed.
   198  type xfccParser struct {
   199  	Header  []rune
   200  	Index   int
   201  	State   parseXFCCState
   202  	Key     []rune
   203  	Value   []rune
   204  	Element XFCCElement
   205  	Parsed  XFCC
   206  }
   207  
   208  // ParseXFCC parses the XFCC header.
   209  func ParseXFCC(header string) (XFCC, error) {
   210  	if header == "" {
   211  		return XFCC{}, nil
   212  	}
   213  
   214  	xp := &xfccParser{
   215  		Header: []rune(header),
   216  		Index:  0,
   217  		State:  parseXFCCKey,
   218  		Key:    make([]rune, 0, initialKeyCapacity),
   219  		Value:  make([]rune, 0, initialValueCapacity),
   220  	}
   221  	lastCharacter := xp.Header[len(xp.Header)-1]
   222  	if lastCharacter == ',' || lastCharacter == ';' {
   223  		return XFCC{}, ex.New(ErrXFCCParsing).WithMessage("Ends with separator character")
   224  	}
   225  
   226  	for xp.Index < len(xp.Header) {
   227  		char := xp.Header[xp.Index]
   228  		switch xp.State {
   229  		case parseXFCCKey:
   230  			xp.HandleKeyCharacter(char)
   231  		case parseXFCCValueStart:
   232  			xp.HandleValueStartCharacter(char)
   233  		case parseXFCCValue:
   234  			err := xp.HandleValueCharacter(char)
   235  			if err != nil {
   236  				return XFCC{}, err
   237  			}
   238  		case parseXFCCValueQuoted:
   239  			err := xp.HandleQuotedValueCharacter(char)
   240  			if err != nil {
   241  				return XFCC{}, err
   242  			}
   243  		}
   244  
   245  		// Increment the index for the next iteration. (Note that branches of the
   246  		// `switch` statement may have already incremented the index as well.)
   247  		xp.Index++
   248  	}
   249  
   250  	err := xp.Finalize()
   251  	if err != nil {
   252  		return XFCC{}, err
   253  	}
   254  
   255  	return xp.Parsed, nil
   256  }
   257  
   258  // FillKeyValue takes the currently active `.Key` and `.Value` and populates
   259  // the current `.Element`.
   260  func (xp *xfccParser) FillKeyValue() error {
   261  	keyLower := strings.ToLower(string(xp.Key))
   262  	switch keyLower {
   263  	case "by":
   264  		if xp.Element.By != "" {
   265  			return ex.New(ErrXFCCParsing).WithMessagef("Key already encountered %q", keyLower)
   266  		}
   267  		xp.Element.By = string(xp.Value)
   268  	case "hash":
   269  		if len(xp.Element.Hash) > 0 {
   270  			return ex.New(ErrXFCCParsing).WithMessagef("Key already encountered %q", keyLower)
   271  		}
   272  		xp.Element.Hash = string(xp.Value)
   273  	case "cert":
   274  		if len(xp.Element.Cert) > 0 {
   275  			return ex.New(ErrXFCCParsing).WithMessagef("Key already encountered %q", keyLower)
   276  		}
   277  		xp.Element.Cert = string(xp.Value)
   278  	case "chain":
   279  		if len(xp.Element.Chain) > 0 {
   280  			return ex.New(ErrXFCCParsing).WithMessagef("Key already encountered %q", keyLower)
   281  		}
   282  		xp.Element.Chain = string(xp.Value)
   283  	case "subject":
   284  		if len(xp.Element.Subject) > 0 {
   285  			return ex.New(ErrXFCCParsing).WithMessagef("Key already encountered %q", keyLower)
   286  		}
   287  		xp.Element.Subject = string(xp.Value)
   288  	case "uri":
   289  		if xp.Element.URI != "" {
   290  			return ex.New(ErrXFCCParsing).WithMessagef("Key already encountered %q", keyLower)
   291  		}
   292  		xp.Element.URI = string(xp.Value)
   293  	case "dns":
   294  		xp.Element.DNS = append(xp.Element.DNS, string(xp.Value))
   295  	default:
   296  		return ex.New(ErrXFCCParsing).WithMessagef("Unknown key %q", keyLower)
   297  	}
   298  
   299  	return nil
   300  }
   301  
   302  // HandleKeyCharacter advances the state machine if the current state is
   303  // `parseXFCCKey`.
   304  func (xp *xfccParser) HandleKeyCharacter(char rune) {
   305  	if char == '=' {
   306  		xp.State = parseXFCCValueStart
   307  	} else {
   308  		xp.Key = append(xp.Key, char)
   309  	}
   310  }
   311  
   312  // HandleValueStartCharacter advances the state machine if the current state is
   313  // `parseXFCCValueStart`.
   314  func (xp *xfccParser) HandleValueStartCharacter(char rune) {
   315  	if char == '"' {
   316  		xp.State = parseXFCCValueQuoted
   317  	} else {
   318  		xp.Value = append(xp.Value, char)
   319  		xp.State = parseXFCCValue
   320  	}
   321  }
   322  
   323  // HandleValueCharacter advances the state machine if the current state is
   324  // `parseXFCCValue`.
   325  func (xp *xfccParser) HandleValueCharacter(char rune) error {
   326  	if char == ',' || char == ';' {
   327  		if len(xp.Key) == 0 || len(xp.Value) == 0 {
   328  			return ex.New(ErrXFCCParsing).WithMessage("Key or Value missing")
   329  		}
   330  		err := xp.FillKeyValue()
   331  		if err != nil {
   332  			return err
   333  		}
   334  
   335  		xp.Key = make([]rune, 0, initialKeyCapacity)
   336  		xp.Value = make([]rune, 0, initialValueCapacity)
   337  		xp.State = parseXFCCKey
   338  		if char == ',' {
   339  			xp.Parsed = append(xp.Parsed, xp.Element)
   340  			xp.Element = XFCCElement{}
   341  		}
   342  	} else {
   343  		xp.Value = append(xp.Value, char)
   344  	}
   345  
   346  	return nil
   347  }
   348  
   349  // HandleQuotedValueCharacter advances the state machine if the current state is
   350  // `parseXFCCValueQuoted`.
   351  func (xp *xfccParser) HandleQuotedValueCharacter(char rune) error {
   352  	if char == '\\' {
   353  		nextIndex := xp.Index + 1
   354  		if nextIndex < len(xp.Header) && xp.Header[nextIndex] == '"' {
   355  			// Consume two characters at once here (since we have an
   356  			// escaped quote).
   357  			xp.Value = append(xp.Value, '"')
   358  			xp.Index = nextIndex
   359  		} else {
   360  			xp.Value = append(xp.Value, char)
   361  		}
   362  	} else if char == '"' {
   363  		// Since the **escaped quote** case `\"` has already been
   364  		// covered, this case should only occur in the closing quote
   365  		// case.
   366  		nextIndex := xp.Index + 1
   367  		if nextIndex < len(xp.Header) {
   368  			if xp.Header[nextIndex] == ';' || xp.Header[nextIndex] == ',' {
   369  				// Consume two characters at once here (since we have an
   370  				// closing quote).
   371  				xp.Index = nextIndex
   372  
   373  				if len(xp.Key) == 0 {
   374  					// Quoted values, e.g. `""`, are allowed to be empty.
   375  					return ex.New(ErrXFCCParsing).WithMessage("Key missing")
   376  				}
   377  				err := xp.FillKeyValue()
   378  				if err != nil {
   379  					return err
   380  				}
   381  
   382  				xp.Key = make([]rune, 0, initialKeyCapacity)
   383  				xp.Value = make([]rune, 0, initialValueCapacity)
   384  				xp.State = parseXFCCKey
   385  				if xp.Header[nextIndex] == ',' {
   386  					xp.Parsed = append(xp.Parsed, xp.Element)
   387  					xp.Element = XFCCElement{}
   388  				}
   389  			} else {
   390  				return ex.New(ErrXFCCParsing).WithMessage("Closing quote not followed by `;`.")
   391  			}
   392  		} else {
   393  			// NOTE: If `nextIndex >= len(xp.Header)` then we are at the end,
   394  			//       which is a no-op here.
   395  			xp.State = parseXFCCKey
   396  		}
   397  	} else {
   398  		xp.Value = append(xp.Value, char)
   399  	}
   400  
   401  	return nil
   402  }
   403  
   404  // Finalize runs when the state machine has exhausted the `.Header`. It consumes
   405  // any remaining `.Key` or `.Value` slices and adds them to the return value
   406  // if need be.
   407  func (xp *xfccParser) Finalize() error {
   408  	if len(xp.Key) > 0 && len(xp.Value) > 0 {
   409  		err := xp.FillKeyValue()
   410  		if err != nil {
   411  			return err
   412  		}
   413  	} else if len(xp.Key) > 0 || len(xp.Value) > 0 {
   414  		return ex.New(ErrXFCCParsing).WithMessage("Key or value found but not both")
   415  	}
   416  
   417  	xp.Parsed = append(xp.Parsed, xp.Element)
   418  	return nil
   419  }