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 }