blob: 44099c000dc112bbc9ac5cf3faa0b336b7e78745 [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"
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +010023 "strings"
24
25 "github.com/golang/glog"
26 "golang.org/x/net/trace"
27 "google.golang.org/grpc"
28 "google.golang.org/grpc/codes"
29 "google.golang.org/grpc/credentials"
30 "google.golang.org/grpc/peer"
31 "google.golang.org/grpc/status"
32)
33
34var (
35 flagCAPath string
36 flagCertificatePath string
37 flagKeyPath string
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +020038 flagPKICluster string
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +010039 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")
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +020056 flag.StringVar(&flagPKICluster, "hspki_cluster", "local.hswaw.net", "FQDN of cluster on which this service runs")
57 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 +020058 flag.BoolVar(&flagPKIDisable, "hspki_disable", false, "Disable PKI entirely (insecure!)")
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +010059}
60
61func maybeTrace(ctx context.Context, f string, args ...interface{}) {
62 if Log {
63 glog.Infof(f, args...)
64 }
65
66 if !Trace {
67 return
68 }
69
70 tr, ok := trace.FromContext(ctx)
71 if !ok {
72 if !Log {
73 fmtd := fmt.Sprintf(f, args...)
74 glog.Info("[no trace] %v", fmtd)
75 }
76 return
77 }
78 tr.LazyPrintf(f, args...)
79}
80
81func parseClientName(name string) (*ClientInfo, error) {
82 if !strings.HasSuffix(name, "."+flagPKIRealm) {
83 return nil, fmt.Errorf("invalid realm")
84 }
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +020085
86 inRealm := strings.TrimSuffix(name, "."+flagPKIRealm)
87
88 special := []string{"person", "external"}
89
90 for _, s := range special {
91 // Special case for people running jobs from workstations, or for non-cluster services.
92 if strings.HasSuffix(inRealm, "."+s) {
93 asPerson := strings.TrimSuffix(inRealm, "."+s)
94 parts := strings.Split(asPerson, ".")
95 if len(parts) != 1 {
96 return nil, fmt.Errorf("invalid person fqdn")
97 }
98 return &ClientInfo{
99 Cluster: fmt.Sprintf("%s.%s", s, flagPKIRealm),
100 Principal: parts[0],
101 Job: "",
102 }, nil
103 }
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100104 }
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +0200105
106 parts := strings.Split(inRealm, ".")
107 if len(parts) != 4 {
108 return nil, fmt.Errorf("invalid job/principal format for in-cluster")
109 }
110 if parts[2] != "svc" {
111 return nil, fmt.Errorf("can only refer to services within cluster")
112 }
113 clusterShort := parts[3]
114
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100115 return &ClientInfo{
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +0200116 Cluster: fmt.Sprintf("%s.%s", clusterShort, flagPKIRealm),
117 Principal: fmt.Sprintf("%s.svc", parts[1]),
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100118 Job: parts[0],
119 }, nil
120}
121
122func withPKIInfo(ctx context.Context, c *ClientInfo) context.Context {
123 maybeTrace(ctx, "HSPKI: Applying ClientInfo: %s", c.String())
124 return context.WithValue(ctx, ctxKeyClientInfo, c)
125}
126
127func grpcInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
128 peer, ok := peer.FromContext(ctx)
129 if !ok {
130 maybeTrace(ctx, "HSPKI: Could not establish identity of peer.")
131 return nil, status.Errorf(codes.PermissionDenied, "no peer info")
132 }
133
134 authInfo, ok := peer.AuthInfo.(credentials.TLSInfo)
135 if !ok {
136 maybeTrace(ctx, "HSPKI: Could not establish TLS identity of peer.")
137 return nil, status.Errorf(codes.PermissionDenied, "no TLS certificate presented")
138 }
139
140 chains := authInfo.State.VerifiedChains
141 if len(chains) != 1 {
142 maybeTrace(ctx, "HSPKI: No trusted chains found.")
143 return nil, status.Errorf(codes.PermissionDenied, "no trusted TLS certificate presented")
144 }
145
146 chain := chains[0]
147
148 certDNs := make([]string, len(chain))
149 for i, cert := range chain {
150 certDNs[i] = cert.Subject.String()
151 }
152 maybeTrace(ctx, "HSPKI: Trust chain: %s", strings.Join(certDNs, ", "))
153
154 clientInfo, err := parseClientName(chain[0].Subject.CommonName)
155 if err != nil {
156 maybeTrace(ctx, "HSPKI: Invalid CN %q: %v", chain[0].Subject.CommonName, err)
157 return nil, status.Errorf(codes.PermissionDenied, "invalid TLS CN format")
158 }
159 ctx = withPKIInfo(ctx, clientInfo)
160 return handler(ctx, req)
161}
162
163// ClientInfo contains information about the HSPKI authentication data of the
164// gRPC client that has made the request.
165type ClientInfo struct {
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +0200166 Cluster string
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100167 Principal string
168 Job string
169}
170
171// String returns a human-readable representation of the ClientInfo in the
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +0200172// form "job=foo, principal=bar.svc, cluster=baz.hswaw.net".
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100173func (c *ClientInfo) String() string {
Sergiusz Bazanski6f773e02019-10-02 20:46:48 +0200174 return fmt.Sprintf("job=%q, principal=%q, cluster=%q", c.Job, c.Principal, c.Cluster)
175}
176
177// Person returns a reference to a person's ID if the ClientInfo describes a person.
178// Otherwise, it returns an empty string.
179func (c *ClientInfo) Person() string {
180 if c.Cluster != fmt.Sprintf("person.%s", flagPKIRealm) {
181 return ""
182 }
183 return c.Principal
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100184}
185
186// ClientInfoFromContext returns ClientInfo from a gRPC service context.
187func ClientInfoFromContext(ctx context.Context) *ClientInfo {
188 v := ctx.Value(ctxKeyClientInfo)
189 if v == nil {
190 return nil
191 }
192 ci, ok := v.(*ClientInfo)
193 if !ok {
194 return nil
195 }
196 return ci
197}
198
199// WithServerHSPKI is a grpc.ServerOptions array that ensures that the gRPC server:
200// - runs with HSPKI TLS Service Certificate
201// - rejects all non_HSPKI compatible requests
202// - injects ClientInfo into the service context, which can be later retrieved
203// using ClientInfoFromContext
204func WithServerHSPKI() []grpc.ServerOption {
205 if !flag.Parsed() {
206 glog.Exitf("WithServerHSPKI called before flag.Parse!")
207 }
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +0200208 if flagPKIDisable {
209 return []grpc.ServerOption{}
210 }
211
Serge Bazanskif3312ef2020-08-01 17:15:52 +0200212 loc, err := loadCredentials()
213 if err != nil {
214 glog.Exitf("WithServerHSPKI: loadCredentials: %v", err)
215 }
216
217 serverCert, err := tls.X509KeyPair(loc.cert, loc.key)
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100218 if err != nil {
219 glog.Exitf("WithServerHSPKI: cannot load service certificate/key: %v", err)
220 }
221
222 certPool := x509.NewCertPool()
Serge Bazanskif3312ef2020-08-01 17:15:52 +0200223 if ok := certPool.AppendCertsFromPEM(loc.ca); !ok {
224 glog.Exitf("WithServerHSPKI: cannot use CA certificate")
Sergiusz Bazanskif02cd772018-08-28 15:25:33 +0100225 }
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
Serge Bazanskief2fbaf2020-08-01 22:01:33 +0200238type ClientHSPKIOption func(c *tls.Config)
239
240func OverrideServerName(name string) ClientHSPKIOption {
241 return func(c *tls.Config) {
242 c.ServerName = name
243 }
244}
245
246func WithClientHSPKI(opts ...ClientHSPKIOption) grpc.DialOption {
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +0200247 if !flag.Parsed() {
248 glog.Exitf("WithServerHSPKI called before flag.Parse!")
249 }
250 if flagPKIDisable {
251 return grpc.WithInsecure()
252 }
253
Serge Bazanskif3312ef2020-08-01 17:15:52 +0200254 loc, err := loadCredentials()
Serge Bazanski624295d2018-10-06 18:17:56 +0100255 if err != nil {
Serge Bazanskif3312ef2020-08-01 17:15:52 +0200256 glog.Exitf("WithServerHSPKI: loadCredentials: %v", err)
Serge Bazanski624295d2018-10-06 18:17:56 +0100257 }
258
Serge Bazanskif3312ef2020-08-01 17:15:52 +0200259 certPool := x509.NewCertPool()
260 if ok := certPool.AppendCertsFromPEM(loc.ca); !ok {
261 glog.Exitf("WithServerHSPKI: cannot use CA certificate")
262 }
263
264 clientCert, err := tls.X509KeyPair(loc.cert, loc.key)
Serge Bazanski624295d2018-10-06 18:17:56 +0100265 if err != nil {
266 glog.Exitf("WithClientHSPKI: cannot load service certificate/key: %v", err)
267 }
268
Serge Bazanskief2fbaf2020-08-01 22:01:33 +0200269 config := &tls.Config{
Serge Bazanski624295d2018-10-06 18:17:56 +0100270 Certificates: []tls.Certificate{clientCert},
271 RootCAs: certPool,
Serge Bazanskief2fbaf2020-08-01 22:01:33 +0200272 }
273
274 for _, opt := range opts {
275 opt(config)
276 }
277
278 creds := credentials.NewTLS(config)
Serge Bazanski624295d2018-10-06 18:17:56 +0100279 return grpc.WithTransportCredentials(creds)
280}