Serge Bazanski | 814749f | 2018-10-25 12:01:10 +0100 | [diff] [blame] | 1 | package pki |
Sergiusz Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 2 | |
| 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 | |
| 17 | import ( |
| 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 | |
| 35 | var ( |
| 36 | flagCAPath string |
| 37 | flagCertificatePath string |
| 38 | flagKeyPath string |
Sergiusz Bazanski | 6f773e0 | 2019-10-02 20:46:48 +0200 | [diff] [blame] | 39 | flagPKICluster string |
Sergiusz Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 40 | flagPKIRealm string |
Sergiusz Bazanski | 9dc4b68 | 2019-04-05 23:51:49 +0200 | [diff] [blame] | 41 | flagPKIDisable bool |
Sergiusz Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 42 | |
| 43 | // Enable logging HSPKI info into traces |
| 44 | Trace = true |
| 45 | // Enable logging HSPKI info into glog |
| 46 | Log = false |
| 47 | ) |
| 48 | |
| 49 | const ( |
| 50 | ctxKeyClientInfo = "hspki-client-info" |
| 51 | ) |
| 52 | |
| 53 | func 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 Bazanski | 6f773e0 | 2019-10-02 20:46:48 +0200 | [diff] [blame] | 57 | 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 Bazanski | 9dc4b68 | 2019-04-05 23:51:49 +0200 | [diff] [blame] | 59 | flag.BoolVar(&flagPKIDisable, "hspki_disable", false, "Disable PKI entirely (insecure!)") |
Sergiusz Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 60 | } |
| 61 | |
| 62 | func 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 | |
| 82 | func parseClientName(name string) (*ClientInfo, error) { |
| 83 | if !strings.HasSuffix(name, "."+flagPKIRealm) { |
| 84 | return nil, fmt.Errorf("invalid realm") |
| 85 | } |
Sergiusz Bazanski | 6f773e0 | 2019-10-02 20:46:48 +0200 | [diff] [blame] | 86 | |
| 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 Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 105 | } |
Sergiusz Bazanski | 6f773e0 | 2019-10-02 20:46:48 +0200 | [diff] [blame] | 106 | |
| 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 Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 116 | return &ClientInfo{ |
Sergiusz Bazanski | 6f773e0 | 2019-10-02 20:46:48 +0200 | [diff] [blame] | 117 | Cluster: fmt.Sprintf("%s.%s", clusterShort, flagPKIRealm), |
| 118 | Principal: fmt.Sprintf("%s.svc", parts[1]), |
Sergiusz Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 119 | Job: parts[0], |
| 120 | }, nil |
| 121 | } |
| 122 | |
| 123 | func 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 | |
| 128 | func 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. |
| 166 | type ClientInfo struct { |
Sergiusz Bazanski | 6f773e0 | 2019-10-02 20:46:48 +0200 | [diff] [blame] | 167 | Cluster string |
Sergiusz Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 168 | Principal string |
| 169 | Job string |
| 170 | } |
| 171 | |
| 172 | // String returns a human-readable representation of the ClientInfo in the |
Sergiusz Bazanski | 6f773e0 | 2019-10-02 20:46:48 +0200 | [diff] [blame] | 173 | // form "job=foo, principal=bar.svc, cluster=baz.hswaw.net". |
Sergiusz Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 174 | func (c *ClientInfo) String() string { |
Sergiusz Bazanski | 6f773e0 | 2019-10-02 20:46:48 +0200 | [diff] [blame] | 175 | 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. |
| 180 | func (c *ClientInfo) Person() string { |
| 181 | if c.Cluster != fmt.Sprintf("person.%s", flagPKIRealm) { |
| 182 | return "" |
| 183 | } |
| 184 | return c.Principal |
Sergiusz Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 185 | } |
| 186 | |
| 187 | // ClientInfoFromContext returns ClientInfo from a gRPC service context. |
| 188 | func 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 |
| 205 | func WithServerHSPKI() []grpc.ServerOption { |
| 206 | if !flag.Parsed() { |
| 207 | glog.Exitf("WithServerHSPKI called before flag.Parse!") |
| 208 | } |
Sergiusz Bazanski | 9dc4b68 | 2019-04-05 23:51:49 +0200 | [diff] [blame] | 209 | if flagPKIDisable { |
| 210 | return []grpc.ServerOption{} |
| 211 | } |
| 212 | |
Sergiusz Bazanski | f02cd77 | 2018-08-28 15:25:33 +0100 | [diff] [blame] | 213 | 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 Bazanski | 624295d | 2018-10-06 18:17:56 +0100 | [diff] [blame] | 237 | |
| 238 | func WithClientHSPKI() grpc.DialOption { |
Sergiusz Bazanski | 9dc4b68 | 2019-04-05 23:51:49 +0200 | [diff] [blame] | 239 | if !flag.Parsed() { |
| 240 | glog.Exitf("WithServerHSPKI called before flag.Parse!") |
| 241 | } |
| 242 | if flagPKIDisable { |
| 243 | return grpc.WithInsecure() |
| 244 | } |
| 245 | |
Serge Bazanski | 624295d | 2018-10-06 18:17:56 +0100 | [diff] [blame] | 246 | 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 | } |