Skip to content

Commit 014eb4a

Browse files
authored
Merge branch 'main' into dependabot/go_modules/runtime-watcher/github.com/kyma-project/runtime-watcher/listener-1.2.0
2 parents 8b2059f + 8645b18 commit 014eb4a

14 files changed

+339
-24
lines changed

.github/workflows/build-image.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
tags: ${{ steps.get_custom_tags.outputs.tags }}
2626
steps:
2727
- name: Checkout
28-
uses: actions/checkout@v5
28+
uses: actions/checkout@v6
2929
- name: Get tags
3030
id: get_custom_tags
3131
run: |

.github/workflows/create-listener-release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- name: Checkout
18-
uses: actions/checkout@v5
18+
uses: actions/checkout@v6
1919
with:
2020
fetch-depth: 0
2121
- uses: actions/setup-go@v6

.github/workflows/create-runtime-watcher-release.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ jobs:
2626
runs-on: ubuntu-latest
2727
steps:
2828
- name: Checkout code
29-
uses: actions/checkout@v5
29+
uses: actions/checkout@v6
3030
with:
3131
fetch-depth: 0
3232
- name: Validate the release tag
@@ -40,7 +40,7 @@ jobs:
4040
needs: validate-release
4141
steps:
4242
- name: Checkout code
43-
uses: actions/checkout@v5
43+
uses: actions/checkout@v6
4444
with:
4545
fetch-depth: 0
4646
- name: Generate changelog
@@ -72,7 +72,7 @@ jobs:
7272
runs-on: ubuntu-latest
7373
steps:
7474
- name: Checkout code
75-
uses: actions/checkout@v5
75+
uses: actions/checkout@v6
7676
with:
7777
fetch-depth: 0
7878
- name: Wait for the Docker image

.github/workflows/lint-markdown-links.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
markdown-link-check:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v5
16+
- uses: actions/checkout@v6
1717
- uses: gaurav-nelson/github-action-markdown-link-check@3c3b66f1f7d0900e37b71eca45b63ea9eedfce31
1818
with:
1919
use-quiet-mode: 'yes'

.github/workflows/pull-listener-pkg.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
lint-build-test-listener-pkg:
99
runs-on: ubuntu-latest
1010
steps:
11-
- uses: actions/checkout@v5
11+
- uses: actions/checkout@v6
1212
- uses: actions/setup-go@v6
1313
with:
1414
go-version-file: "listener/go.mod"

.github/workflows/pull-runtime-watcher.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
lint-build-test-runtime-watcher:
99
runs-on: ubuntu-latest
1010
steps:
11-
- uses: actions/checkout@v5
11+
- uses: actions/checkout@v6
1212
- uses: actions/setup-go@v6
1313
with:
1414
go-version-file: "runtime-watcher/go.mod"

.github/workflows/report-sprint-commits.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-latest
1616
steps:
1717
- name: Check out report scripts
18-
uses: actions/checkout@v5
18+
uses: actions/checkout@v6
1919
with:
2020
repository: kyma-project/qa-toolkit
2121
path: scripts

.github/workflows/test-e2e-runtime-watcher.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ jobs:
6565
runs-on: ubuntu-latest
6666
steps:
6767
- name: Checkout code
68-
uses: actions/checkout@v5
68+
uses: actions/checkout@v6
6969
with:
7070
fetch-depth: 0
7171
- name: Wait for the Docker image
@@ -89,10 +89,10 @@ jobs:
8989
timeout-minutes: 10
9090
steps:
9191
- name: Checkout runtime-watcher
92-
uses: actions/checkout@v5
92+
uses: actions/checkout@v6
9393

