blob: 1720ad80e0da491e6ac7ca140bdd40a0efd1f533 [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
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +020039 flagPKICluster string
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +010040 flagPKIRealm string
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +020041 flagPKIDisable bool
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +010042
43 // Enable logging HSPKI info into traces
44 Trace = true
45 // Enable logging HSPKI info into glog
46 Log = false
47)
48
49const (
50 ctxKeyClientInfo = "hspki-client-info"
51)
52
53func init() {
54 flag.StringVar(&flagCAPath, "hspki_tls_ca_path", "pki/ca.pem", "Path to PKI CA certificate")
55 flag.StringVar(&flagCertificatePath, "hspki_tls_certificate_path", "pki/service.pem", "Path to PKI service certificate")
56 flag.StringVar(&flagKeyPath, "hspki_tls_key_path", "pki/service-key.pem", "Path to PKI service private key")
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +020057 flag.StringVar(&flagPKICluster, "hspki_cluster", "local.hswaw.net", "FQDN of cluster on which this service runs")
58 flag.StringVar(&flagPKIRealm, "hspki_realm", "hswaw.net", "Cluster realm (top level from which we accept foreign cluster certs)")
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +020059 flag.BoolVar(&flagPKIDisable, "hspki_disable", false, "Disable PKI entirely (insecure!)")
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +010060}
61
62func maybeTrace(ctx context.Context, f string, args ...interface{}) {
63 if Log {
64 glog.Infof(f, args...)
65 }
66
67 if !Trace {
68 return
69 }
70
71 tr, ok := trace.FromContext(ctx)
72 if !ok {
73 if !Log {
74 fmtd := fmt.Sprintf(f, args...)
75 glog.Info("[no trace] %v", fmtd)
76 }
77 return
78 }
79 tr.LazyPrintf(f, args...)
80}
81
82func parseClientName(name string) (*ClientInfo, error) {
83 if !strings.HasSuffix(name, "."+flagPKIRealm) {
84 return nil, fmt.Errorf("invalid realm")
85 }
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +020086
87 inRealm := strings.TrimSuffix(name, "."+flagPKIRealm)
88
89 special := []string{"person", "external"}
90
91 for _, s := range special {
92 // Special case for people running jobs from workstations, or for non-cluster services.
93 if strings.HasSuffix(inRealm, "."+s) {
94 asPerson := strings.TrimSuffix(inRealm, "."+s)
95 parts := strings.Split(asPerson, ".")
96 if len(parts) != 1 {
97 return nil, fmt.Errorf("invalid person fqdn")
98 }
99 return &ClientInfo{
100 Cluster: fmt.Sprintf("%s.%s", s, flagPKIRealm),
101 Principal: parts[0],
102 Job: "",
103 }, nil
104 }
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100105 }
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +0200106
107 parts := strings.Split(inRealm, ".")
108 if len(parts) != 4 {
109 return nil, fmt.Errorf("invalid job/principal format for in-cluster")
110 }
111 if parts[2] != "svc" {
112 return nil, fmt.Errorf("can only refer to services within cluster")
113 }
114 clusterShort := parts[3]
115
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100116 return &ClientInfo{
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +0200117 Cluster: fmt.Sprintf("%s.%s", clusterShort, flagPKIRealm),
118 Principal: fmt.Sprintf("%s.svc", parts[1]),
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100119 Job: parts[0],
120 }, nil
121}
122
123func withPKIInfo(ctx context.Context, c *ClientInfo) context.Context {
124 maybeTrace(ctx, "HSPKI: Applying ClientInfo: %s", c.String())
125 return context.WithValue(ctx, ctxKeyClientInfo, c)
126}
127
128func grpcInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
129 peer, ok := peer.FromContext(ctx)
130 if !ok {
131 maybeTrace(ctx, "HSPKI: Could not establish identity of peer.")
132 return nil, status.Errorf(codes.PermissionDenied, "no peer info")
133 }
134
135 authInfo, ok := peer.AuthInfo.(credentials.TLSInfo)
136 if !ok {
137 maybeTrace(ctx, "HSPKI: Could not establish TLS identity of peer.")
138 return nil, status.Errorf(codes.PermissionDenied, "no TLS certificate presented")
139 }
140
141 chains := authInfo.State.VerifiedChains
142 if len(chains) != 1 {
143 maybeTrace(ctx, "HSPKI: No trusted chains found.")
144 return nil, status.Errorf(codes.PermissionDenied, "no trusted TLS certificate presented")
145 }
146
147 chain := chains[0]
148
149 certDNs := make([]string, len(chain))
150 for i, cert := range chain {
151 certDNs[i] = cert.Subject.String()
152 }
153 maybeTrace(ctx, "HSPKI: Trust chain: %s", strings.Join(certDNs, ", "))
154
155 clientInfo, err := parseClientName(chain[0].Subject.CommonName)
156 if err != nil {
157 maybeTrace(ctx, "HSPKI: Invalid CN %q: %v", chain[0].Subject.CommonName, err)
158 return nil, status.Errorf(codes.PermissionDenied, "invalid TLS CN format")
159 }
160 ctx = withPKIInfo(ctx, clientInfo)
161 return handler(ctx, req)
162}
163
164// ClientInfo contains information about the HSPKI authentication data of the
165// gRPC client that has made the request.
166type ClientInfo struct {
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +0200167 Cluster string
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100168 Principal string
169 Job string
170}
171
172// String returns a human-readable representation of the ClientInfo in the
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +0200173// form "job=foo, principal=bar.svc, cluster=baz.hswaw.net".
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100174func (c *ClientInfo) String() string {
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +0200175 return fmt.Sprintf("job=%q, principal=%q, cluster=%q", c.Job, c.Principal, c.Cluster)
176}
177
178// Person returns a reference to a person's ID if the ClientInfo describes a person.
179// Otherwise, it returns an empty string.
180func (c *ClientInfo) Person() string {
181 if c.Cluster != fmt.Sprintf("person.%s", flagPKIRealm) {
182 return ""
183 }
184 return c.Principal
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100185}
186
187// ClientInfoFromContext returns ClientInfo from a gRPC service context.
188func ClientInfoFromContext(ctx context.Context) *ClientInfo {
189 v := ctx.Value(ctxKeyClientInfo)
190 if v == nil {
191 return nil
192 }
193 ci, ok := v.(*ClientInfo)
194 if !ok {
195 return nil
196 }
197 return ci
198}
199
200// WithServerHSPKI is a grpc.ServerOptions array that ensures that the gRPC server:
201// - runs with HSPKI TLS Service Certificate
202// - rejects all non_HSPKI compatible requests
203// - injects ClientInfo into the service context, which can be later retrieved
204// using ClientInfoFromContext
205func WithServerHSPKI() []grpc.ServerOption {
206 if !flag.Parsed() {
207 glog.Exitf("WithServerHSPKI called before flag.Parse!")
208 }
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +0200209 if flagPKIDisable {
210 return []grpc.ServerOption{}
211 }
212
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100213 serverCert, err := tls.LoadX509KeyPair(flagCertificatePath, flagKeyPath)
214 if err != nil {
215 glog.Exitf("WithServerHSPKI: cannot load service certificate/key: %v", err)
216 }
217
218 certPool := x509.NewCertPool()
219 ca, err := ioutil.ReadFile(flagCAPath)
220 if err != nil {
221 glog.Exitf("WithServerHSPKI: cannot load CA certificate: %v", err)
222 }
223 if ok := certPool.AppendCertsFromPEM(ca); !ok {
224 glog.Exitf("WithServerHSPKI: cannot use CA certificate: %v", err)
225 }
226
227 creds := grpc.Creds(credentials.NewTLS(&tls.Config{
228 ClientAuth: tls.RequireAndVerifyClientCert,
229 Certificates: []tls.Certificate{serverCert},
230 ClientCAs: certPool,
231 }))
232
233 interceptor := grpc.UnaryInterceptor(grpcInterceptor)
234
235 return []grpc.ServerOption{creds, interceptor}
236}
Serge Bazanski624295d2018-10-06 18:17:56 +0100237
238func WithClientHSPKI() grpc.DialOption {
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +0200239 if !flag.Parsed() {
240 glog.Exitf("WithServerHSPKI called before flag.Parse!")
241 }
242 if flagPKIDisable {
243 return grpc.WithInsecure()
244 }
245
Serge Bazanski624295d2018-10-06 18:17:56 +0100246 certPool := x509.NewCertPool()
247 ca, err := ioutil.ReadFile(flagCAPath)
248 if err != nil {
249 glog.Exitf("WithClientHSPKI: cannot load CA certificate: %v", err)
250 }
251 if ok := certPool.AppendCertsFromPEM(ca); !ok {
252 glog.Exitf("WithClientHSPKI: cannot use CA certificate: %v", err)
253 }
254
255 clientCert, err := tls.LoadX509KeyPair(flagCertificatePath, flagKeyPath)
256 if err != nil {
257 glog.Exitf("WithClientHSPKI: cannot load service certificate/key: %v", err)
258 }
259
260 creds := credentials.NewTLS(&tls.Config{
261 Certificates: []tls.Certificate{clientCert},
262 RootCAs: certPool,
263 })
264 return grpc.WithTransportCredentials(creds)
265}