Skip to content

Commit 6b25c5c

Browse files
author
Pavel
committed
initial commit
0 parents  commit 6b25c5c

11 files changed

+919
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
proxy
2+
.idea

Dockerfile

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# syntax=docker/dockerfile:1
2+
3+
##
4+
## Build
5+
##
6+
FROM golang:1.18-buster AS build
7+
8+
WORKDIR /app
9+
10+
COPY go.mod ./
11+
COPY go.sum ./
12+
RUN go mod download
13+
14+
COPY *.go ./
15+
16+
RUN go build -o /proxy
17+
18+
##
19+
## Deploy
20+
##
21+
FROM gcr.io/distroless/base-debian10
22+
23+
WORKDIR /
24+
25+
COPY --from=build /proxy /proxy
26+
27+
EXPOSE 8080
28+
29+
USER nonroot:nonroot
30+
31+
ENTRYPOINT ["/proxy"]

GcloudKeyLoader.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
import (
4+
secretmanager "cloud.google.com/go/secretmanager/apiv1"
5+
"context"
6+
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
7+
"log"
8+
)
9+
10+
type GcloudKeyLoader struct {
11+
}
12+
13+
func (g GcloudKeyLoader) LoadKey(key string) ([]byte, error) {
14+
ctx := context.Background()
15+
client, err := secretmanager.NewClient(ctx)
16+
if err != nil {
17+
return []byte{}, err
18+
}
19+
defer func(client *secretmanager.Client) {
20+
err := client.Close()
21+
if err != nil {
22+
log.Fatal(err)
23+
}
24+
}(client)
25+
accessRequest := &secretmanagerpb.AccessSecretVersionRequest{
26+
Name: key,
27+
}
28+
result, err := client.AccessSecretVersion(ctx, accessRequest)
29+
if err != nil {
30+
return []byte{}, err
31+
}
32+
return result.Payload.Data, nil
33+
}

KeyLoader.go

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package main
2+
3+
type KeyLoader interface {
4+
LoadKey(key string) ([]byte, error)
5+
}

README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# TLS and eIDAS proxy server
2+
3+
Simple proxy server meant to be run in secure Google Cloud environment.
4+
5+
Server support two use-cases:
6+
1. TLS connection to any server using TLS certificates from Google Cloud Secret Manager
7+
8+
TLS connection is implemented using regular proxy server.
9+
You need to specify name of a public certificate and a private key in the following headers:
10+
`X-Cert`
11+
`X-Key`
12+
Those headers are removed from actual request
13+
You can also specify `X-Follow-Redirects` header to follow redirects
14+
15+
16+
2. Signing data with eIDAS private key
17+
In order to do that just send a request to `/sign` endpoint with your data in the body and `X-Key` header
18+
19+
Proxy is meant to be run in the secured environment, that is why no authentication is implemented.
20+
21+
This project is just an experiment and the way for me to learn Go. Some (all?) of the things might be done in a better way.

consts.go

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package main
2+
3+
const CertHeader = "X-Cert"
4+
const KeyHeader = "X-Key"
5+
const FollowRedirectsHeader = "X-Follow-Redirects"

go.mod

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
module proxy
2+
3+
go 1.17
4+
5+
require (
6+
cloud.google.com/go v0.100.2 // indirect
7+
cloud.google.com/go/compute v1.3.0 // indirect
8+
cloud.google.com/go/iam v0.1.0 // indirect
9+
cloud.google.com/go/secretmanager v1.3.0 // indirect
10+
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
11+
github.com/golang/protobuf v1.5.2 // indirect
12+
github.com/google/go-cmp v0.5.7 // indirect
13+
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
14+
go.opencensus.io v0.23.0 // indirect
15+
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
16+
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
17+
golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect
18+
golang.org/x/text v0.3.7 // indirect
19+
google.golang.org/api v0.70.0 // indirect
20+
google.golang.org/appengine v1.6.7 // indirect
21+
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6 // indirect
22+
google.golang.org/grpc v1.44.0 // indirect
23+
google.golang.org/protobuf v1.27.1 // indirect
24+
)

go.sum

+593
Large diffs are not rendered by default.

main.go

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package main
2+
3+
func main() {
4+
proxy := Proxy{
5+
GcloudKeyLoader{},
6+
}
7+
err := proxy.ListenAndServe()
8+
if err != nil {
9+
panic(err)
10+
}
11+
}

