istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/spiffe/spiffe.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 spiffe
    16  
    17  import (
    18  	"crypto/tls"
    19  	"crypto/x509"
    20  	"encoding/json"
    21  	"encoding/pem"
    22  	"fmt"
    23  	"net"
    24  	"net/http"
    25  	"net/url"
    26  	"strings"
    27  	"sync"
    28  	"time"
    29  
    30  	jose "github.com/go-jose/go-jose/v3"
    31  
    32  	"istio.io/istio/pkg/config/constants"
    33  	"istio.io/istio/pkg/log"
    34  	"istio.io/istio/pkg/util/sets"
    35  )
    36  
    37  const (
    38  	Scheme = "spiffe"
    39  
    40  	URIPrefix    = Scheme + "://"
    41  	URIPrefixLen = len(URIPrefix)
    42  
    43  	// The default SPIFFE URL value for trust domain
    44  	defaultTrustDomain = constants.DefaultClusterLocalDomain
    45  
    46  	ServiceAccountSegment = "sa"
    47  	NamespaceSegment      = "ns"
    48  )
    49  
    50  var (
    51  	trustDomain      = defaultTrustDomain
    52  	trustDomainMutex sync.RWMutex
    53  
    54  	firstRetryBackOffTime = time.Millisecond * 50
    55  
    56  	spiffeLog = log.RegisterScope("spiffe", "SPIFFE library logging")
    57  )
    58  
    59  type Identity struct {
    60  	TrustDomain    string
    61  	Namespace      string
    62  	ServiceAccount string
    63  }
    64  
    65  func ParseIdentity(s string) (Identity, error) {
    66  	if !strings.HasPrefix(s, URIPrefix) {
    67  		return Identity{}, fmt.Errorf("identity is not a spiffe format")
    68  	}
    69  	split := strings.Split(s[URIPrefixLen:], "/")
    70  	if len(split) != 5 {
    71  		return Identity{}, fmt.Errorf("identity is not a spiffe format")
    72  	}
    73  	if split[1] != NamespaceSegment || split[3] != ServiceAccountSegment {
    74  		return Identity{}, fmt.Errorf("identity is not a spiffe format")
    75  	}
    76  	return Identity{
    77  		TrustDomain:    split[0],
    78  		Namespace:      split[2],
    79  		ServiceAccount: split[4],
    80  	}, nil
    81  }
    82  
    83  func (i Identity) String() string {
    84  	return URIPrefix + i.TrustDomain + "/ns/" + i.Namespace + "/sa/" + i.ServiceAccount
    85  }
    86  
    87  type bundleDoc struct {
    88  	jose.JSONWebKeySet
    89  	Sequence    uint64 `json:"spiffe_sequence,omitempty"`
    90  	RefreshHint int    `json:"spiffe_refresh_hint,omitempty"`
    91  }
    92  
    93  func SetTrustDomain(value string) {
    94  	// Replace special characters in spiffe
    95  	v := strings.Replace(value, "@", ".", -1)
    96  	trustDomainMutex.Lock()
    97  	trustDomain = v
    98  	trustDomainMutex.Unlock()
    99  }
   100  
   101  func GetTrustDomain() string {
   102  	trustDomainMutex.RLock()
   103  	defer trustDomainMutex.RUnlock()
   104  	return trustDomain
   105  }
   106  
   107  // GenSpiffeURI returns the formatted uri(SPIFFE format for now) for the certificate.
   108  func GenSpiffeURI(ns, serviceAccount string) (string, error) {
   109  	var err error
   110  	if ns == "" || serviceAccount == "" {
   111  		err = fmt.Errorf(
   112  			"namespace or service account empty for SPIFFE uri ns=%v serviceAccount=%v", ns, serviceAccount)
   113  	}
   114  	return URIPrefix + GetTrustDomain() + "/ns/" + ns + "/sa/" + serviceAccount, err
   115  }
   116  
   117  // MustGenSpiffeURI returns the formatted uri(SPIFFE format for now) for the certificate and logs if there was an error.
   118  func MustGenSpiffeURI(ns, serviceAccount string) string {
   119  	uri, err := GenSpiffeURI(ns, serviceAccount)
   120  	if err != nil {
   121  		spiffeLog.Debug(err.Error())
   122  	}
   123  	return uri
   124  }
   125  
   126  // ExpandWithTrustDomains expands a given spiffe identities, plus a list of trust domain aliases.
   127  // We ensure the returned list does not contain duplicates; the original input is always retained.
   128  // For example,
   129  // ExpandWithTrustDomains({"spiffe://td1/ns/def/sa/def"}, {"td1", "td2"}) returns
   130  //
   131  //	{"spiffe://td1/ns/def/sa/def", "spiffe://td2/ns/def/sa/def"}.
   132  //
   133  // ExpandWithTrustDomains({"spiffe://td1/ns/def/sa/a", "spiffe://td1/ns/def/sa/b"}, {"td2"}) returns
   134  //
   135  //	{"spiffe://td1/ns/def/sa/a", "spiffe://td2/ns/def/sa/a", "spiffe://td1/ns/def/sa/b", "spiffe://td2/ns/def/sa/b"}.
   136  func ExpandWithTrustDomains(spiffeIdentities sets.String, trustDomainAliases []string) sets.String {
   137  	if len(trustDomainAliases) == 0 {
   138  		return spiffeIdentities
   139  	}
   140  	out := sets.New[string]()
   141  	for id := range spiffeIdentities {
   142  		out.Insert(id)
   143  		// Skip if not a SPIFFE identity - This can happen for example if the identity is a DNS name.
   144  		if !strings.HasPrefix(id, URIPrefix) {
   145  			continue
   146  		}
   147  		// Expand with aliases set.
   148  		m, err := ParseIdentity(id)
   149  		if err != nil {
   150  			spiffeLog.Errorf("Failed to extract SPIFFE trust domain from %v: %v", id, err)
   151  			continue
   152  		}
   153  		for _, td := range trustDomainAliases {
   154  			m.TrustDomain = td
   155  			out.Insert(m.String())
   156  		}
   157  	}
   158  	return out
   159  }
   160  
   161  // GetTrustDomainFromURISAN extracts the trust domain part from the URI SAN in the X.509 certificate.
   162  func GetTrustDomainFromURISAN(uriSan string) (string, error) {
   163  	parsed, err := ParseIdentity(uriSan)
   164  	if err != nil {
   165  		return "", fmt.Errorf("failed to parse URI SAN %s. Error: %v", uriSan, err)
   166  	}
   167  	return parsed.TrustDomain, nil
   168  }
   169  
   170  // RetrieveSpiffeBundleRootCerts retrieves the trusted CA certificates from a list of SPIFFE bundle endpoints.
   171  // It can use the system cert pool and the supplied certificates to validate the endpoints.
   172  func RetrieveSpiffeBundleRootCerts(config map[string]string, caCertPool *x509.CertPool, retryTimeout time.Duration) (
   173  	map[string][]*x509.Certificate, error,
   174  ) {
   175  	httpClient := &http.Client{
   176  		Timeout: time.Second * 10,
   177  	}
   178  
   179  	ret := map[string][]*x509.Certificate{}
   180  	for trustDomain, endpoint := range config {
   181  		if !strings.HasPrefix(endpoint, "https://") {
   182  			endpoint = "https://" + endpoint
   183  		}
   184  		u, err := url.Parse(endpoint)
   185  		if err != nil {
   186  			return nil, fmt.Errorf("failed to split the SPIFFE bundle URL: %v", err)
   187  		}
   188  
   189  		config := &tls.Config{
   190  			ServerName: u.Hostname(),
   191  			RootCAs:    caCertPool,
   192  			MinVersion: tls.VersionTLS12,
   193  		}
   194  
   195  		httpClient.Transport = &http.Transport{
   196  			Proxy:           http.ProxyFromEnvironment,
   197  			TLSClientConfig: config,
   198  			DialContext: (&net.Dialer{
   199  				Timeout: time.Second * 10,
   200  			}).DialContext,
   201  			IdleConnTimeout:       90 * time.Second,
   202  			TLSHandshakeTimeout:   10 * time.Second,
   203  			ExpectContinueTimeout: 1 * time.Second,
   204  		}
   205  
   206  		retryBackoffTime := firstRetryBackOffTime
   207  		startTime := time.Now()
   208  		var resp *http.Response
   209  		for {
   210  			resp, err = httpClient.Get(endpoint)
   211  			var errMsg string
   212  			if err != nil {
   213  				errMsg = fmt.Sprintf("Calling %s failed with error: %v", endpoint, err)
   214  			} else if resp == nil {
   215  				errMsg = fmt.Sprintf("Calling %s failed with nil response", endpoint)
   216  			} else if resp.StatusCode != http.StatusOK {
   217  				b := make([]byte, 1024)
   218  				n, _ := resp.Body.Read(b)
   219  				errMsg = fmt.Sprintf("Calling %s failed with unexpected status: %v, fetching bundle: %s",
   220  					endpoint, resp.StatusCode, string(b[:n]))
   221  			} else {
   222  				break
   223  			}
   224  
   225  			if startTime.Add(retryTimeout).Before(time.Now()) {
   226  				return nil, fmt.Errorf("exhausted retries to fetch the SPIFFE bundle %s from url %s. Latest error: %v",
   227  					trustDomain, endpoint, errMsg)
   228  			}
   229  
   230  			spiffeLog.Warnf("%s, retry in %v", errMsg, retryBackoffTime)
   231  			time.Sleep(retryBackoffTime)
   232  			retryBackoffTime *= 2 // Exponentially increase the retry backoff time.
   233  		}
   234  		defer resp.Body.Close()
   235  
   236  		doc := new(bundleDoc)
   237  		if err := json.NewDecoder(resp.Body).Decode(doc); err != nil {
   238  			return nil, fmt.Errorf("trust domain [%s] at URL [%s] failed to decode bundle: %v", trustDomain, endpoint, err)
   239  		}
   240  
   241  		var certs []*x509.Certificate
   242  		for i, key := range doc.Keys {
   243  			if key.Use == "x509-svid" {
   244  				if len(key.Certificates) != 1 {
   245  					return nil, fmt.Errorf("trust domain [%s] at URL [%s] expected 1 certificate in x509-svid entry %d; got %d",
   246  						trustDomain, endpoint, i, len(key.Certificates))
   247  				}
   248  				certs = append(certs, key.Certificates[0])
   249  			}
   250  		}
   251  		if len(certs) == 0 {
   252  			return nil, fmt.Errorf("trust domain [%s] at URL [%s] does not provide a X509 SVID", trustDomain, endpoint)
   253  		}
   254  		ret[trustDomain] = certs
   255  	}
   256  	for trustDomain, certs := range ret {
   257  		spiffeLog.Infof("Loaded SPIFFE trust bundle for: %v, containing %d certs", trustDomain, len(certs))
   258  	}
   259  	return ret, nil
   260  }
   261  
   262  // PeerCertVerifier is an instance to verify the peer certificate in the SPIFFE way using the retrieved root certificates.
   263  type PeerCertVerifier struct {
   264  	generalCertPool *x509.CertPool
   265  	certPools       map[string]*x509.CertPool
   266  }
   267  
   268  // NewPeerCertVerifier returns a new PeerCertVerifier.
   269  func NewPeerCertVerifier() *PeerCertVerifier {
   270  	return &PeerCertVerifier{
   271  		generalCertPool: x509.NewCertPool(),
   272  		certPools:       make(map[string]*x509.CertPool),
   273  	}
   274  }
   275  
   276  // GetGeneralCertPool returns generalCertPool containing all root certs.
   277  func (v *PeerCertVerifier) GetGeneralCertPool() *x509.CertPool {
   278  	return v.generalCertPool
   279  }
   280  
   281  // AddMapping adds a new trust domain to certificates mapping to the certPools map.
   282  func (v *PeerCertVerifier) AddMapping(trustDomain string, certs []*x509.Certificate) {
   283  	if v.certPools[trustDomain] == nil {
   284  		v.certPools[trustDomain] = x509.NewCertPool()
   285  	}
   286  	for _, cert := range certs {
   287  		v.certPools[trustDomain].AddCert(cert)
   288  		v.generalCertPool.AddCert(cert)
   289  	}
   290  	spiffeLog.Infof("Added %d certs to trust domain %s in peer cert verifier", len(certs), trustDomain)
   291  }
   292  
   293  // AddMappingFromPEM adds multiple RootCA's to the spiffe Trust bundle in the trustDomain namespace
   294  func (v *PeerCertVerifier) AddMappingFromPEM(trustDomain string, rootCertBytes []byte) error {
   295  	block, rest := pem.Decode(rootCertBytes)
   296  	var blockBytes []byte
   297  
   298  	// Loop while there are no block are found
   299  	for block != nil {
   300  		blockBytes = append(blockBytes, block.Bytes...)
   301  		block, rest = pem.Decode(rest)
   302  	}
   303  
   304  	rootCAs, err := x509.ParseCertificates(blockBytes)
   305  	if err != nil {
   306  		spiffeLog.Errorf("parse certificate from rootPEM got error: %v", err)
   307  		return fmt.Errorf("parse certificate from rootPEM got error: %v", err)
   308  	}
   309  
   310  	v.AddMapping(trustDomain, rootCAs)
   311  	return nil
   312  }
   313  
   314  // AddMappings merges a trust domain to certs map to the certPools map.
   315  func (v *PeerCertVerifier) AddMappings(certMap map[string][]*x509.Certificate) {
   316  	for trustDomain, certs := range certMap {
   317  		v.AddMapping(trustDomain, certs)
   318  	}
   319  }
   320  
   321  // VerifyPeerCert is an implementation of tls.Config.VerifyPeerCertificate.
   322  // It verifies the peer certificate using the root certificates associated with its trust domain.
   323  func (v *PeerCertVerifier) VerifyPeerCert(rawCerts [][]byte, _ [][]*x509.Certificate) error {
   324  	if len(rawCerts) == 0 {
   325  		// Peer doesn't present a certificate. Just skip. Other authn methods may be used.
   326  		return nil
   327  	}
   328  	var peerCert *x509.Certificate
   329  	intCertPool := x509.NewCertPool()
   330  	for id, rawCert := range rawCerts {
   331  		cert, err := x509.ParseCertificate(rawCert)
   332  		if err != nil {
   333  			return err
   334  		}
   335  		if id == 0 {
   336  			peerCert = cert
   337  		} else {
   338  			intCertPool.AddCert(cert)
   339  		}
   340  	}
   341  	if len(peerCert.URIs) != 1 {
   342  		return fmt.Errorf("peer certificate does not contain 1 URI type SAN, detected %d", len(peerCert.URIs))
   343  	}
   344  	trustDomain, err := GetTrustDomainFromURISAN(peerCert.URIs[0].String())
   345  	if err != nil {
   346  		return err
   347  	}
   348  	rootCertPool, ok := v.certPools[trustDomain]
   349  	if !ok {
   350  		return fmt.Errorf("no cert pool found for trust domain %s", trustDomain)
   351  	}
   352  
   353  	_, err = peerCert.Verify(x509.VerifyOptions{
   354  		Roots:         rootCertPool,
   355  		Intermediates: intCertPool,
   356  	})
   357  	return err
   358  }