9494
- name: Checkout lifecycle-manager
95-
uses: actions/checkout@v5
95+
uses: actions/checkout@v6
9696
with:
9797
repository: kyma-project/lifecycle-manager
9898
path: lifecycle-manager
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package certificate
2+
3+
import (
4+
"crypto/x509"
5+
"encoding/pem"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"net/url"
10+
"strings"
11+
)
12+
13+
const (
14+
XFCCHeader = "X-Forwarded-Client-Cert"
15+
CertificateKey = "Cert="
16+
Limit32KiB = 32 * 1024
17+
)
18+
19+
var (
20+
ErrPemDecode = errors.New("failed to decode PEM block")
21+
ErrEmptyCert = errors.New("empty certificate")
22+
ErrHeaderValueTooLong = errors.New(XFCCHeader + " header value too long (over 32KiB)")
23+
ErrHeaderMissing = fmt.Errorf("request does not contain '%s' header", XFCCHeader)
24+
)
25+
26+
// GetCertificateFromHeader extracts the XFCC header and pareses it into a valid x509 certificate.
27+
func GetCertificateFromHeader(r *http.Request) (*x509.Certificate, error) {
28+
// Fetch XFCC-Header data
29+
xfccValues, ok := r.Header[XFCCHeader]
30+
if !ok {
31+
return nil, ErrHeaderMissing
32+
}
33+
34+
xfccVal := xfccValues[0]
35+
36+
// Limit the length of the data (prevent resource exhaustion attack)
37+
if len(xfccVal) > Limit32KiB {
38+
return nil, ErrHeaderValueTooLong
39+
}
40+
41+
// Extract raw certificate from the first header value
42+
cert := getCertTokenFromXFCCHeader(xfccVal)
43+
if cert == "" {
44+
return nil, ErrEmptyCert
45+
}
46+
47+
// Decode URL-format
48+
decodedValue, err := url.QueryUnescape(cert)
49+
if err != nil {
50+
return nil, fmt.Errorf("could not decode certificate URL format: %w", err)
51+
}
52+
decodedValue = strings.Trim(decodedValue, "\"")
53+
54+
// Decode PEM block and parse certificate
55+
block, _ := pem.Decode([]byte(decodedValue))
56+
if block == nil {
57+
return nil, ErrPemDecode
58+
}
59+
certificate, err := x509.ParseCertificate(block.Bytes)
60+
if err != nil {
61+
return nil, fmt.Errorf("failed to parse PEM block into x509 certificate: %w", err)
62+
}
63+
64+
return certificate, nil
65+
}
66+
67+
// getCertTokenFromXFCCHeader returns the first certificate embedded in the XFFC Header,
68+
// if exists. Otherwise an empty string is returned.
69+
func getCertTokenFromXFCCHeader(hVal string) string {
70+
certStartIdx := strings.Index(hVal, CertificateKey)
71+
if certStartIdx >= 0 {
72+
tokenWithCert := hVal[(certStartIdx + len(CertificateKey)):]
73+
// we shouldn't have "," here but it's safer to add it anyway
74+
certEndIdx := strings.IndexAny(tokenWithCert, ";,")
75+
if certEndIdx == -1 {
76+
// no suffix, the entire token is the cert value
77+
return tokenWithCert
78+
}
79+
80+
// there's some data after the cert value, return just the cert part
81+
return tokenWithCert[:certEndIdx]
82+
}
83+
return ""
84+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package certificate_test
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/url"
7+
"testing"
8+
9+
"github.com/kyma-project/runtime-watcher/listener/pkg/v2/certificate"
10+
"github.com/kyma-project/runtime-watcher/listener/pkg/v2/certificate/utils"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestGetCertificateFromHeader_Success(t *testing.T) {
15+
pemCert, err := utils.NewPemCertificateBuilder().Build()
16+
require.NoError(t, err)
17+
r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", nil)
18+
r.Header.Set(certificate.XFCCHeader, certificate.CertificateKey+pemCert)
19+
cert, err := certificate.GetCertificateFromHeader(r)
20+
require.NoError(t, err)
21+
require.NotNil(t, cert)
22+
}
23+
24+
func TestGetCertificateFromHeader_MissingHeader(t *testing.T) {
25+
r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", nil)
26+
cert, err := certificate.GetCertificateFromHeader(r)
27+
require.Error(t, err)
28+
require.Nil(t, cert)
29+
require.Equal(t, certificate.ErrHeaderMissing, err)
30+
}
31+
32+
func TestGetCertificateFromHeader_TooLong(t *testing.T) {
33+
longValue := make([]byte, certificate.Limit32KiB+1)
34+
for i := range longValue {
35+
longValue[i] = 'A'
36+
}
37+
r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", nil)
38+
r.Header.Set(certificate.XFCCHeader, certificate.CertificateKey+string(longValue))
39+
cert, err := certificate.GetCertificateFromHeader(r)
40+
require.Error(t, err)
41+
require.Nil(t, cert)
42+
require.Equal(t, certificate.ErrHeaderValueTooLong, err)
43+
}
44+
45+
func TestGetCertificateFromHeader_EmptyCert(t *testing.T) {
46+
r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", nil)
47+
r.Header.Set(certificate.XFCCHeader, "Cert=;Other=foo")
48+
cert, err := certificate.GetCertificateFromHeader(r)
49+
require.Error(t, err)
50+
require.Nil(t, cert)
51+
require.Equal(t, certificate.ErrEmptyCert, err)
52+
}
53+
54+
func TestGetCertificateFromHeader_NoCertToken(t *testing.T) {
55+
r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", nil)
56+
r.Header.Set(certificate.XFCCHeader, "Other=foo;Stuff=bar")
57+
cert, err := certificate.GetCertificateFromHeader(r)
58+
require.Error(t, err)
59+
require.Nil(t, cert)
60+
require.Equal(t, certificate.ErrEmptyCert, err)
61+
}
62+
63+
func TestGetCertificateFromHeader_InvalidURLFormat(t *testing.T) {
64+
r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", nil)
65+
r.Header.Set(certificate.XFCCHeader, "Cert=%ZZ;Other=foo")
66+
cert, err := certificate.GetCertificateFromHeader(r)
67+
require.Error(t, err)
68+
require.Nil(t, cert)
69+
require.Contains(t, err.Error(), "could not decode certificate URL format")
70+
}
71+
72+
func TestGetCertificateFromHeader_PEMDecodeError(t *testing.T) {
73+
invalid := url.QueryEscape("not a pem block")
74+
r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", nil)
75+
r.Header.Set(certificate.XFCCHeader, certificate.CertificateKey+invalid)
76+
cert, err := certificate.GetCertificateFromHeader(r)
77+
require.Error(t, err)
78+
require.Nil(t, cert)
79+
require.Equal(t, certificate.ErrPemDecode, err)
80+
}
81+
82+
func TestGetCertificateFromHeader_CertificateParseError(t *testing.T) {
83+
pemInvalid := "-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----"
84+
escaped := url.QueryEscape(pemInvalid)
85+
r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", nil)
86+
r.Header.Set(certificate.XFCCHeader, certificate.CertificateKey+escaped)
87+
cert, err := certificate.GetCertificateFromHeader(r)
88+
require.Error(t, err)
89+
require.Nil(t, cert)
90+
require.Contains(t, err.Error(), "failed to parse PEM block into x509 certificate")
91+
}
92+
93+
func TestGetCertificateFromHeader_MultipleValuesFirstHasCert(t *testing.T) {
94+
pemCert, err := utils.NewPemCertificateBuilder().Build()
95+
require.NoError(t, err)
96+
r, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost", nil)
97+
r.Header[certificate.XFCCHeader] = []string{certificate.CertificateKey + pemCert + ";Other=foo", "Cert=ignored"}
98+
cert, err := certificate.GetCertificateFromHeader(r)
99+
require.NoError(t, err)
100+
require.NotNil(t, cert)
101+
}

0 commit comments

Comments
 (0)