Skip to content

Commit

Permalink
CertExtractor filter (#474)
Browse files Browse the repository at this point in the history
* certExtractor filter

* remove unused import

* Apply suggestions from code review

Co-authored-by: Bomin Zhang <[email protected]>

* Update pkg/filter/certextractor/spec.go

Co-authored-by: Bomin Zhang <[email protected]>

* revert changes in HTTPContext

* make fmt

* remove DefaultHeaderKey

* add filter to registry

Co-authored-by: Bomin Zhang <[email protected]>
  • Loading branch information
Samu Tamminen and localvar authored Feb 10, 2022
1 parent d14d7fe commit 2c8b26a
Show file tree
Hide file tree
Showing 4 changed files with 406 additions and 2 deletions.
30 changes: 28 additions & 2 deletions doc/reference/filters.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
- [HeaderToJSON](#headertojson)
- [Configuration](#configuration-16)
- [Results](#results-16)
- [CertExtractor](#certextractor)
- [Configuration](#configuration-17)
- [Common Types](#common-types)
- [apiaggregator.Pipeline](#apiaggregatorpipeline)
- [pathadaptor.Spec](#pathadaptorspec)
Expand Down Expand Up @@ -734,8 +736,7 @@ headerMap:

| Name | Type | Description | Required |
| ------------ | -------- | -------------------------------- | -------- |
| headerMap | [][HeaderToJSON.HeaderMap](#headertojsonheadermap) | headerMap defines a map between HTTP header name and corresponding JSON filed name
| Yes |
| headerMap | [][HeaderToJSON.HeaderMap](#headertojsonheadermap) | headerMap defines a map between HTTP header name and corresponding JSON field name | Yes |


### Results
Expand All @@ -744,6 +745,31 @@ headerMap:
| ----------------------- | ------------------------------------ |
| jsonEncodeDecodeErr | Failed to convert HTTP headers to JSON. |

## CertExtractor

CertExtractor extracts a value from requests TLS certificates Subject or Issuer metadata (https://pkg.go.dev/crypto/x509/pkix#Name) and adds the value to headers. Request can contain zero or multiple certificates so the position (first, second, last, etc) of the certificate in the chain is required.

Here's an example configuration, that adds a new header `tls-cert-postalcode`, based on the PostalCode of the last TLS certificate's Subject:

```yaml
kind: "CertExtractor"
name: "postalcode-extractor"
certIndex: -1 # take last certificate in chain
target: "subject"
field: "PostalCode"
headerKey: "tls-cert-postalcode"
```

### Configuration

| Name | Type | Description | Required |
| ------------ | -------- | -------------------------------- | -------- |
| certIndex | int16 | The index of the certificate in the chain. Negative indexes from the end of the chain (-1 is the last index, -2 second last etc.) | Yes |
| target | string | Either `subject` or `issuer` of the [x509.Certificate](https://pkg.go.dev/crypto/x509#Certificate) | Yes |
| field | string | One of the string or string slice fields from https://pkg.go.dev/crypto/x509/pkix#Name | Yes |
| headerKey | string | Extracted value is added to this request header key. | Yes |


## Common Types

### apiaggregator.Pipeline
Expand Down
162 changes: 162 additions & 0 deletions pkg/filter/certextractor/certextractor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/*
* Copyright (c) 2017, MegaEase
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package certextractor

import (
"crypto/x509/pkix"
"fmt"

httpcontext "github.com/megaease/easegress/pkg/context"
"github.com/megaease/easegress/pkg/object/httppipeline"
)

const (
// Kind is the kind of CertExtractor.
Kind = "CertExtractor"
)

var results = []string{}

func init() {
httppipeline.Register(&CertExtractor{})
}

type (
// CertExtractor extracts given field from TLS certificates and sets it to request headers.
CertExtractor struct {
filterSpec *httppipeline.FilterSpec
spec *Spec

headerKey string
}

// Spec describes the CertExtractor.
Spec struct {
CertIndex int16 `yaml:"certIndex" jsonschema:"required"`
Target string `yaml:"target" jsonschema:"required,enum=subject,enum=issuer"`
// Different field options listed here https://pkg.go.dev/crypto/x509/pkix#Name
Field string `yaml:"field" jsonschema:"required,enum=Country,enum=Organization,enum=OrganizationalUnit,enum=Locality,enum=Province,enum=StreetAddress,enum=PostalCode,enum=SerialNumber,enum=CommonName"`
HeaderKey string `yaml:"headerKey" jsonschema:"required"`
}
)

// Validate is dummy as yaml rules already validate Spec.
func (spec *Spec) Validate() error { return nil }

// Kind returns the kind of CertExtractor.
func (ce *CertExtractor) Kind() string {
return Kind
}

// DefaultSpec returns the default spec of CertExtractor.
func (ce *CertExtractor) DefaultSpec() interface{} {
return &Spec{}
}

// Description returns the description of CertExtractor.
func (ce *CertExtractor) Description() string {
return "CertExtractor extracts given field from TLS certificates and sets it to request headers."
}

// Results returns the results of CertExtractor.
func (ce *CertExtractor) Results() []string {
return results
}

// Init initializes CertExtractor.
func (ce *CertExtractor) Init(filterSpec *httppipeline.FilterSpec) {
ce.filterSpec, ce.spec = filterSpec, filterSpec.FilterSpec().(*Spec)

ce.headerKey = fmt.Sprintf("tls-%s-%s", ce.spec.Target, ce.spec.Field)
if ce.spec.HeaderKey != "" {
ce.headerKey = ce.spec.HeaderKey
}
}

// Inherit inherits previous generation of CertExtractor.
func (ce *CertExtractor) Inherit(filterSpec *httppipeline.FilterSpec, previousGeneration httppipeline.Filter) {
previousGeneration.Close()
ce.Init(filterSpec)
}

// Close closes CertExtractor.
func (ce *CertExtractor) Close() {}

// Handle retrieves header values and sets request headers.
func (ce *CertExtractor) Handle(ctx httpcontext.HTTPContext) string {
result := ce.handle(ctx)
return ctx.CallNextHandler(result)
}

// CertExtractor extracts given field from TLS certificates and sets it to request headers.
func (ce *CertExtractor) handle(ctx httpcontext.HTTPContext) string {
r := ctx.Request()
connectionState := r.Std().TLS
if connectionState == nil {
return ""
}

certs := connectionState.PeerCertificates
if certs == nil || len(certs) < 1 {
return ""
}

n := int16(len(certs))
// positive ce.spec.CertIndex from the beginning, negative from the end
relativeIndex := ce.spec.CertIndex % n
index := (n + relativeIndex) % n
cert := certs[index]

var target pkix.Name
if ce.spec.Target == "subject" {
target = cert.Subject
} else {
target = cert.Issuer
}

var result []string
switch ce.spec.Field {
case "Country":
result = target.Country
case "Organization":
result = target.Organization
case "OrganizationalUnit":
result = target.OrganizationalUnit
case "Locality":
result = target.Locality
case "Province":
result = target.Province
case "StreetAddress":
result = target.StreetAddress
case "PostalCode":
result = target.PostalCode
case "SerialNumber":
result = append(result, target.SerialNumber)
case "CommonName":
result = append(result, target.CommonName)
}
for _, res := range result {
if res != "" {
r.Header().Add(ce.headerKey, res)
}
}
return ""
}

// Status returns status.
func (ce *CertExtractor) Status() interface{} { return nil }
Loading

0 comments on commit 2c8b26a

Please sign in to comment.