blob: 7d093e8cf33fd80bd0b0d4987d6af0e3d7f4596f [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package spiffe
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
import (
"gopkg.in/square/go-jose.v2"
"istio.io/pkg/log"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/config/constants"
)
const (
Scheme = "spiffe"
URIPrefix = Scheme + "://"
URIPrefixLen = len(URIPrefix)
// The default SPIFFE URL value for trust domain
defaultTrustDomain = constants.DefaultKubernetesDomain
ServiceAccountSegment = "sa"
NamespaceSegment = "ns"
)
var (
trustDomain = defaultTrustDomain
trustDomainMutex sync.RWMutex
firstRetryBackOffTime = time.Millisecond * 50
spiffeLog = log.RegisterScope("spiffe", "SPIFFE library logging", 0)
totalRetryTimeout = time.Second * 10
)
type Identity struct {
TrustDomain string
Namespace string
ServiceAccount string
}
func ParseIdentity(s string) (Identity, error) {
if !strings.HasPrefix(s, URIPrefix) {
return Identity{}, fmt.Errorf("identity is not a spiffe format: %v", s)
}
split := strings.Split(s[URIPrefixLen:], "/")
if len(split) != 5 {
return Identity{}, fmt.Errorf("identity is not a spiffe format: %v", s)
}
if split[1] != NamespaceSegment || split[3] != ServiceAccountSegment {
return Identity{}, fmt.Errorf("identity is not a spiffe format: %v", s)
}
return Identity{
TrustDomain: split[0],
Namespace: split[2],
ServiceAccount: split[4],
}, nil
}
func (i Identity) String() string {
return URIPrefix + i.TrustDomain + "/ns/" + i.Namespace + "/sa/" + i.ServiceAccount
}
type bundleDoc struct {
jose.JSONWebKeySet
Sequence uint64 `json:"spiffe_sequence,omitempty"`
RefreshHint int `json:"spiffe_refresh_hint,omitempty"`
}
func SetTrustDomain(value string) {
// Replace special characters in spiffe
v := strings.Replace(value, "@", ".", -1)
trustDomainMutex.Lock()
trustDomain = v
trustDomainMutex.Unlock()
}
func GetTrustDomain() string {
trustDomainMutex.RLock()
defer trustDomainMutex.RUnlock()
return trustDomain
}
// GenSpiffeURI returns the formatted uri(SPIFFE format for now) for the certificate.
func GenSpiffeURI(ns, serviceAccount string) (string, error) {
var err error
if ns == "" || serviceAccount == "" {
err = fmt.Errorf(
"namespace or service account empty for SPIFFE uri ns=%v serviceAccount=%v", ns, serviceAccount)
}
return URIPrefix + GetTrustDomain() + "/ns/" + ns + "/sa/" + serviceAccount, err
}
// MustGenSpiffeURI returns the formatted uri(SPIFFE format for now) for the certificate and logs if there was an error.
func MustGenSpiffeURI(ns, serviceAccount string) string {
uri, err := GenSpiffeURI(ns, serviceAccount)
if err != nil {
spiffeLog.Debug(err.Error())
}
return uri
}
// ExpandWithTrustDomains expands a given spiffe identities, plus a list of truts domain aliases.
// We ensure the returned list does not contain duplicates; the original input is always retained.
// For example,
// ExpandWithTrustDomains({"spiffe://td1/ns/def/sa/def"}, {"td1", "td2"}) returns
//
// {"spiffe://td1/ns/def/sa/def", "spiffe://td2/ns/def/sa/def"}.
//
// ExpandWithTrustDomains({"spiffe://td1/ns/def/sa/a", "spiffe://td1/ns/def/sa/b"}, {"td2"}) returns
//
// {"spiffe://td1/ns/def/sa/a", "spiffe://td2/ns/def/sa/a", "spiffe://td1/ns/def/sa/b", "spiffe://td2/ns/def/sa/b"}.
func ExpandWithTrustDomains(spiffeIdentities, trustDomainAliases []string) map[string]struct{} {
out := map[string]struct{}{}
for _, id := range spiffeIdentities {
out[id] = struct{}{}
// Expand with aliases set.
m, err := ParseIdentity(id)
if err != nil {
spiffeLog.Errorf("Failed to extract SPIFFE trust domain from %v: %v", id, err)
continue
}
for _, td := range trustDomainAliases {
m.TrustDomain = td
out[m.String()] = struct{}{}
}
}
return out
}
// GetTrustDomainFromURISAN extracts the trust domain part from the URI SAN in the X.509 certificate.
func GetTrustDomainFromURISAN(uriSan string) (string, error) {
parsed, err := ParseIdentity(uriSan)
if err != nil {
return "", fmt.Errorf("failed to parse URI SAN %s. Error: %v", uriSan, err)
}
return parsed.TrustDomain, nil
}
// RetrieveSpiffeBundleRootCertsFromStringInput retrieves the trusted CA certificates from a list of SPIFFE bundle endpoints.
// It can use the system cert pool and the supplied certificates to validate the endpoints.
// The input endpointTuples should be in the format of:
// "foo|URL1||bar|URL2||baz|URL3..."
func RetrieveSpiffeBundleRootCertsFromStringInput(inputString string, extraTrustedCerts []*x509.Certificate) (
map[string][]*x509.Certificate, error) {
spiffeLog.Infof("Processing SPIFFE bundle configuration: %v", inputString)
config := make(map[string]string)
tuples := strings.Split(inputString, "||")
for _, tuple := range tuples {
items := strings.Split(tuple, "|")
if len(items) != 2 {
return nil, fmt.Errorf("config is invalid: %v. Expected <trustdomain>|<url>", tuple)
}
trustDomain := items[0]
endpoint := items[1]
config[trustDomain] = endpoint
}
caCertPool, err := x509.SystemCertPool()
if err != nil {
return nil, fmt.Errorf("failed to get SystemCertPool: %v", err)
}
for _, cert := range extraTrustedCerts {
caCertPool.AddCert(cert)
}
return RetrieveSpiffeBundleRootCerts(config, caCertPool, totalRetryTimeout)
}
// RetrieveSpiffeBundleRootCerts retrieves the trusted CA certificates from a list of SPIFFE bundle endpoints.
// It can use the system cert pool and the supplied certificates to validate the endpoints.
func RetrieveSpiffeBundleRootCerts(config map[string]string, caCertPool *x509.CertPool, retryTimeout time.Duration) (
map[string][]*x509.Certificate, error) {
httpClient := &http.Client{
Timeout: time.Second * 10,
}
ret := map[string][]*x509.Certificate{}
for trustdomain, endpoint := range config {
if !strings.HasPrefix(endpoint, "https://") {
endpoint = "https://" + endpoint
}
u, err := url.Parse(endpoint)
if err != nil {
return nil, fmt.Errorf("failed to split the SPIFFE bundle URL: %v", err)
}
config := &tls.Config{
ServerName: u.Hostname(),
RootCAs: caCertPool,
}
httpClient.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
TLSClientConfig: config,
DialContext: (&net.Dialer{
Timeout: time.Second * 10,
}).DialContext,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
retryBackoffTime := firstRetryBackOffTime
startTime := time.Now()
var resp *http.Response
for {
resp, err = httpClient.Get(endpoint)
var errMsg string
if err != nil {
errMsg = fmt.Sprintf("Calling %s failed with error: %v", endpoint, err)
} else if resp == nil {
errMsg = fmt.Sprintf("Calling %s failed with nil response", endpoint)
} else if resp.StatusCode != http.StatusOK {
b := make([]byte, 1024)
n, _ := resp.Body.Read(b)
errMsg = fmt.Sprintf("Calling %s failed with unexpected status: %v, fetching bundle: %s",
endpoint, resp.StatusCode, string(b[:n]))
} else {
break
}
if startTime.Add(retryTimeout).Before(time.Now()) {
return nil, fmt.Errorf("exhausted retries to fetch the SPIFFE bundle %s from url %s. Latest error: %v",
trustdomain, endpoint, errMsg)
}
spiffeLog.Warnf("%s, retry in %v", errMsg, retryBackoffTime)
time.Sleep(retryBackoffTime)
retryBackoffTime *= 2 // Exponentially increase the retry backoff time.
}
defer resp.Body.Close()
doc := new(bundleDoc)
if err := json.NewDecoder(resp.Body).Decode(doc); err != nil {
return nil, fmt.Errorf("trust domain [%s] at URL [%s] failed to decode bundle: %v", trustdomain, endpoint, err)
}
var cert *x509.Certificate
for i, key := range doc.Keys {
if key.Use == "x509-svid" {
if len(key.Certificates) != 1 {
return nil, fmt.Errorf("trust domain [%s] at URL [%s] expected 1 certificate in x509-svid entry %d; got %d",
trustdomain, endpoint, i, len(key.Certificates))
}
cert = key.Certificates[0]
}
}
if cert == nil {
return nil, fmt.Errorf("trust domain [%s] at URL [%s] does not provide a X509 SVID", trustdomain, endpoint)
}
if certs, ok := ret[trustdomain]; ok {
ret[trustdomain] = append(certs, cert)
} else {
ret[trustdomain] = []*x509.Certificate{cert}
}
}
for trustDomain, certs := range ret {
spiffeLog.Infof("Loaded SPIFFE trust bundle for: %v, containing %d certs", trustDomain, len(certs))
}
return ret, nil
}
// PeerCertVerifier is an instance to verify the peer certificate in the SPIFFE way using the retrieved root certificates.
type PeerCertVerifier struct {
generalCertPool *x509.CertPool
certPools map[string]*x509.CertPool
}
// NewPeerCertVerifier returns a new PeerCertVerifier.
func NewPeerCertVerifier() *PeerCertVerifier {
return &PeerCertVerifier{
generalCertPool: x509.NewCertPool(),
certPools: make(map[string]*x509.CertPool),
}
}
// GetGeneralCertPool returns generalCertPool containing all root certs.
func (v *PeerCertVerifier) GetGeneralCertPool() *x509.CertPool {
return v.generalCertPool
}
// AddMapping adds a new trust domain to certificates mapping to the certPools map.
func (v *PeerCertVerifier) AddMapping(trustDomain string, certs []*x509.Certificate) {
if v.certPools[trustDomain] == nil {
v.certPools[trustDomain] = x509.NewCertPool()
}
for _, cert := range certs {
v.certPools[trustDomain].AddCert(cert)
v.generalCertPool.AddCert(cert)
}
spiffeLog.Infof("Added %d certs to trust domain %s in peer cert verifier", len(certs), trustDomain)
}
// AddMappingFromPEM adds multiple RootCA's to the spiffe Trust bundle in the trustDomain namespace
func (v *PeerCertVerifier) AddMappingFromPEM(trustDomain string, rootCertBytes []byte) error {
block, rest := pem.Decode(rootCertBytes)
var blockBytes []byte
// Loop while there are no block are found
for block != nil {
blockBytes = append(blockBytes, block.Bytes...)
block, rest = pem.Decode(rest)
}
rootCAs, err := x509.ParseCertificates(blockBytes)
if err != nil {
spiffeLog.Errorf("parse certificate from rootPEM got error: %v", err)
return fmt.Errorf("parse certificate from rootPEM got error: %v", err)
}
v.AddMapping(trustDomain, rootCAs)
return nil
}
// AddMappings merges a trust domain to certs map to the certPools map.
func (v *PeerCertVerifier) AddMappings(certMap map[string][]*x509.Certificate) {
for trustDomain, certs := range certMap {
v.AddMapping(trustDomain, certs)
}
}
// VerifyPeerCert is an implementation of tls.Config.VerifyPeerCertificate.
// It verifies the peer certificate using the root certificates associated with its trust domain.
func (v *PeerCertVerifier) VerifyPeerCert(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
// Peer doesn't present a certificate. Just skip. Other authn methods may be used.
return nil
}
var peerCert *x509.Certificate
intCertPool := x509.NewCertPool()
for id, rawCert := range rawCerts {
cert, err := x509.ParseCertificate(rawCert)
if err != nil {
return err
}
if id == 0 {
peerCert = cert
} else {
intCertPool.AddCert(cert)
}
}
if len(peerCert.URIs) != 1 {
return fmt.Errorf("peer certificate does not contain 1 URI type SAN, detected %d", len(peerCert.URIs))
}
trustDomain, err := GetTrustDomainFromURISAN(peerCert.URIs[0].String())
if err != nil {
return err
}
rootCertPool, ok := v.certPools[trustDomain]
if !ok {
return fmt.Errorf("no cert pool found for trust domain %s", trustDomain)
}
_, err = peerCert.Verify(x509.VerifyOptions{
Roots: rootCertPool,
Intermediates: intCertPool,
})
return err
}