Support SPIFFE mTLS between Traefik and Backend servers
This commit is contained in:
parent
33f0aed5ea
commit
b39ce8cc58
30 changed files with 736 additions and 24 deletions
|
@ -312,7 +312,7 @@ func TestRouterManager_Get(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
roundTripperManager := service.NewRoundTripperManager()
|
||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
|
||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||
|
@ -418,7 +418,7 @@ func TestAccessLog(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
roundTripperManager := service.NewRoundTripperManager()
|
||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
|
||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||
|
@ -713,7 +713,7 @@ func TestRuntimeConfiguration(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
roundTripperManager := service.NewRoundTripperManager()
|
||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
|
||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||
|
@ -788,7 +788,7 @@ func TestProviderOnMiddlewares(t *testing.T) {
|
|||
},
|
||||
})
|
||||
|
||||
roundTripperManager := service.NewRoundTripperManager()
|
||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
serviceManager := service.NewManager(rtConf.Services, nil, nil, roundTripperManager)
|
||||
middlewaresBuilder := middleware.NewBuilder(rtConf.Middlewares, serviceManager, nil)
|
||||
|
|
|
@ -48,7 +48,7 @@ func TestReuseService(t *testing.T) {
|
|||
),
|
||||
)
|
||||
|
||||
roundTripperManager := service.NewRoundTripperManager()
|
||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil)
|
||||
tlsManager := tls.NewManager()
|
||||
|
@ -184,7 +184,7 @@ func TestServerResponseEmptyBackend(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
roundTripperManager := service.NewRoundTripperManager()
|
||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil)
|
||||
tlsManager := tls.NewManager()
|
||||
|
@ -225,7 +225,7 @@ func TestInternalServices(t *testing.T) {
|
|||
),
|
||||
)
|
||||
|
||||
roundTripperManager := service.NewRoundTripperManager()
|
||||
roundTripperManager := service.NewRoundTripperManager(nil)
|
||||
roundTripperManager.Update(map[string]*dynamic.ServersTransport{"default@internal": {}})
|
||||
managerFactory := service.NewManagerFactory(staticConfig, nil, metrics.NewVoidRegistry(), roundTripperManager, nil)
|
||||
tlsManager := tls.NewManager()
|
||||
|
|
|
@ -11,6 +11,10 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
|
||||
"github.com/spiffe/go-spiffe/v2/spiffeid"
|
||||
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
|
||||
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
|
||||
"github.com/traefik/traefik/v2/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v2/pkg/log"
|
||||
traefiktls "github.com/traefik/traefik/v2/pkg/tls"
|
||||
|
@ -26,11 +30,18 @@ func (t *h2cTransportWrapper) RoundTrip(req *http.Request) (*http.Response, erro
|
|||
return t.Transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
// SpiffeX509Source allows to retrieve a x509 SVID and bundle.
|
||||
type SpiffeX509Source interface {
|
||||
x509svid.Source
|
||||
x509bundle.Source
|
||||
}
|
||||
|
||||
// NewRoundTripperManager creates a new RoundTripperManager.
|
||||
func NewRoundTripperManager() *RoundTripperManager {
|
||||
func NewRoundTripperManager(spiffeX509Source SpiffeX509Source) *RoundTripperManager {
|
||||
return &RoundTripperManager{
|
||||
roundTrippers: make(map[string]http.RoundTripper),
|
||||
configs: make(map[string]*dynamic.ServersTransport),
|
||||
roundTrippers: make(map[string]http.RoundTripper),
|
||||
configs: make(map[string]*dynamic.ServersTransport),
|
||||
spiffeX509Source: spiffeX509Source,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,6 +50,8 @@ type RoundTripperManager struct {
|
|||
rtLock sync.RWMutex
|
||||
roundTrippers map[string]http.RoundTripper
|
||||
configs map[string]*dynamic.ServersTransport
|
||||
|
||||
spiffeX509Source SpiffeX509Source
|
||||
}
|
||||
|
||||
// Update updates the roundtrippers configurations.
|
||||
|
@ -59,7 +72,7 @@ func (r *RoundTripperManager) Update(newConfigs map[string]*dynamic.ServersTrans
|
|||
}
|
||||
|
||||
var err error
|
||||
r.roundTrippers[configName], err = createRoundTripper(newConfig)
|
||||
r.roundTrippers[configName], err = r.createRoundTripper(newConfig)
|
||||
if err != nil {
|
||||
log.WithoutContext().Errorf("Could not configure HTTP Transport %s, fallback on default transport: %v", configName, err)
|
||||
r.roundTrippers[configName] = http.DefaultTransport
|
||||
|
@ -72,7 +85,7 @@ func (r *RoundTripperManager) Update(newConfigs map[string]*dynamic.ServersTrans
|
|||
}
|
||||
|
||||
var err error
|
||||
r.roundTrippers[newConfigName], err = createRoundTripper(newConfig)
|
||||
r.roundTrippers[newConfigName], err = r.createRoundTripper(newConfig)
|
||||
if err != nil {
|
||||
log.WithoutContext().Errorf("Could not configure HTTP Transport %s, fallback on default transport: %v", newConfigName, err)
|
||||
r.roundTrippers[newConfigName] = http.DefaultTransport
|
||||
|
@ -102,7 +115,7 @@ func (r *RoundTripperManager) Get(name string) (http.RoundTripper, error) {
|
|||
// For the settings that can't be configured in Traefik it uses the default http.Transport settings.
|
||||
// An exception to this is the MaxIdleConns setting as we only provide the option MaxIdleConnsPerHost in Traefik at this point in time.
|
||||
// Setting this value to the default of 100 could lead to confusing behavior and backwards compatibility issues.
|
||||
func createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error) {
|
||||
func (r *RoundTripperManager) createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error) {
|
||||
if cfg == nil {
|
||||
return nil, errors.New("no transport configuration given")
|
||||
}
|
||||
|
@ -132,7 +145,24 @@ func createRoundTripper(cfg *dynamic.ServersTransport) (http.RoundTripper, error
|
|||
transport.IdleConnTimeout = time.Duration(cfg.ForwardingTimeouts.IdleConnTimeout)
|
||||
}
|
||||
|
||||
if cfg.Spiffe != nil {
|
||||
if r.spiffeX509Source == nil {
|
||||
return nil, errors.New("SPIFFE is enabled for this transport, but not configured")
|
||||
}
|
||||
|
||||
spiffeAuthorizer, err := buildSpiffeAuthorizer(cfg.Spiffe)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to build SPIFFE authorizer: %w", err)
|
||||
}
|
||||
|
||||
transport.TLSClientConfig = tlsconfig.MTLSClientConfig(r.spiffeX509Source, r.spiffeX509Source, spiffeAuthorizer)
|
||||
}
|
||||
|
||||
if cfg.InsecureSkipVerify || len(cfg.RootCAs) > 0 || len(cfg.ServerName) > 0 || len(cfg.Certificates) > 0 || cfg.PeerCertURI != "" {
|
||||
if transport.TLSClientConfig != nil {
|
||||
return nil, errors.New("TLS and SPIFFE configuration cannot be defined at the same time")
|
||||
}
|
||||
|
||||
transport.TLSClientConfig = &tls.Config{
|
||||
ServerName: cfg.ServerName,
|
||||
InsecureSkipVerify: cfg.InsecureSkipVerify,
|
||||
|
@ -173,3 +203,31 @@ func createRootCACertPool(rootCAs []traefiktls.FileOrContent) *x509.CertPool {
|
|||
|
||||
return roots
|
||||
}
|
||||
|
||||
func buildSpiffeAuthorizer(cfg *dynamic.Spiffe) (tlsconfig.Authorizer, error) {
|
||||
switch {
|
||||
case len(cfg.IDs) > 0:
|
||||
spiffeIDs := make([]spiffeid.ID, 0, len(cfg.IDs))
|
||||
for _, rawID := range cfg.IDs {
|
||||
id, err := spiffeid.FromString(rawID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid SPIFFE ID: %w", err)
|
||||
}
|
||||
|
||||
spiffeIDs = append(spiffeIDs, id)
|
||||
}
|
||||
|
||||
return tlsconfig.AuthorizeOneOf(spiffeIDs...), nil
|
||||
|
||||
case cfg.TrustDomain != "":
|
||||
trustDomain, err := spiffeid.TrustDomainFromString(cfg.TrustDomain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid SPIFFE trust domain: %w", err)
|
||||
}
|
||||
|
||||
return tlsconfig.AuthorizeMemberOf(trustDomain), nil
|
||||
|
||||
default:
|
||||
return tlsconfig.AuthorizeAny(), nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/spiffe/go-spiffe/v2/bundle/x509bundle"
|
||||
"github.com/spiffe/go-spiffe/v2/spiffeid"
|
||||
"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
|
||||
"github.com/spiffe/go-spiffe/v2/svid/x509svid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v2/pkg/config/dynamic"
|
||||
|
@ -129,7 +139,7 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
|
|||
srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
srv.StartTLS()
|
||||
|
||||
rtManager := NewRoundTripperManager()
|
||||
rtManager := NewRoundTripperManager(nil)
|
||||
|
||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||
"test": {
|
||||
|
@ -197,7 +207,7 @@ func TestMTLS(t *testing.T) {
|
|||
}
|
||||
srv.StartTLS()
|
||||
|
||||
rtManager := NewRoundTripperManager()
|
||||
rtManager := NewRoundTripperManager(nil)
|
||||
|
||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||
"test": {
|
||||
|
@ -228,6 +238,141 @@ func TestMTLS(t *testing.T) {
|
|||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestSpiffeMTLS(t *testing.T) {
|
||||
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
trustDomain := spiffeid.RequireTrustDomainFromString("spiffe://traefik.test")
|
||||
|
||||
pki, err := newFakeSpiffePKI(trustDomain)
|
||||
require.NoError(t, err)
|
||||
|
||||
clientSVID, err := pki.genSVID(spiffeid.RequireFromPath(trustDomain, "/client"))
|
||||
require.NoError(t, err)
|
||||
|
||||
serverSVID, err := pki.genSVID(spiffeid.RequireFromPath(trustDomain, "/server"))
|
||||
require.NoError(t, err)
|
||||
|
||||
serverSource := fakeSpiffeSource{
|
||||
svid: serverSVID,
|
||||
bundle: pki.bundle,
|
||||
}
|
||||
|
||||
// go-spiffe's `tlsconfig.MTLSServerConfig` (that should be used here) does not set a certificate on
|
||||
// the returned `tls.Config` and relies instead on `GetCertificate` being always called.
|
||||
// But it turns out that `StartTLS` from `httptest.Server`, enforces a default certificate
|
||||
// if no certificate is previously set on the configured TLS config.
|
||||
// It makes the test server always serve the httptest default certificate, and not the SPIFFE certificate,
|
||||
// as GetCertificate is in that case never called (there's a default cert, and SNI is not used).
|
||||
// To bypass this issue, we're manually extracting the server ceritificate from the server SVID
|
||||
// and use another initialization method that forces serving the server SPIFFE certificate.
|
||||
serverCert, err := tlsconfig.GetCertificate(&serverSource)(nil)
|
||||
require.NoError(t, err)
|
||||
srv.TLS = tlsconfig.MTLSWebServerConfig(
|
||||
serverCert,
|
||||
&serverSource,
|
||||
tlsconfig.AuthorizeAny(),
|
||||
)
|
||||
srv.StartTLS()
|
||||
defer srv.Close()
|
||||
|
||||
clientSource := fakeSpiffeSource{
|
||||
svid: clientSVID,
|
||||
bundle: pki.bundle,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
config dynamic.Spiffe
|
||||
clientSource SpiffeX509Source
|
||||
wantStatusCode int
|
||||
wantErrorMessage string
|
||||
}{
|
||||
{
|
||||
desc: "supports SPIFFE mTLS",
|
||||
config: dynamic.Spiffe{},
|
||||
clientSource: &clientSource,
|
||||
wantStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "allows expected server SPIFFE ID",
|
||||
config: dynamic.Spiffe{
|
||||
IDs: []string{"spiffe://traefik.test/server"},
|
||||
},
|
||||
clientSource: &clientSource,
|
||||
wantStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "blocks unexpected server SPIFFE ID",
|
||||
config: dynamic.Spiffe{
|
||||
IDs: []string{"spiffe://traefik.test/not-server"},
|
||||
},
|
||||
clientSource: &clientSource,
|
||||
wantErrorMessage: `unexpected ID "spiffe://traefik.test/server"`,
|
||||
},
|
||||
{
|
||||
desc: "allows expected server trust domain",
|
||||
config: dynamic.Spiffe{
|
||||
TrustDomain: "spiffe://traefik.test",
|
||||
},
|
||||
clientSource: &clientSource,
|
||||
wantStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "denies unexpected server trust domain",
|
||||
config: dynamic.Spiffe{
|
||||
TrustDomain: "spiffe://not-traefik.test",
|
||||
},
|
||||
clientSource: &clientSource,
|
||||
wantErrorMessage: `unexpected trust domain "traefik.test"`,
|
||||
},
|
||||
{
|
||||
desc: "spiffe IDs allowlist takes precedence",
|
||||
config: dynamic.Spiffe{
|
||||
IDs: []string{"spiffe://traefik.test/not-server"},
|
||||
TrustDomain: "spiffe://not-traefik.test",
|
||||
},
|
||||
clientSource: &clientSource,
|
||||
wantErrorMessage: `unexpected ID "spiffe://traefik.test/server"`,
|
||||
},
|
||||
{
|
||||
desc: "raises an error when spiffe is enabled on the transport but no workloadapi address is given",
|
||||
config: dynamic.Spiffe{},
|
||||
clientSource: nil,
|
||||
wantErrorMessage: `remote error: tls: bad certificate`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
rtManager := NewRoundTripperManager(test.clientSource)
|
||||
|
||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||
"test": {
|
||||
Spiffe: &test.config,
|
||||
},
|
||||
}
|
||||
|
||||
rtManager.Update(dynamicConf)
|
||||
|
||||
tr, err := rtManager.Get("test")
|
||||
require.NoError(t, err)
|
||||
|
||||
client := http.Client{Transport: tr}
|
||||
|
||||
resp, err := client.Get(srv.URL)
|
||||
if test.wantErrorMessage != "" {
|
||||
assert.ErrorContains(t, err, test.wantErrorMessage)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.wantStatusCode, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisableHTTP2(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
|
@ -269,7 +414,7 @@ func TestDisableHTTP2(t *testing.T) {
|
|||
srv.EnableHTTP2 = test.serverHTTP2
|
||||
srv.StartTLS()
|
||||
|
||||
rtManager := NewRoundTripperManager()
|
||||
rtManager := NewRoundTripperManager(nil)
|
||||
|
||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||
"test": {
|
||||
|
@ -293,3 +438,116 @@ func TestDisableHTTP2(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fakeSpiffePKI simulates a SPIFFE aware PKI and allows generating multiple valid SVIDs.
|
||||
type fakeSpiffePKI struct {
|
||||
caPrivateKey *rsa.PrivateKey
|
||||
|
||||
bundle *x509bundle.Bundle
|
||||
}
|
||||
|
||||
func newFakeSpiffePKI(trustDomain spiffeid.TrustDomain) (fakeSpiffePKI, error) {
|
||||
caPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return fakeSpiffePKI{}, err
|
||||
}
|
||||
|
||||
caTemplate := x509.Certificate{
|
||||
SerialNumber: big.NewInt(2000),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"spiffe"},
|
||||
},
|
||||
URIs: []*url.URL{spiffeid.RequireFromPath(trustDomain, "/ca").URL()},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
SubjectKeyId: []byte("ca"),
|
||||
KeyUsage: x509.KeyUsageCertSign |
|
||||
x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
PublicKey: caPrivateKey.Public(),
|
||||
}
|
||||
if err != nil {
|
||||
return fakeSpiffePKI{}, err
|
||||
}
|
||||
|
||||
caCertDER, err := x509.CreateCertificate(
|
||||
rand.Reader,
|
||||
&caTemplate,
|
||||
&caTemplate,
|
||||
caPrivateKey.Public(),
|
||||
caPrivateKey,
|
||||
)
|
||||
if err != nil {
|
||||
return fakeSpiffePKI{}, err
|
||||
}
|
||||
|
||||
bundle, err := x509bundle.ParseRaw(
|
||||
trustDomain,
|
||||
caCertDER,
|
||||
)
|
||||
if err != nil {
|
||||
return fakeSpiffePKI{}, err
|
||||
}
|
||||
|
||||
return fakeSpiffePKI{
|
||||
bundle: bundle,
|
||||
caPrivateKey: caPrivateKey,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fakeSpiffePKI) genSVID(id spiffeid.ID) (*x509svid.SVID, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(200001),
|
||||
URIs: []*url.URL{id.URL()},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
SubjectKeyId: []byte("svid"),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment |
|
||||
x509.KeyUsageKeyAgreement |
|
||||
x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageServerAuth,
|
||||
x509.ExtKeyUsageClientAuth,
|
||||
},
|
||||
BasicConstraintsValid: true,
|
||||
PublicKey: privateKey.PublicKey,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(
|
||||
rand.Reader,
|
||||
&template,
|
||||
f.bundle.X509Authorities()[0],
|
||||
privateKey.Public(),
|
||||
f.caPrivateKey,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyPKCS8, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return x509svid.ParseRaw(certDER, keyPKCS8)
|
||||
}
|
||||
|
||||
// fakeSpiffeSource allows retrieving staticly an SVID and its associated bundle.
|
||||
type fakeSpiffeSource struct {
|
||||
bundle *x509bundle.Bundle
|
||||
svid *x509svid.SVID
|
||||
}
|
||||
|
||||
func (s *fakeSpiffeSource) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) {
|
||||
return s.bundle, nil
|
||||
}
|
||||
|
||||
func (s *fakeSpiffeSource) GetX509SVID() (*x509svid.SVID, error) {
|
||||
return s.svid, nil
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue