-
Notifications
You must be signed in to change notification settings - Fork 30
/
Copy pathsan_pinning.go
158 lines (136 loc) · 4.28 KB
/
san_pinning.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
package security
import (
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"github.com/go-logr/logr"
"github.com/kyma-project/lifecycle-manager/api/v1beta2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/kyma-project/lifecycle-manager/pkg/log"
"github.com/kyma-project/runtime-watcher/listener/pkg/types"
)
const (
XFCCHeader = "X-Forwarded-Client-Cert"
headerValueSeparator = ";"
keyValueSeparator = "="
certificateKey = "Cert="
shootDomainKey = "skr-domain"
)
var (
// Static errors.
errNotVerified = errors.New("SAN from certificate does not match domain specified in KymaCR")
errPemDecode = errors.New("failed to decode PEM block")
errEmptyCert = errors.New("empty certificate")
errHeaderMissing = fmt.Errorf("request does not contain '%s' header", XFCCHeader)
)
type RequestVerifier struct {
Client client.Client
Log logr.Logger
}
func NewRequestVerifier(client client.Client) *RequestVerifier {
return &RequestVerifier{
Client: client,
Log: ctrl.Log.WithName("request–verifier"),
}
}
// Verify verifies the given request by fetching the KymaCR given in the request payload
// and comparing the SAN(subject alternative name) of the certificate with the SKR-domain of the KymaCR.
// If the request can be verified 'nil' will be returned.
func (v *RequestVerifier) Verify(request *http.Request, watcherEvtObject *types.WatchEvent) error {
certificate, err := v.getCertificateFromHeader(request)
if err != nil {
return err
}
domain, err := v.getDomain(request, watcherEvtObject)
if err != nil {
return err
}
if v.VerifySAN(certificate, domain) {
return nil
}
return errNotVerified
}
// getCertificateFromHeader extracts the XFCC header and pareses it into a valid x509 certificate.
func (v *RequestVerifier) getCertificateFromHeader(r *http.Request) (*x509.Certificate, error) {
// Fetch XFCC-Header data
xfccValue, ok := r.Header[XFCCHeader]
if !ok {
return nil, errHeaderMissing
}
xfccData := strings.Split(xfccValue[0], headerValueSeparator)
// Extract raw certificate
var cert string
for _, keyValuePair := range xfccData {
if strings.Contains(keyValuePair, certificateKey) {
cert = strings.Split(keyValuePair, keyValueSeparator)[1]
break
}
}
if cert == "" {
return nil, errEmptyCert
}
// Decode URL-format
decodedValue, err := url.QueryUnescape(cert)
if err != nil {
return nil, fmt.Errorf("could not decode certificate URL format: %w", err)
}
decodedValue = strings.Trim(decodedValue, "\"")
// Decode PEM block and parse certificate
block, _ := pem.Decode([]byte(decodedValue))
if block == nil {
return nil, errPemDecode
}
certificate, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse PEM block into x509 certificate: %w", err)
}
return certificate, nil
}
// getDomain fetches the KymaCR, mentioned in the requests body, and returns the value of the SKR-Domain annotation.
func (v *RequestVerifier) getDomain(request *http.Request, watcherEvtObject *types.WatchEvent) (string, error) {
var kymaCR v1beta2.Kyma
if err := v.Client.Get(request.Context(), watcherEvtObject.Owner, &kymaCR); err != nil {
return "", err
}
domain, ok := kymaCR.Annotations[shootDomainKey]
if !ok {
return "", AnnotationMissingError{
KymaCR: watcherEvtObject.Owner.String(),
Annotation: shootDomainKey,
}
}
return domain, nil
}
// VerifySAN checks if given domain exists in the SAN information of the given certificate.
func (v *RequestVerifier) VerifySAN(certificate *x509.Certificate, kymaDomain string) bool {
if contains(certificate.URIs, kymaDomain) ||
contains(certificate.DNSNames, kymaDomain) ||
contains(certificate.IPAddresses, kymaDomain) {
v.Log.V(log.DebugLevel).Info("Received request verified")
return true
}
return false
}
// contains checks if given string is present in slice.
func contains[E net.IP | *url.URL | string](arr []E, s string) bool {
for i := range arr {
a := fmt.Sprintf("%s", arr[i])
if a == s {
return true
}
}
return false
}
type AnnotationMissingError struct {
KymaCR string
Annotation string
}
func (e AnnotationMissingError) Error() string {
return fmt.Sprintf("KymaCR '%s' does not have annotation `%s`", e.KymaCR, e.Annotation)
}