proxy.go

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package main
2+
3+
import (
4+
"crypto/tls"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"os"
9+
"strconv"
10+
)
11+
12+
type Proxy struct {
13+
KeyLoader KeyLoader
14+
}
15+
16+
func (p *Proxy) GetTLSConfig(certPath string, keyPath string) (*tls.Config, error) {
17+
if certPath == "" && keyPath == "" {
18+
return nil, nil
19+
}
20+
tlsConfig := &tls.Config{}
21+
cert, err := p.KeyLoader.LoadKey(certPath)
22+
if err != nil {
23+
return nil, err
24+
}
25+
key, err := p.KeyLoader.LoadKey(keyPath)
26+
if err != nil {
27+
return nil, err
28+
}
29+
certificate, err := tls.X509KeyPair(cert, key)
30+
if err != nil {
31+
return nil, err
32+
}
33+
tlsConfig.Certificates = []tls.Certificate{certificate}
34+
return tlsConfig, nil
35+
}
36+
37+
func (p *Proxy) ListenAndServe() error {
38+
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
39+
if r.URL.Scheme == "" {
40+
// This handler is expected to handle proxy requests.
41+
// It should not be used for the regular request.
42+
// Perhaps there is a better way to handle this.
43+
http.Error(w, "Bad request", http.StatusBadRequest)
44+
return
45+
}
46+
transport := http.Transport{}
47+
tlsConfig, err := p.GetTLSConfig(r.Header.Get(CertHeader), r.Header.Get(KeyHeader))
48+
if err != nil {
49+
http.Error(w, "Bad request", http.StatusBadRequest)
50+
return
51+
}
52+
r.Header.Del(CertHeader)
53+
r.Header.Del(KeyHeader)
54+
outReq := &http.Request{
55+
Method: r.Method,
56+
URL: r.URL,
57+
Header: r.Header,
58+
Body: r.Body,
59+
}
60+
61+
if tlsConfig != nil {
62+
transport.TLSClientConfig = tlsConfig
63+
}
64+
followRedirectsHeader := r.Header.Get(FollowRedirectsHeader)
65+
r.Header.Del(FollowRedirectsHeader)
66+
followRedirects, err := strconv.ParseBool(followRedirectsHeader)
67+
if err != nil {
68+
followRedirects = true
69+
}
70+
client := &http.Client{
71+
Transport: &transport,
72+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
73+
if followRedirects {
74+
return nil
75+
}
76+
return http.ErrUseLastResponse
77+
},
78+
}
79+
resp, err := client.Do(outReq)
80+
81+
if err != nil {
82+
http.Error(w, err.Error(), http.StatusBadGateway)
83+
return
84+
}
85+
defer func(Body io.ReadCloser) {
86+
err := Body.Close()
87+
if err != nil {
88+
fmt.Println(err)
89+
}
90+
}(resp.Body)
91+
for k, v := range resp.Header {
92+
for _, vv := range v {
93+
w.Header().Add(k, vv)
94+
}
95+
}
96+
w.WriteHeader(resp.StatusCode)
97+
_, err = io.Copy(w, resp.Body)
98+
if err != nil {
99+
fmt.Println("Error:", err)
100+
}
101+
})
102+
103+
http.HandleFunc("/sign", func(w http.ResponseWriter, r *http.Request) {
104+
privateKey, err := loadPrivateKey(r.Header.Get(KeyHeader))
105+
if err != nil {
106+
http.Error(w, err.Error(), http.StatusBadRequest)
107+
return
108+
}
109+
r.Header.Del(KeyHeader)
110+
body, err := io.ReadAll(r.Body)
111+
if err != nil {
112+
http.Error(w, err.Error(), http.StatusBadRequest)
113+
return
114+
}
115+
signature, err := sign(privateKey, body)
116+
if err != nil {
117+
http.Error(w, err.Error(), http.StatusBadRequest)
118+
return
119+
}
120+
_, err = w.Write([]byte(signature))
121+
if err != nil {
122+
http.Error(w, err.Error(), http.StatusBadRequest)
123+
return
124+
}
125+
})
126+
127+
port := os.Getenv("PORT")
128+
if port == "" {
129+
port = "8080"
130+
}
131+
return http.ListenAndServe(":"+port, nil)
132+
}

sign.go

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
import (
4+
"crypto"
5+
"crypto/rand"
6+
"crypto/rsa"
7+
"crypto/sha256"
8+
"crypto/x509"
9+
"encoding/base64"
10+
"encoding/pem"
11+
"errors"
12+
"fmt"
13+
"io/ioutil"
14+
)
15+
16+
func readKey(path string) ([]byte, error) {
17+
file, err := ioutil.ReadFile(path)
18+
if err != nil {
19+
return nil, err
20+
}
21+
return file, nil
22+
}
23+
24+
func loadPrivateKey(path string) (*rsa.PrivateKey, error) {
25+
keyContent, err := readKey(path)
26+
if err != nil {
27+
return nil, err
28+
}
29+
block, _ := pem.Decode(keyContent)
30+
if block == nil {
31+
return nil, errors.New("failed to parse PEM private key")
32+
}
33+
switch block.Type {
34+
case "RSA PRIVATE KEY":
35+
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
36+
if err != nil {
37+
return nil, err
38+
}
39+
return privateKey, nil
40+
case "PRIVATE KEY":
41+
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
42+
if err != nil {
43+
return nil, err
44+
}
45+
return privateKey.(*rsa.PrivateKey), nil
46+
default:
47+
fmt.Println("Unsupported key type:", block.Type)
48+
return nil, errors.New("failed to parse PEM private key")
49+
}
50+
}
51+
52+
func sign(privateKey *rsa.PrivateKey, data []byte) (string, error) {
53+
hash := sha256.New()
54+
hash.Write(data)
55+
hashed := hash.Sum(nil)
56+
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
57+
if err != nil {
58+
return "", err
59+
}
60+
encodedSignature := base64.RawURLEncoding.EncodeToString(signature)
61+
return encodedSignature, nil
62+
}

0 commit comments

Comments
 (0)