diff --git a/api/api.go b/api/api.go index e427bee3..563e65ea 100644 --- a/api/api.go +++ b/api/api.go @@ -22,10 +22,11 @@ type Authority interface { GetTLSOptions() *tlsutil.TLSOptions Root(shasum string) (*x509.Certificate, error) Sign(cr *x509.CertificateRequest, signOpts authority.SignOptions, extraOpts ...interface{}) (*x509.Certificate, *x509.Certificate, error) - Renew(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) + Renew(peer *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) GetProvisioners(cursor string, limit int) ([]*authority.Provisioner, string, error) GetEncryptedKey(kid string) (string, error) - GetFederation(cert *x509.Certificate) ([]*x509.Certificate, error) + GetRoots(peer *x509.Certificate) (federation []*x509.Certificate, err error) + GetFederation(peer *x509.Certificate) ([]*x509.Certificate, error) } // Certificate wraps a *x509.Certificate and adds the json.Marshaler interface. @@ -187,6 +188,11 @@ type SignResponse struct { TLS *tls.ConnectionState `json:"-"` } +// RootsResponse is the response object of the roots request. +type RootsResponse struct { + Certificates []Certificate `json:"crts"` +} + // FederationResponse is the response object of the federation request. type FederationResponse struct { Certificates []Certificate `json:"crts"` @@ -211,6 +217,7 @@ func (h *caHandler) Route(r Router) { r.MethodFunc("POST", "/renew", h.Renew) r.MethodFunc("GET", "/provisioners", h.Provisioners) r.MethodFunc("GET", "/provisioners/{kid}/encrypted-key", h.ProvisionerKey) + r.MethodFunc("GET", "/roots", h.Roots) r.MethodFunc("GET", "/federation", h.Federation) // For compatibility with old code: r.MethodFunc("POST", "/re-sign", h.Renew) @@ -327,7 +334,33 @@ func (h *caHandler) ProvisionerKey(w http.ResponseWriter, r *http.Request) { JSON(w, &ProvisionerKeyResponse{key}) } -// Federation returns all the public certificates in the federation. +// Roots returns all the root certificates for the CA. It requires a valid TLS +// client. +func (h *caHandler) Roots(w http.ResponseWriter, r *http.Request) { + if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { + WriteError(w, BadRequest(errors.New("missing peer certificate"))) + return + } + + roots, err := h.Authority.GetRoots(r.TLS.PeerCertificates[0]) + if err != nil { + WriteError(w, Forbidden(err)) + return + } + + certs := make([]Certificate, len(roots)) + for i := range roots { + certs[i] = Certificate{roots[i]} + } + + w.WriteHeader(http.StatusCreated) + JSON(w, &RootsResponse{ + Certificates: certs, + }) +} + +// Federation returns all the public certificates in the federation. It requires +// a valid TLS client. func (h *caHandler) Federation(w http.ResponseWriter, r *http.Request) { if r.TLS == nil || len(r.TLS.PeerCertificates) == 0 { WriteError(w, BadRequest(errors.New("missing peer certificate"))) diff --git a/api/api_test.go b/api/api_test.go index 82e12c8c..0a9eb120 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -392,6 +392,7 @@ type mockAuthority struct { renew func(cert *x509.Certificate) (*x509.Certificate, *x509.Certificate, error) getProvisioners func(nextCursor string, limit int) ([]*authority.Provisioner, string, error) getEncryptedKey func(kid string) (string, error) + getRoots func(cert *x509.Certificate) ([]*x509.Certificate, error) getFederation func(cert *x509.Certificate) ([]*x509.Certificate, error) } @@ -444,6 +445,13 @@ func (m *mockAuthority) GetEncryptedKey(kid string) (string, error) { return m.ret1.(string), m.err } +func (m *mockAuthority) GetRoots(cert *x509.Certificate) ([]*x509.Certificate, error) { + if m.getFederation != nil { + return m.getRoots(cert) + } + return m.ret1.([]*x509.Certificate), m.err +} + func (m *mockAuthority) GetFederation(cert *x509.Certificate) ([]*x509.Certificate, error) { if m.getFederation != nil { return m.getFederation(cert) @@ -821,6 +829,53 @@ func Test_caHandler_ProvisionerKey(t *testing.T) { } } +func Test_caHandler_Roots(t *testing.T) { + cs := &tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)}, + } + tests := []struct { + name string + tls *tls.ConnectionState + cert *x509.Certificate + root *x509.Certificate + err error + statusCode int + }{ + {"ok", cs, parseCertificate(certPEM), parseCertificate(rootPEM), nil, http.StatusCreated}, + {"no tls", nil, nil, nil, nil, http.StatusBadRequest}, + {"no peer certificates", &tls.ConnectionState{}, nil, nil, nil, http.StatusBadRequest}, + {"renew error", cs, nil, nil, fmt.Errorf("an error"), http.StatusForbidden}, + } + + expected := []byte(`{"crts":["` + strings.Replace(rootPEM, "\n", `\n`, -1) + `\n"]}`) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := New(&mockAuthority{ret1: []*x509.Certificate{tt.root}, err: tt.err}).(*caHandler) + req := httptest.NewRequest("GET", "http://example.com/roots", nil) + req.TLS = tt.tls + w := httptest.NewRecorder() + h.Roots(w, req) + res := w.Result() + + if res.StatusCode != tt.statusCode { + t.Errorf("caHandler.Roots StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + } + + body, err := ioutil.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Errorf("caHandler.Roots unexpected error = %v", err) + } + if tt.statusCode < http.StatusBadRequest { + if !bytes.Equal(bytes.TrimSpace(body), expected) { + t.Errorf("caHandler.Roots Body = %s, wants %s", body, expected) + } + } + }) + } +} + func Test_caHandler_Federation(t *testing.T) { cs := &tls.ConnectionState{ PeerCertificates: []*x509.Certificate{parseCertificate(certPEM)}, @@ -851,17 +906,17 @@ func Test_caHandler_Federation(t *testing.T) { res := w.Result() if res.StatusCode != tt.statusCode { - t.Errorf("caHandler.Root StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) + t.Errorf("caHandler.Federation StatusCode = %d, wants %d", res.StatusCode, tt.statusCode) } body, err := ioutil.ReadAll(res.Body) res.Body.Close() if err != nil { - t.Errorf("caHandler.Root unexpected error = %v", err) + t.Errorf("caHandler.Federation unexpected error = %v", err) } if tt.statusCode < http.StatusBadRequest { if !bytes.Equal(bytes.TrimSpace(body), expected) { - t.Errorf("caHandler.Root Body = %s, wants %s", body, expected) + t.Errorf("caHandler.Federation Body = %s, wants %s", body, expected) } } }) diff --git a/authority/root.go b/authority/root.go index d041ae8f..98974904 100644 --- a/authority/root.go +++ b/authority/root.go @@ -33,6 +33,15 @@ func (a *Authority) GetRootCertificates() []*x509.Certificate { return a.rootX509Certs } +// GetRoots returns all the root certificates for this CA. +func (a *Authority) GetRoots(peer *x509.Certificate) (federation []*x509.Certificate, err error) { + // Check step provisioner extensions + if err := a.authorizeRenewal(peer); err != nil { + return nil, err + } + return a.rootX509Certs, nil +} + // GetFederation returns all the root certificates in the federation. func (a *Authority) GetFederation(peer *x509.Certificate) (federation []*x509.Certificate, err error) { // Check step provisioner extensions diff --git a/ca/client.go b/ca/client.go index 374a68ff..b8aab67f 100644 --- a/ca/client.go +++ b/ca/client.go @@ -237,9 +237,10 @@ func WithProvisionerLimit(limit int) ProvisionerOption { // Client implements an HTTP client for the CA server. type Client struct { - client *http.Client - endpoint *url.URL - certPool *x509.CertPool + client *http.Client + endpoint *url.URL + certPool *x509.CertPool + cachedSign *api.SignResponse } // NewClient creates a new Client with the given endpoint and options. @@ -413,6 +414,25 @@ func (c *Client) ProvisionerKey(kid string) (*api.ProvisionerKeyResponse, error) return &key, nil } +// Roots performs the get roots request to the CA and returns the +// api.RootsResponse struct. +func (c *Client) Roots(tr http.RoundTripper) (*api.RootsResponse, error) { + u := c.endpoint.ResolveReference(&url.URL{Path: "/roots"}) + client := &http.Client{Transport: tr} + resp, err := client.Get(u.String()) + if err != nil { + return nil, errors.Wrapf(err, "client GET %s failed", u) + } + if resp.StatusCode >= 400 { + return nil, readError(resp.Body) + } + var federation api.RootsResponse + if err := readJSON(resp.Body, &federation); err != nil { + return nil, errors.Wrapf(err, "error reading %s", u) + } + return &federation, nil +} + // Federation performs the get federation request to the CA and returns the // api.FederationResponse struct. func (c *Client) Federation(tr http.RoundTripper) (*api.FederationResponse, error) { diff --git a/ca/client_test.go b/ca/client_test.go index 138b0d7d..0ec6324b 100644 --- a/ca/client_test.go +++ b/ca/client_test.go @@ -512,6 +512,67 @@ func TestClient_ProvisionerKey(t *testing.T) { } } +func TestClient_Roots(t *testing.T) { + ok := &api.RootsResponse{ + Certificates: []api.Certificate{ + {Certificate: parseCertificate(rootPEM)}, + }, + } + unauthorized := api.Unauthorized(fmt.Errorf("Unauthorized")) + badRequest := api.BadRequest(fmt.Errorf("Bad Request")) + + tests := []struct { + name string + response interface{} + responseCode int + wantErr bool + }{ + {"ok", ok, 200, false}, + {"unauthorized", unauthorized, 401, true}, + {"empty request", badRequest, 403, true}, + {"nil request", badRequest, 403, true}, + } + + srv := httptest.NewServer(nil) + defer srv.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := NewClient(srv.URL, WithTransport(http.DefaultTransport)) + if err != nil { + t.Errorf("NewClient() error = %v", err) + return + } + + srv.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(tt.responseCode) + api.JSON(w, tt.response) + }) + + got, err := c.Roots(nil) + if (err != nil) != tt.wantErr { + fmt.Printf("%+v", err) + t.Errorf("Client.Roots() error = %v, wantErr %v", err, tt.wantErr) + return + } + + switch { + case err != nil: + if got != nil { + t.Errorf("Client.Roots() = %v, want nil", got) + } + if !reflect.DeepEqual(err, tt.response) { + t.Errorf("Client.Roots() error = %v, want %v", err, tt.response) + } + default: + if !reflect.DeepEqual(got, tt.response) { + t.Errorf("Client.Roots() = %v, want %v", got, tt.response) + } + } + }) + } +} + func TestClient_Federation(t *testing.T) { ok := &api.FederationResponse{ Certificates: []api.Certificate{ diff --git a/ca/testdata/ca.json b/ca/testdata/ca.json index d61a8c49..f5484a7c 100644 --- a/ca/testdata/ca.json +++ b/ca/testdata/ca.json @@ -1,5 +1,6 @@ { "root": "../ca/testdata/secrets/root_ca.crt", + "federatedRoots": ["../ca/testdata/secrets/federated_ca.crt"], "crt": "../ca/testdata/secrets/intermediate_ca.crt", "key": "../ca/testdata/secrets/intermediate_ca_key", "password": "password", diff --git a/ca/testdata/secrets/federated_ca.crt b/ca/testdata/secrets/federated_ca.crt new file mode 100644 index 00000000..87cd5650 --- /dev/null +++ b/ca/testdata/secrets/federated_ca.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBfTCCASKgAwIBAgIRAJPUE0MTA+fMz6f6i/XYmTwwCgYIKoZIzj0EAwIwHDEa +MBgGA1UEAxMRU21hbGxzdGVwIFJvb3QgQ0EwHhcNMTkwMTA3MjAxMTMwWhcNMjkw +MTA0MjAxMTMwWjAcMRowGAYDVQQDExFTbWFsbHN0ZXAgUm9vdCBDQTBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABCOH/PGThn0cMOGDeqDxb22olsdCm8hVdyW9cHQL +jfIYAqpWNh9f7E5umlnxkOy6OEROTtpq7etzfBbzb52loVWjRTBDMA4GA1UdDwEB +/wQEAwIBpjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBSAjOUIvGH9GXAR +qfLglAc6issAgTAKBggqhkjOPQQDAgNJADBGAiEAjs0yjbQ/9dmGoUn7JS3lE83z +YlnXZ0fHdeNakkIKhQICIQCUENhGZp63pMtm3ipgwp91EM0T7YtKgrFNvDekqufc +Sw== +-----END CERTIFICATE----- diff --git a/ca/tls.go b/ca/tls.go index bef8e553..22eb667b 100644 --- a/ca/tls.go +++ b/ca/tls.go @@ -41,7 +41,7 @@ func (c *Client) GetClientTLSConfig(ctx context.Context, sign *api.SignResponse, } // Apply options if given - if err := setTLSOptions(c, tlsConfig, options); err != nil { + if err := setTLSOptions(c, sign, pk, tlsConfig, options); err != nil { return nil, err } @@ -87,7 +87,7 @@ func (c *Client) GetServerTLSConfig(ctx context.Context, sign *api.SignResponse, } // Apply options if given - if err := setTLSOptions(c, tlsConfig, options); err != nil { + if err := setTLSOptions(c, sign, pk, tlsConfig, options); err != nil { return nil, err } diff --git a/ca/tls_options.go b/ca/tls_options.go index b1cd4696..26eae156 100644 --- a/ca/tls_options.go +++ b/ca/tls_options.go @@ -1,28 +1,57 @@ package ca import ( + "crypto" "crypto/tls" "crypto/x509" + "net/http" + + "github.com/smallstep/certificates/api" ) // TLSOption defines the type of a function that modifies a tls.Config. -type TLSOption func(c *Client, config *tls.Config) error +type TLSOption func(c *Client, tr http.RoundTripper, config *tls.Config) error // setTLSOptions takes one or more option function and applies them in order to // a tls.Config. -func setTLSOptions(c *Client, config *tls.Config, options []TLSOption) error { +func setTLSOptions(c *Client, sign *api.SignResponse, pk crypto.PrivateKey, config *tls.Config, options []TLSOption) error { + tr, err := getTLSOptionsTransport(sign, pk) + if err != nil { + return err + } + for _, opt := range options { - if err := opt(c, config); err != nil { + if err := opt(c, tr, config); err != nil { return err } } return nil } +// getTLSOptionsTransport is the transport used by TLSOptions. It is used to get +// root certificates using a mTLS connection with the CA. +func getTLSOptionsTransport(sign *api.SignResponse, pk crypto.PrivateKey) (http.RoundTripper, error) { + cert, err := TLSCertificate(sign, pk) + if err != nil { + return nil, err + } + + // Build default transport with fixed certificate + tlsConfig := getDefaultTLSConfig(sign) + tlsConfig.Certificates = []tls.Certificate{*cert} + tlsConfig.PreferServerCipherSuites = true + // Build RootCAs with given root certificate + if pool := getCertPool(sign); pool != nil { + tlsConfig.RootCAs = pool + } + + return getDefaultTransport(tlsConfig) +} + // RequireAndVerifyClientCert is a tls.Config option used on servers to enforce // a valid TLS client certificate. This is the default option for mTLS servers. func RequireAndVerifyClientCert() TLSOption { - return func(_ *Client, config *tls.Config) error { + return func(_ *Client, _ http.RoundTripper, config *tls.Config) error { config.ClientAuth = tls.RequireAndVerifyClientCert return nil } @@ -31,7 +60,7 @@ func RequireAndVerifyClientCert() TLSOption { // VerifyClientCertIfGiven is a tls.Config option used on on servers to validate // a TLS client certificate if it is provided. It does not requires a certificate. func VerifyClientCertIfGiven() TLSOption { - return func(_ *Client, config *tls.Config) error { + return func(_ *Client, _ http.RoundTripper, config *tls.Config) error { config.ClientAuth = tls.VerifyClientCertIfGiven return nil } @@ -41,7 +70,7 @@ func VerifyClientCertIfGiven() TLSOption { // defines the set of root certificate authorities that clients use when // verifying server certificates. func AddRootCA(cert *x509.Certificate) TLSOption { - return func(_ *Client, config *tls.Config) error { + return func(_ *Client, _ http.RoundTripper, config *tls.Config) error { if config.RootCAs == nil { config.RootCAs = x509.NewCertPool() } @@ -54,7 +83,7 @@ func AddRootCA(cert *x509.Certificate) TLSOption { // defines the set of root certificate authorities that servers use if required // to verify a client certificate by the policy in ClientAuth. func AddClientCA(cert *x509.Certificate) TLSOption { - return func(_ *Client, config *tls.Config) error { + return func(_ *Client, _ http.RoundTripper, config *tls.Config) error { if config.ClientCAs == nil { config.ClientCAs = x509.NewCertPool() } @@ -63,19 +92,18 @@ func AddClientCA(cert *x509.Certificate) TLSOption { } } -// AddRootFederation does a federation request and adds to the tls.Config -// RootCAs all the certificates in the response. RootCAs -// defines the set of root certificate authorities that clients use when -// verifying server certificates. -func AddRootFederation() TLSOption { - return func(c *Client, config *tls.Config) error { - if config.RootCAs == nil { - config.RootCAs = x509.NewCertPool() - } - certs, err := c.Federation(nil) +// AddRootsToRootCAs does a roots request and adds to the tls.Config RootCAs all +// the certificates in the response. RootCAs defines the set of root certificate +// authorities that clients use when verifying server certificates. +func AddRootsToRootCAs() TLSOption { + return func(c *Client, tr http.RoundTripper, config *tls.Config) error { + certs, err := c.Roots(tr) if err != nil { return err } + if config.RootCAs == nil { + config.RootCAs = x509.NewCertPool() + } for _, cert := range certs.Certificates { config.RootCAs.AddCert(cert.Certificate) } @@ -83,19 +111,58 @@ func AddRootFederation() TLSOption { } } -// AddClientFederation does a federation request and adds to the tls.Config -// ClientCAs all the certificates in the response. ClientCAs defines the set of -// root certificate authorities that servers use if required to verify a client +// AddRootsToClientCAs does a roots request and adds to the tls.Config ClientCAs +// all the certificates in the response. ClientCAs defines the set of root +// certificate authorities that servers use if required to verify a client // certificate by the policy in ClientAuth. -func AddClientFederation() TLSOption { - return func(c *Client, config *tls.Config) error { - if config.ClientCAs == nil { - config.ClientCAs = x509.NewCertPool() - } - certs, err := c.Federation(nil) +func AddRootsToClientCAs() TLSOption { + return func(c *Client, tr http.RoundTripper, config *tls.Config) error { + certs, err := c.Roots(tr) if err != nil { return err } + if config.ClientCAs == nil { + config.ClientCAs = x509.NewCertPool() + } + for _, cert := range certs.Certificates { + config.ClientCAs.AddCert(cert.Certificate) + } + return nil + } +} + +// AddFederationToRootCAs does a federation request and adds to the tls.Config +// RootCAs all the certificates in the response. RootCAs defines the set of root +// certificate authorities that clients use when verifying server certificates. +func AddFederationToRootCAs() TLSOption { + return func(c *Client, tr http.RoundTripper, config *tls.Config) error { + certs, err := c.Federation(tr) + if err != nil { + return err + } + if config.RootCAs == nil { + config.RootCAs = x509.NewCertPool() + } + for _, cert := range certs.Certificates { + config.RootCAs.AddCert(cert.Certificate) + } + return nil + } +} + +// AddFederationToClientCAs does a federation request and adds to the tls.Config +// ClientCAs all the certificates in the response. ClientCAs defines the set of +// root certificate authorities that servers use if required to verify a client +// certificate by the policy in ClientAuth. +func AddFederationToClientCAs() TLSOption { + return func(c *Client, tr http.RoundTripper, config *tls.Config) error { + certs, err := c.Federation(tr) + if err != nil { + return err + } + if config.ClientCAs == nil { + config.ClientCAs = x509.NewCertPool() + } for _, cert := range certs.Certificates { config.ClientCAs.AddCert(cert.Certificate) } diff --git a/ca/tls_options_test.go b/ca/tls_options_test.go index 9886e487..07068ca4 100644 --- a/ca/tls_options_test.go +++ b/ca/tls_options_test.go @@ -4,13 +4,15 @@ import ( "crypto/tls" "crypto/x509" "fmt" + "io/ioutil" + "net/http" "reflect" "testing" ) func Test_setTLSOptions(t *testing.T) { fail := func() TLSOption { - return func(c *Client, config *tls.Config) error { + return func(c *Client, tr http.RoundTripper, config *tls.Config) error { return fmt.Errorf("an error") } } @@ -27,9 +29,13 @@ func Test_setTLSOptions(t *testing.T) { {"ok", args{&tls.Config{}, []TLSOption{VerifyClientCertIfGiven()}}, false}, {"fail", args{&tls.Config{}, []TLSOption{VerifyClientCertIfGiven(), fail()}}, true}, } + + ca := startCATestServer() + defer ca.Close() + client, sr, pk := signDuration(ca, "127.0.0.1", 0) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if err := setTLSOptions(nil, tt.args.c, tt.args.options); (err != nil) != tt.wantErr { + if err := setTLSOptions(client, sr, pk, tt.args.c, tt.args.options); (err != nil) != tt.wantErr { t.Errorf("setTLSOptions() error = %v, wantErr %v", err, tt.wantErr) } }) @@ -46,7 +52,7 @@ func TestRequireAndVerifyClientCert(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := &tls.Config{} - if err := RequireAndVerifyClientCert()(nil, got); err != nil { + if err := RequireAndVerifyClientCert()(nil, nil, got); err != nil { t.Errorf("RequireAndVerifyClientCert() error = %v", err) return } @@ -67,7 +73,7 @@ func TestVerifyClientCertIfGiven(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := &tls.Config{} - if err := VerifyClientCertIfGiven()(nil, got); err != nil { + if err := VerifyClientCertIfGiven()(nil, nil, got); err != nil { t.Errorf("VerifyClientCertIfGiven() error = %v", err) return } @@ -96,7 +102,7 @@ func TestAddRootCA(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := &tls.Config{} - if err := AddRootCA(tt.args.cert)(nil, got); err != nil { + if err := AddRootCA(tt.args.cert)(nil, nil, got); err != nil { t.Errorf("AddRootCA() error = %v", err) return } @@ -125,7 +131,7 @@ func TestAddClientCA(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := &tls.Config{} - if err := AddClientCA(tt.args.cert)(nil, got); err != nil { + if err := AddClientCA(tt.args.cert)(nil, nil, got); err != nil { t.Errorf("AddClientCA() error = %v", err) return } @@ -135,3 +141,185 @@ func TestAddClientCA(t *testing.T) { }) } } + +func TestAddRootsToRootCAs(t *testing.T) { + ca := startCATestServer() + defer ca.Close() + + client, sr, pk := signDuration(ca, "127.0.0.1", 0) + tr, err := getTLSOptionsTransport(sr, pk) + if err != nil { + t.Fatal(err) + } + + root, err := ioutil.ReadFile("testdata/secrets/root_ca.crt") + if err != nil { + t.Fatal(err) + } + + cert := parseCertificate(string(root)) + pool := x509.NewCertPool() + pool.AddCert(cert) + + tests := []struct { + name string + tr http.RoundTripper + want *tls.Config + wantErr bool + }{ + {"ok", tr, &tls.Config{RootCAs: pool}, false}, + {"fail", http.DefaultTransport, &tls.Config{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := &tls.Config{} + if err := AddRootsToRootCAs()(client, tt.tr, got); (err != nil) != tt.wantErr { + t.Errorf("AddRootsToRootCAs() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AddRootsToRootCAs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAddRootsToClientCAs(t *testing.T) { + ca := startCATestServer() + defer ca.Close() + + client, sr, pk := signDuration(ca, "127.0.0.1", 0) + tr, err := getTLSOptionsTransport(sr, pk) + if err != nil { + t.Fatal(err) + } + + root, err := ioutil.ReadFile("testdata/secrets/root_ca.crt") + if err != nil { + t.Fatal(err) + } + + cert := parseCertificate(string(root)) + pool := x509.NewCertPool() + pool.AddCert(cert) + + tests := []struct { + name string + tr http.RoundTripper + want *tls.Config + wantErr bool + }{ + {"ok", tr, &tls.Config{ClientCAs: pool}, false}, + {"fail", http.DefaultTransport, &tls.Config{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := &tls.Config{} + if err := AddRootsToClientCAs()(client, tt.tr, got); (err != nil) != tt.wantErr { + t.Errorf("AddRootsToClientCAs() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AddRootsToClientCAs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAddFederationToRootCAs(t *testing.T) { + ca := startCATestServer() + defer ca.Close() + + client, sr, pk := signDuration(ca, "127.0.0.1", 0) + tr, err := getTLSOptionsTransport(sr, pk) + if err != nil { + t.Fatal(err) + } + + root, err := ioutil.ReadFile("testdata/secrets/root_ca.crt") + if err != nil { + t.Fatal(err) + } + + federated, err := ioutil.ReadFile("testdata/secrets/federated_ca.crt") + if err != nil { + t.Fatal(err) + } + + crt1 := parseCertificate(string(root)) + crt2 := parseCertificate(string(federated)) + pool := x509.NewCertPool() + pool.AddCert(crt1) + pool.AddCert(crt2) + + tests := []struct { + name string + tr http.RoundTripper + want *tls.Config + wantErr bool + }{ + {"ok", tr, &tls.Config{RootCAs: pool}, false}, + {"fail", http.DefaultTransport, &tls.Config{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := &tls.Config{} + if err := AddFederationToRootCAs()(client, tt.tr, got); (err != nil) != tt.wantErr { + t.Errorf("AddFederationToRootCAs() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AddFederationToRootCAs() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAddFederationToClientCAs(t *testing.T) { + ca := startCATestServer() + defer ca.Close() + + client, sr, pk := signDuration(ca, "127.0.0.1", 0) + tr, err := getTLSOptionsTransport(sr, pk) + if err != nil { + t.Fatal(err) + } + + root, err := ioutil.ReadFile("testdata/secrets/root_ca.crt") + if err != nil { + t.Fatal(err) + } + + federated, err := ioutil.ReadFile("testdata/secrets/federated_ca.crt") + if err != nil { + t.Fatal(err) + } + + crt1 := parseCertificate(string(root)) + crt2 := parseCertificate(string(federated)) + pool := x509.NewCertPool() + pool.AddCert(crt1) + pool.AddCert(crt2) + + tests := []struct { + name string + tr http.RoundTripper + want *tls.Config + wantErr bool + }{ + {"ok", tr, &tls.Config{ClientCAs: pool}, false}, + {"fail", http.DefaultTransport, &tls.Config{}, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := &tls.Config{} + if err := AddFederationToClientCAs()(client, tt.tr, got); (err != nil) != tt.wantErr { + t.Errorf("AddFederationToClientCAs() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("AddFederationToClientCAs() = %v, want %v", got, tt.want) + } + }) + } +}