Skip to content

Commit 34c212f

Browse files
authored
feat: add CRL support (#22)
Currently, octo-proxy lacks the capability to verify the peer certificate against the CRL. This PR introduces CRL validation during the TLS handshake. This CRL validation will only occur if the user enables the crl configuration by defining the CRL path in the tls field. This validation can be used in both server and client. Issue: #19
1 parent 0d2ae3b commit 34c212f

22 files changed

+563
-160
lines changed

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ jobs:
1919
- name: Set up Go
2020
uses: actions/setup-go@v2
2121
with:
22-
go-version: 1.17
22+
go-version: 1.21
2323
- name: Run GoReleaser
2424
uses: goreleaser/goreleaser-action@v2
2525
with:

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
- name: Set up Go
1313
uses: actions/setup-go@v2
1414
with:
15-
go-version: 1.17
15+
go-version: 1.21
1616

1717
- name: Check out code
1818
uses: actions/checkout@v2

docs/CONFIGURATION.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
| cert | `<string>` | The path to the Certificate file, this option need to be set if the mode is `mutual` to authenticate/verify the server/client. | yes |
3737
| key | `<string>` | The path to the Private key file, this option need to be set if the mode is `mutual`. | yes |
3838
| sni | `<string>` | Set SNI during TLS handshake | no |
39+
| crl | `<string>` | The path to CRL file, If the CRL is configured, the server/client will verify the peer's certificate against the CRL | no |
3940

4041
## tlsMode
4142
| Field | Type | Description |
@@ -46,6 +47,6 @@
4647
> Currently, in mutual mode octo-proxy will only verify the ip address of it's client and try to match it with ip sans in certificate. In the future we will adding more alternative names verification.
4748
4849
## Metrics
49-
| Field | Type | Description | | Required |
50-
| -------- | ------------- | ------------------------------- | ----------------------------------------------- | -------- |
50+
| Field | Type | Description | Required |
51+
| -------- | ------------- | ------------------------------- | -------- |
5152
| metrics | [HostConfig] | Configures the host and port for the metrics server, currently doesn't support tls settings | no |

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module github.com/nothinux/octo-proxy
22

3-
go 1.17
3+
go 1.21
44

55
require (
66
github.com/kavu/go_reuseport v1.5.0

pkg/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ type TLSConfig struct {
5656
Cert string `yaml:"cert"`
5757
Key string `yaml:"key"`
5858
SNI string `yaml:"sni"`
59+
CRL string `yaml:"crl"`
5960
Mode string `yaml:"mode"`
6061
SubjectAltNames []string `yaml:"subjectAltNames"`
6162
SubjectAltName

pkg/proxy/helper.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
package proxy
22

33
import (
4+
"crypto/tls"
5+
"crypto/x509"
6+
"encoding/pem"
47
"net"
8+
"os"
59

610
goerrors "errors"
711

812
"github.com/nothinux/octo-proxy/pkg/config"
13+
"github.com/nothinux/octo-proxy/pkg/errors"
914
"github.com/prometheus/client_golang/prometheus"
1015
)
1116

@@ -25,3 +30,64 @@ func errCopy(err error, tConf config.HostConfig) error {
2530
func timeoutIsZero(c config.HostConfig) bool {
2631
return c.ConnectionConfig.TimeoutDuration == 0
2732
}
33+
34+
func getCACertPool(c config.TLSConfig) (*x509.CertPool, error) {
35+
cacert, err := os.ReadFile(c.CaCert)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
pool := x509.NewCertPool()
41+
if ok := pool.AppendCertsFromPEM(cacert); !ok {
42+
return nil, errors.New("tlsConfig", "can't add CA to pool")
43+
}
44+
45+
return pool, nil
46+
}
47+
48+
func getCACertificate(c config.TLSConfig) (*x509.Certificate, error) {
49+
cacert, err := os.ReadFile(c.CaCert)
50+
if err != nil {
51+
return nil, err
52+
}
53+
54+
b, _ := pem.Decode(cacert)
55+
if b == nil {
56+
return nil, goerrors.New("error: can't decode CA file")
57+
}
58+
59+
return x509.ParseCertificate(b.Bytes)
60+
}
61+
62+
func getCACRL(c config.TLSConfig) (*x509.RevocationList, error) {
63+
rawCRL, err := os.ReadFile(c.CRL)
64+
if err != nil {
65+
return nil, err
66+
}
67+
68+
b, _ := pem.Decode(rawCRL)
69+
if b == nil {
70+
return nil, goerrors.New("error: can't decode CRL file")
71+
}
72+
73+
return x509.ParseRevocationList(b.Bytes)
74+
}
75+
76+
func getCertKeyPair(c config.TLSConfig) (tls.Certificate, error) {
77+
pcert, err := os.ReadFile(c.Cert)
78+
if err != nil {
79+
return tls.Certificate{}, err
80+
}
81+
82+
pkey, err := os.ReadFile(c.Key)
83+
if err != nil {
84+
return tls.Certificate{}, err
85+
}
86+
87+
cert, err := tls.X509KeyPair(pcert, pkey)
88+
if err != nil {
89+
return tls.Certificate{}, errors.New("tlsConfig", "can't parse public & private key pair "+err.Error())
90+
}
91+
92+
return cert, nil
93+
}

pkg/proxy/helper_test.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package proxy
22

33
import (
4+
"errors"
5+
"strings"
46
"testing"
57
"time"
68

@@ -43,3 +45,171 @@ func TestTimeoutIsZero(t *testing.T) {
4345
})
4446
}
4547
}
48+
49+
func TestGetCACertPool(t *testing.T) {
50+
tests := []struct {
51+
Name string
52+
Config config.TLSConfig
53+
wantError error
54+
}{
55+
{
56+
Name: "Check ca-cert file",
57+
Config: config.TLSConfig{CaCert: "../testdata/ca-cert.pem"},
58+
wantError: nil,
59+
},
60+
{
61+
Name: "Check if ca-cert file not found",
62+
Config: config.TLSConfig{CaCert: "/tmp/zzz"},
63+
wantError: errors.New("no such file or directory"),
64+
},
65+
{
66+
Name: "Check not ca-cert file",
67+
Config: config.TLSConfig{CaCert: "../testdata/file"},
68+
wantError: errors.New("can't add CA to pool"),
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.Name, func(t *testing.T) {
74+
_, err := getCACertPool(tt.Config)
75+
if err != tt.wantError {
76+
if !strings.Contains(err.Error(), tt.wantError.Error()) {
77+
t.Fatalf("got %v, want %v", err, tt.wantError)
78+
}
79+
}
80+
})
81+
}
82+
}
83+
84+
func TestGetCertKeyPair(t *testing.T) {
85+
tests := []struct {
86+
Name string
87+
Config config.TLSConfig
88+
wantError error
89+
}{
90+
{
91+
Name: "Check if cert file not found",
92+
Config: config.TLSConfig{
93+
Cert: "../testdata/zzzz",
94+
},
95+
wantError: errors.New("no such file or directory"),
96+
},
97+
{
98+
Name: "Check if key file not found",
99+
Config: config.TLSConfig{
100+
Cert: "../testdata/cert.pem",
101+
Key: "../testdata/zzzz",
102+
},
103+
wantError: errors.New("no such file or directory"),
104+
},
105+
{
106+
Name: "Check if cert and key is not match",
107+
Config: config.TLSConfig{
108+
Cert: "../testdata/cert.pem",
109+
Key: "../testdata/ca-key.pem",
110+
},
111+
wantError: errors.New("can't parse public & private key pair tls: private key does not match public key"),
112+
},
113+
{
114+
Name: "Check if cert and key is match",
115+
Config: config.TLSConfig{
116+
Cert: "../testdata/cert.pem",
117+
Key: "../testdata/cert-key.pem",
118+
},
119+
wantError: nil,
120+
},
121+
}
122+
123+
for _, tt := range tests {
124+
t.Run(tt.Name, func(t *testing.T) {
125+
_, err := getCertKeyPair(tt.Config)
126+
if err != tt.wantError {
127+
if !strings.Contains(err.Error(), tt.wantError.Error()) {
128+
t.Fatalf("got %v, want %v", err, tt.wantError)
129+
}
130+
}
131+
})
132+
}
133+
}
134+
135+
func TestGetCACertificate(t *testing.T) {
136+
tests := []struct {
137+
Name string
138+
Config config.TLSConfig
139+
wantError error
140+
}{
141+
{
142+
Name: "Test if cert file not found",
143+
Config: config.TLSConfig{
144+
CaCert: "../testdata/zzzz",
145+
},
146+
wantError: errors.New("no such file or directory"),
147+
},
148+
{
149+
Name: "Test invalid certificate file",
150+
Config: config.TLSConfig{
151+
CaCert: "../testdata/file",
152+
},
153+
wantError: errors.New("can't decode CA file"),
154+
},
155+
{
156+
Name: "Test valid certificate file",
157+
Config: config.TLSConfig{
158+
CaCert: "../testdata/ca-cert.pem",
159+
},
160+
wantError: nil,
161+
},
162+
}
163+
164+
for _, tt := range tests {
165+
t.Run(tt.Name, func(t *testing.T) {
166+
_, err := getCACertificate(tt.Config)
167+
if err != tt.wantError {
168+
if !strings.Contains(err.Error(), tt.wantError.Error()) {
169+
t.Fatalf("got %v, want %v", err, tt.wantError)
170+
}
171+
}
172+
})
173+
}
174+
}
175+
176+
func TestGetCACRL(t *testing.T) {
177+
tests := []struct {
178+
Name string
179+
Config config.TLSConfig
180+
wantError error
181+
}{
182+
{
183+
Name: "Test if cert file not found",
184+
Config: config.TLSConfig{
185+
CRL: "../testdata/zzzz",
186+
},
187+
wantError: errors.New("no such file or directory"),
188+
},
189+
{
190+
Name: "Test invalid certificate file",
191+
Config: config.TLSConfig{
192+
CRL: "../testdata/file",
193+
},
194+
wantError: errors.New("can't decode CRL file"),
195+
},
196+
{
197+
Name: "Test valid certificate file",
198+
Config: config.TLSConfig{
199+
CRL: "../testdata/ca-crl.pem",
200+
},
201+
wantError: nil,
202+
},
203+
}
204+
205+
for _, tt := range tests {
206+
t.Run(tt.Name, func(t *testing.T) {
207+
_, err := getCACRL(tt.Config)
208+
if err != tt.wantError {
209+
if !strings.Contains(err.Error(), tt.wantError.Error()) {
210+
t.Fatalf("got %v, want %v", err, tt.wantError)
211+
}
212+
}
213+
})
214+
}
215+
}

0 commit comments

Comments
 (0)