1// Copyright 2019 Istio Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package sdscompare
16
17import (
18	"crypto/x509"
19	"encoding/pem"
20	"fmt"
21	"time"
22
23	envoy_admin "github.com/envoyproxy/go-control-plane/envoy/admin/v3"
24	auth "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
25	"github.com/golang/protobuf/ptypes"
26
27	"istio.io/istio/istioctl/pkg/util/configdump"
28	"istio.io/istio/security/pkg/nodeagent/sds"
29	"istio.io/pkg/log"
30)
31
32// SecretItemDiff represents a secret that has been diffed between nodeagent and proxy
33type SecretItemDiff struct {
34	Agent string `json:"agent"`
35	Proxy string `json:"proxy"`
36	SecretItem
37}
38
39// SecretItem is an intermediate representation of secrets, used to provide a common
40// format between the envoy proxy secrets and node agent output which can be diffed
41type SecretItem struct {
42	Name        string `json:"resource_name"`
43	Data        string `json:"cert"`
44	Source      string `json:"source"`
45	Destination string `json:"destination"`
46	State       string `json:"state"`
47	SecretMeta
48}
49
50// SecretMeta holds selected fields which can be extracted from parsed x509 cert
51type SecretMeta struct {
52	Valid        bool   `json:"cert_valid"`
53	SerialNumber string `json:"serial_number"`
54	NotAfter     string `json:"not_after"`
55	NotBefore    string `json:"not_before"`
56	Type         string `json:"type"`
57}
58
59// NewSecretItemBuilder returns a new builder to create a secret item
60func NewSecretItemBuilder() SecretItemBuilder {
61	return &secretItemBuilder{}
62}
63
64// SecretItemBuilder wraps the process of setting fields for the SecretItem
65// and builds the Metadata fields from the cert contents behind the scenes
66type SecretItemBuilder interface {
67	Name(string) SecretItemBuilder
68	Data(string) SecretItemBuilder
69	Source(string) SecretItemBuilder
70	Destination(string) SecretItemBuilder
71	State(string) SecretItemBuilder
72	Build() (SecretItem, error)
73}
74
75// secretItemBuilder implements SecretItemBuilder, and acts as an intermediate before SecretItem generation
76type secretItemBuilder struct {
77	name   string
78	data   string
79	source string
80	dest   string
81	state  string
82	SecretMeta
83}
84
85// Name sets the name field on a secretItemBuilder
86func (s *secretItemBuilder) Name(name string) SecretItemBuilder {
87	s.name = name
88	return s
89}
90
91// Data sets the data field on a secretItemBuilder
92func (s *secretItemBuilder) Data(data string) SecretItemBuilder {
93	s.data = data
94	return s
95}
96
97// Source sets the source field on a secretItemBuilder
98func (s *secretItemBuilder) Source(source string) SecretItemBuilder {
99	s.source = source
100	return s
101}
102
103// Destination sets the destination field on a secretItemBuilder
104func (s *secretItemBuilder) Destination(dest string) SecretItemBuilder {
105	s.dest = dest
106	return s
107}
108
109// State sets the state of the secret on the agent or sidecar
110func (s *secretItemBuilder) State(state string) SecretItemBuilder {
111	s.state = state
112	return s
113}
114
115// Build takes the set fields from the builder and constructs the actual SecretItem
116// including generating the SecretMeta from the supplied cert data, if present
117func (s *secretItemBuilder) Build() (SecretItem, error) {
118	result := SecretItem{
119		Name:        s.name,
120		Data:        s.data,
121		Source:      s.source,
122		Destination: s.dest,
123		State:       s.state,
124	}
125
126	var meta SecretMeta
127	var err error
128	if s.data != "" {
129		meta, err = secretMetaFromCert([]byte(s.data))
130		if err != nil {
131			log.Debugf("failed to parse secret resource %s from source %s: %v",
132				s.name, s.source, err)
133			result.Valid = false
134			return result, nil
135		}
136		result.SecretMeta = meta
137		result.Valid = true
138		return result, nil
139	}
140	result.Valid = false
141	return result, nil
142}
143
144// connNameFilter used to provide a filter function through which node agent secrets can be filtered out
145type connNameFilter func(string) bool
146
147// GetNodeAgentSecrets takes the sds.Debug results provided to the comparator and parses them into []SecretItem
148func GetNodeAgentSecrets(
149	agentResponses map[string]sds.Debug, connFilter connNameFilter) ([]SecretItem, error) {
150	secrets := make([]SecretItem, 0)
151	for nodeAgentPod, debug := range agentResponses {
152		for _, client := range debug.Clients {
153			// note that the node agent contains secrets for all pods being served on that node
154			// we don't want to include the secret unless the pod name is included in the ProxyID
155			if connFilter(client.ProxyID) {
156				builder := NewSecretItemBuilder()
157				builder.Name(client.ResourceName).Source(nodeAgentPod).Destination(client.ProxyID)
158				if client.CertificateChain != "" {
159					builder.Data(client.CertificateChain)
160				} else if client.RootCert != "" {
161					builder.Data(client.RootCert)
162				}
163
164				secret, err := builder.Build()
165				if err != nil {
166					return nil, fmt.Errorf("error building node agent secret")
167				}
168				secrets = append(secrets, secret)
169			}
170
171		}
172	}
173
174	return secrets, nil
175}
176
177// GetEnvoySecrets parses the secrets section of the config dump into []SecretItem
178func GetEnvoySecrets(
179	wrapper *configdump.Wrapper) ([]SecretItem, error) {
180	secretConfigDump, err := wrapper.GetSecretConfigDump()
181	if err != nil {
182		return nil, err
183	}
184
185	proxySecretItems := make([]SecretItem, 0)
186	for _, warmingSecret := range secretConfigDump.DynamicWarmingSecrets {
187		secret, err := parseDynamicSecret(warmingSecret, "WARMING")
188		if err != nil {
189			return nil, fmt.Errorf("failed building warming secret %s: %v",
190				warmingSecret.Name, err)
191		}
192		proxySecretItems = append(proxySecretItems, secret)
193	}
194	for _, activeSecret := range secretConfigDump.DynamicActiveSecrets {
195		secret, err := parseDynamicSecret(activeSecret, "ACTIVE")
196		if err != nil {
197			return nil, fmt.Errorf("failed building warming secret %s: %v",
198				activeSecret.Name, err)
199		}
200		proxySecretItems = append(proxySecretItems, secret)
201	}
202	return proxySecretItems, nil
203}
204
205func parseDynamicSecret(s *envoy_admin.SecretsConfigDump_DynamicSecret, state string) (SecretItem, error) {
206	builder := NewSecretItemBuilder()
207	builder.Name(s.Name).State(state)
208
209	secretTyped := &auth.Secret{}
210	err := ptypes.UnmarshalAny(s.GetSecret(), secretTyped)
211	if err != nil {
212		return SecretItem{}, err
213	}
214
215	certChainSecret := secretTyped.
216		GetTlsCertificate().
217		GetCertificateChain().
218		GetInlineBytes()
219	caDataSecret := secretTyped.
220		GetValidationContext().
221		GetTrustedCa().
222		GetInlineBytes()
223
224	// seems as though the most straightforward way to tell whether this is a root ca or not
225	// is to check whether the inline bytes of the cert chain or the trusted ca field is zero length
226	if len(certChainSecret) > 0 {
227		builder.Data(string(certChainSecret))
228	} else if len(caDataSecret) > 0 {
229		builder.Data(string(caDataSecret))
230	}
231
232	secret, err := builder.Build()
233	if err != nil {
234		return SecretItem{}, fmt.Errorf("error building secret: %v", err)
235	}
236
237	return secret, nil
238}
239
240func secretMetaFromCert(rawCert []byte) (SecretMeta, error) {
241	block, _ := pem.Decode(rawCert)
242	if block == nil {
243		return SecretMeta{}, fmt.Errorf("failed to parse certificate PEM")
244	}
245	cert, err := x509.ParseCertificate(block.Bytes)
246	if err != nil {
247		return SecretMeta{}, err
248	}
249	var certType string
250	if cert.IsCA {
251		certType = "CA"
252	} else {
253		certType = "Cert Chain"
254	}
255
256	return SecretMeta{
257		SerialNumber: fmt.Sprintf("%d", cert.SerialNumber),
258		NotAfter:     cert.NotAfter.Format(time.RFC3339),
259		NotBefore:    cert.NotBefore.Format(time.RFC3339),
260		Type:         certType,
261	}, nil
262}
263