Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CertExtractor filter #474

Merged
merged 8 commits into from
Feb 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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