blob: 6d8f17312b6771b36264e5a9762be6c95bfcd0c1 [file] [log] [blame]
Serge Bazanski814749f2018-10-25 12:01:10 +01001package pki
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +01002
3// Copyright 2018 Sergiusz Bazanski <q3k@hackerspace.pl>
4//
5// Permission to use, copy, modify, and/or distribute this software for any
6// purpose with or without fee is hereby granted, provided that the above
7// copyright notice and this permission notice appear in all copies.
8//
9// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
15// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
17import (
18 "context"
19 "crypto/tls"
20 "crypto/x509"
21 "flag"
22 "fmt"
23 "io/ioutil"
24 "strings"
25
26 "github.com/golang/glog"
27 "golang.org/x/net/trace"
28 "google.golang.org/grpc"
29 "google.golang.org/grpc/codes"
30 "google.golang.org/grpc/credentials"
31 "google.golang.org/grpc/peer"
32 "google.golang.org/grpc/status"
33)
34
35var (
36 flagCAPath string
37 flagCertificatePath string
38 flagKeyPath string
39 flagPKIRealm string
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +020040 flagPKIDisable bool
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +010041
42 // Enable logging HSPKI info into traces
43 Trace = true
44 // Enable logging HSPKI info into glog
45 Log = false
46)
47
48const (
49 ctxKeyClientInfo = "hspki-client-info"
50)
51
52func init() {
53 flag.StringVar(&flagCAPath, "hspki_tls_ca_path", "pki/ca.pem", "Path to PKI CA certificate")
54 flag.StringVar(&flagCertificatePath, "hspki_tls_certificate_path", "pki/service.pem", "Path to PKI service certificate")
55 flag.StringVar(&flagKeyPath, "hspki_tls_key_path", "pki/service-key.pem", "Path to PKI service private key")
56 flag.StringVar(&flagPKIRealm, "hspki_realm", "svc.cluster.local", "PKI realm")
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +020057 flag.BoolVar(&flagPKIDisable, "hspki_disable", false, "Disable PKI entirely (insecure!)")
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +010058}
59
60func maybeTrace(ctx context.Context, f string, args ...interface{}) {
61 if Log {
62 glog.Infof(f, args...)
63 }
64
65 if !Trace {
66 return
67 }
68
69 tr, ok := trace.FromContext(ctx)
70 if !ok {
71 if !Log {
72 fmtd := fmt.Sprintf(f, args...)
73 glog.Info("[no trace] %v", fmtd)
74 }
75 return
76 }
77 tr.LazyPrintf(f, args...)
78}
79
80func parseClientName(name string) (*ClientInfo, error) {
81 if !strings.HasSuffix(name, "."+flagPKIRealm) {
82 return nil, fmt.Errorf("invalid realm")
83 }
84 service := strings.TrimSuffix(name, "."+flagPKIRealm)
85 parts := strings.Split(service, ".")
86 if len(parts) != 2 {
87 return nil, fmt.Errorf("invalid job/principal format")
88 }
89 return &ClientInfo{
90 Realm: flagPKIRealm,
91 Principal: parts[1],
92 Job: parts[0],
93 }, nil
94}
95
96func withPKIInfo(ctx context.Context, c *ClientInfo) context.Context {
97 maybeTrace(ctx, "HSPKI: Applying ClientInfo: %s", c.String())
98 return context.WithValue(ctx, ctxKeyClientInfo, c)
99}
100
101func grpcInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
102 peer, ok := peer.FromContext(ctx)
103 if !ok {
104 maybeTrace(ctx, "HSPKI: Could not establish identity of peer.")
105 return nil, status.Errorf(codes.PermissionDenied, "no peer info")
106 }
107
108 authInfo, ok := peer.AuthInfo.(credentials.TLSInfo)
109 if !ok {
110 maybeTrace(ctx, "HSPKI: Could not establish TLS identity of peer.")
111 return nil, status.Errorf(codes.PermissionDenied, "no TLS certificate presented")
112 }
113
114 chains := authInfo.State.VerifiedChains
115 if len(chains) != 1 {
116 maybeTrace(ctx, "HSPKI: No trusted chains found.")
117 return nil, status.Errorf(codes.PermissionDenied, "no trusted TLS certificate presented")
118 }
119
120 chain := chains[0]
121
122 certDNs := make([]string, len(chain))
123 for i, cert := range chain {
124 certDNs[i] = cert.Subject.String()
125 }
126 maybeTrace(ctx, "HSPKI: Trust chain: %s", strings.Join(certDNs, ", "))
127
128 clientInfo, err := parseClientName(chain[0].Subject.CommonName)
129 if err != nil {
130 maybeTrace(ctx, "HSPKI: Invalid CN %q: %v", chain[0].Subject.CommonName, err)
131 return nil, status.Errorf(codes.PermissionDenied, "invalid TLS CN format")
132 }
133 ctx = withPKIInfo(ctx, clientInfo)
134 return handler(ctx, req)
135}
136
137// ClientInfo contains information about the HSPKI authentication data of the
138// gRPC client that has made the request.
139type ClientInfo struct {
140 Realm string
141 Principal string
142 Job string
143}
144
145// String returns a human-readable representation of the ClientInfo in the
146// form "job=foo, principal=bar, realm=baz".
147func (c *ClientInfo) String() string {
148 return fmt.Sprintf("job=%q, principal=%q, realm=%q", c.Job, c.Principal, c.Realm)
149}
150
151// ClientInfoFromContext returns ClientInfo from a gRPC service context.
152func ClientInfoFromContext(ctx context.Context) *ClientInfo {
153 v := ctx.Value(ctxKeyClientInfo)
154 if v == nil {
155 return nil
156 }
157 ci, ok := v.(*ClientInfo)
158 if !ok {
159 return nil
160 }
161 return ci
162}
163
164// WithServerHSPKI is a grpc.ServerOptions array that ensures that the gRPC server:
165// - runs with HSPKI TLS Service Certificate
166// - rejects all non_HSPKI compatible requests
167// - injects ClientInfo into the service context, which can be later retrieved
168// using ClientInfoFromContext
169func WithServerHSPKI() []grpc.ServerOption {
170 if !flag.Parsed() {
171 glog.Exitf("WithServerHSPKI called before flag.Parse!")
172 }
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +0200173 if flagPKIDisable {
174 return []grpc.ServerOption{}
175 }
176
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100177 serverCert, err := tls.LoadX509KeyPair(flagCertificatePath, flagKeyPath)
178 if err != nil {
179 glog.Exitf("WithServerHSPKI: cannot load service certificate/key: %v", err)
180 }
181
182 certPool := x509.NewCertPool()
183 ca, err := ioutil.ReadFile(flagCAPath)
184 if err != nil {
185 glog.Exitf("WithServerHSPKI: cannot load CA certificate: %v", err)
186 }
187 if ok := certPool.AppendCertsFromPEM(ca); !ok {
188 glog.Exitf("WithServerHSPKI: cannot use CA certificate: %v", err)
189 }
190
191 creds := grpc.Creds(credentials.NewTLS(&tls.Config{
192 ClientAuth: tls.RequireAndVerifyClientCert,
193 Certificates: []tls.Certificate{serverCert},
194 ClientCAs: certPool,
195 }))
196
197 interceptor := grpc.UnaryInterceptor(grpcInterceptor)
198
199 return []grpc.ServerOption{creds, interceptor}
200}
Serge Bazanski624295d2018-10-06 18:17:56 +0100201
202func WithClientHSPKI() grpc.DialOption {
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +0200203 if !flag.Parsed() {
204 glog.Exitf("WithServerHSPKI called before flag.Parse!")
205 }
206 if flagPKIDisable {
207 return grpc.WithInsecure()
208 }
209
Serge Bazanski624295d2018-10-06 18:17:56 +0100210 certPool := x509.NewCertPool()
211 ca, err := ioutil.ReadFile(flagCAPath)
212 if err != nil {
213 glog.Exitf("WithClientHSPKI: cannot load CA certificate: %v", err)
214 }
215 if ok := certPool.AppendCertsFromPEM(ca); !ok {
216 glog.Exitf("WithClientHSPKI: cannot use CA certificate: %v", err)
217 }
218
219 clientCert, err := tls.LoadX509KeyPair(flagCertificatePath, flagKeyPath)
220 if err != nil {
221 glog.Exitf("WithClientHSPKI: cannot load service certificate/key: %v", err)
222 }
223
224 creds := credentials.NewTLS(&tls.Config{
225 Certificates: []tls.Certificate{clientCert},
226 RootCAs: certPool,
227 })
228 return grpc.WithTransportCredentials(creds)
229}