istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/writer/compare/sds/util.go (about)

     1  // Copyright 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  
    15  package sdscompare
    16  
    17  import (
    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  
    26  	"istio.io/istio/istioctl/pkg/util/configdump"
    27  	"istio.io/istio/pkg/log"
    28  )
    29  
    30  // SecretItemDiff represents a secret that has been diffed between nodeagent and proxy
    31  type SecretItemDiff struct {
    32  	Agent string `json:"agent"`
    33  	Proxy string `json:"proxy"`
    34  	SecretItem
    35  }
    36  
    37  // SecretItem is an intermediate representation of secrets, used to provide a common
    38  // format between the envoy proxy secrets and node agent output which can be diffed
    39  type SecretItem struct {
    40  	Name        string `json:"resource_name"`
    41  	Data        string `json:"cert"`
    42  	Source      string `json:"source"`
    43  	Destination string `json:"destination"`
    44  	State       string `json:"state"`
    45  	SecretMeta
    46  }
    47  
    48  // SecretMeta holds selected fields which can be extracted from parsed x509 cert
    49  type SecretMeta struct {
    50  	Valid        bool   `json:"cert_valid"`
    51  	SerialNumber string `json:"serial_number"`
    52  	NotAfter     string `json:"not_after"`
    53  	NotBefore    string `json:"not_before"`
    54  	Type         string `json:"type"`
    55  }
    56  
    57  // NewSecretItemBuilder returns a new builder to create a secret item
    58  func NewSecretItemBuilder() SecretItemBuilder {
    59  	return &secretItemBuilder{}
    60  }
    61  
    62  // SecretItemBuilder wraps the process of setting fields for the SecretItem
    63  // and builds the Metadata fields from the cert contents behind the scenes
    64  type SecretItemBuilder interface {
    65  	Name(string) SecretItemBuilder
    66  	Data(string) SecretItemBuilder
    67  	Source(string) SecretItemBuilder
    68  	Destination(string) SecretItemBuilder
    69  	State(string) SecretItemBuilder
    70  	Build() (SecretItem, error)
    71  }
    72  
    73  // secretItemBuilder implements SecretItemBuilder, and acts as an intermediate before SecretItem generation
    74  type secretItemBuilder struct {
    75  	name   string
    76  	data   string
    77  	source string
    78  	dest   string
    79  	state  string
    80  	SecretMeta
    81  }
    82  
    83  // Name sets the name field on a secretItemBuilder
    84  func (s *secretItemBuilder) Name(name string) SecretItemBuilder {
    85  	s.name = name
    86  	return s
    87  }
    88  
    89  // Data sets the data field on a secretItemBuilder
    90  func (s *secretItemBuilder) Data(data string) SecretItemBuilder {
    91  	s.data = data
    92  	return s
    93  }
    94  
    95  // Source sets the source field on a secretItemBuilder
    96  func (s *secretItemBuilder) Source(source string) SecretItemBuilder {
    97  	s.source = source
    98  	return s
    99  }
   100  
   101  // Destination sets the destination field on a secretItemBuilder
   102  func (s *secretItemBuilder) Destination(dest string) SecretItemBuilder {
   103  	s.dest = dest
   104  	return s
   105  }
   106  
   107  // State sets the state of the secret on the agent or sidecar
   108  func (s *secretItemBuilder) State(state string) SecretItemBuilder {
   109  	s.state = state
   110  	return s
   111  }
   112  
   113  // Build takes the set fields from the builder and constructs the actual SecretItem
   114  // including generating the SecretMeta from the supplied cert data, if present
   115  func (s *secretItemBuilder) Build() (SecretItem, error) {
   116  	result := SecretItem{
   117  		Name:        s.name,
   118  		Data:        s.data,
   119  		Source:      s.source,
   120  		Destination: s.dest,
   121  		State:       s.state,
   122  	}
   123  
   124  	var meta SecretMeta
   125  	var err error
   126  	if s.data != "" {
   127  		meta, err = secretMetaFromCert([]byte(s.data))
   128  		if err != nil {
   129  			log.Debugf("failed to parse secret resource %s from source %s: %v",
   130  				s.name, s.source, err)
   131  			result.Valid = false
   132  			return result, nil
   133  		}
   134  		result.SecretMeta = meta
   135  		result.Valid = meta.Valid
   136  		return result, nil
   137  	}
   138  	result.Valid = false
   139  	return result, nil
   140  }
   141  
   142  // GetEnvoySecrets parses the secrets section of the config dump into []SecretItem
   143  func GetEnvoySecrets(
   144  	wrapper *configdump.Wrapper,
   145  ) ([]SecretItem, error) {
   146  	secretConfigDump, err := wrapper.GetSecretConfigDump()
   147  	if err != nil {
   148  		return nil, err
   149  	}
   150  
   151  	proxySecretItems := make([]SecretItem, 0)
   152  	for _, warmingSecret := range secretConfigDump.DynamicWarmingSecrets {
   153  		secret, err := parseDynamicSecret(warmingSecret, "WARMING")
   154  		if err != nil {
   155  			return nil, fmt.Errorf("failed building warming secret %s: %v",
   156  				warmingSecret.Name, err)
   157  		}
   158  		proxySecretItems = append(proxySecretItems, secret)
   159  	}
   160  	for _, activeSecret := range secretConfigDump.DynamicActiveSecrets {
   161  		secret, err := parseDynamicSecret(activeSecret, "ACTIVE")
   162  		if err != nil {
   163  			return nil, fmt.Errorf("failed building warming secret %s: %v",
   164  				activeSecret.Name, err)
   165  		}
   166  		if activeSecret.VersionInfo == "uninitialized" {
   167  			secret.State = "UNINITIALIZED"
   168  		}
   169  		proxySecretItems = append(proxySecretItems, secret)
   170  	}
   171  	return proxySecretItems, nil
   172  }
   173  
   174  func parseDynamicSecret(s *envoy_admin.SecretsConfigDump_DynamicSecret, state string) (SecretItem, error) {
   175  	builder := NewSecretItemBuilder()
   176  	builder.Name(s.Name).State(state)
   177  
   178  	secretTyped := &auth.Secret{}
   179  	err := s.GetSecret().UnmarshalTo(secretTyped)
   180  	if err != nil {
   181  		return SecretItem{}, err
   182  	}
   183  
   184  	certChainSecret := secretTyped.
   185  		GetTlsCertificate().
   186  		GetCertificateChain().
   187  		GetInlineBytes()
   188  	caDataSecret := secretTyped.
   189  		GetValidationContext().
   190  		GetTrustedCa().
   191  		GetInlineBytes()
   192  
   193  	// seems as though the most straightforward way to tell whether this is a root ca or not
   194  	// is to check whether the inline bytes of the cert chain or the trusted ca field is zero length
   195  	if len(certChainSecret) > 0 {
   196  		builder.Data(string(certChainSecret))
   197  	} else if len(caDataSecret) > 0 {
   198  		builder.Data(string(caDataSecret))
   199  	}
   200  
   201  	secret, err := builder.Build()
   202  	if err != nil {
   203  		return SecretItem{}, fmt.Errorf("error building secret: %v", err)
   204  	}
   205  
   206  	return secret, nil
   207  }
   208  
   209  func secretMetaFromCert(rawCert []byte) (SecretMeta, error) {
   210  	block, _ := pem.Decode(rawCert)
   211  	if block == nil {
   212  		return SecretMeta{}, fmt.Errorf("failed to parse certificate PEM")
   213  	}
   214  	cert, err := x509.ParseCertificate(block.Bytes)
   215  	if err != nil {
   216  		return SecretMeta{}, err
   217  	}
   218  	var certType string
   219  	if cert.IsCA {
   220  		certType = "CA"
   221  	} else {
   222  		certType = "Cert Chain"
   223  	}
   224  
   225  	today := time.Now()
   226  	return SecretMeta{
   227  		SerialNumber: fmt.Sprintf("%x", cert.SerialNumber),
   228  		NotAfter:     cert.NotAfter.Format(time.RFC3339),
   229  		NotBefore:    cert.NotBefore.Format(time.RFC3339),
   230  		Type:         certType,
   231  		Valid:        today.After(cert.NotBefore) && today.Before(cert.NotAfter),
   232  	}, nil
   233  }