diff --git a/api/client/client.go b/api/client/client.go index 202146a13..9081c5be0 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -38,7 +38,9 @@ type server struct { // are used by the remote. type Remote interface { AuthSign(req, id []byte, provider auth.Provider) ([]byte, error) + BundleAuthSign(req, id []byte, provider auth.Provider) ([]byte, []byte, error) Sign(jsonData []byte) ([]byte, error) + BundleSign(jsonData []byte) ([]byte, []byte, error) Info(jsonData []byte) (*info.Resp, error) Hosts() []string SetReqModifier(func(*http.Request, []byte)) @@ -190,7 +192,18 @@ func (srv *server) post(url string, jsonData []byte) (*api.Response, error) { // It takes the serialized JSON request to send, remote address and // authentication provider. func (srv *server) AuthSign(req, id []byte, provider auth.Provider) ([]byte, error) { - return srv.authReq(req, id, provider, "sign") + _, cert, err := srv.authReq(req, id, provider, "sign", false) + return cert, err +} + +// BundleAuthSign fills out an authenticated signing request to the server, +// receiving a signed certificate in an "optimal" bundle, the root CA +// (if provided by the API) or an error in response. +// It takes the serialized JSON request to send which is required to +// have the bundle parameter set to true, remote address and authentication +// provider. +func (srv *server) BundleAuthSign(req, id []byte, provider auth.Provider) ([]byte, []byte, error) { + return srv.authReq(req, id, provider, "sign", true) } // AuthInfo fills out an authenticated info request to the server, @@ -198,18 +211,19 @@ func (srv *server) AuthSign(req, id []byte, provider auth.Provider) ([]byte, err // It takes the serialized JSON request to send, remote address and // authentication provider. func (srv *server) AuthInfo(req, id []byte, provider auth.Provider) ([]byte, error) { - return srv.authReq(req, id, provider, "info") + _, cert, err := srv.authReq(req, id, provider, "info", false) + return cert, err } // authReq is the common logic for AuthSign and AuthInfo -- perform the given // request, and return the resultant certificate. // The target is either 'sign' or 'info'. -func (srv *server) authReq(req, ID []byte, provider auth.Provider, target string) ([]byte, error) { +func (srv *server) authReq(req, ID []byte, provider auth.Provider, target string, returnBundle bool) ([]byte, []byte, error) { url := srv.getURL("auth" + target) token, err := provider.Token(req) if err != nil { - return nil, errors.Wrap(errors.APIClientError, errors.AuthenticationFailure, err) + return nil, nil, errors.Wrap(errors.APIClientError, errors.AuthenticationFailure, err) } aReq := &auth.AuthenticatedRequest{ @@ -221,32 +235,54 @@ func (srv *server) authReq(req, ID []byte, provider auth.Provider, target string jsonData, err := json.Marshal(aReq) if err != nil { - return nil, errors.Wrap(errors.APIClientError, errors.JSONError, err) + return nil, nil, errors.Wrap(errors.APIClientError, errors.JSONError, err) } response, err := srv.post(url, jsonData) if err != nil { - return nil, err + return nil, nil, err } result, ok := response.Result.(map[string]interface{}) if !ok { - return nil, errors.New(errors.APIClientError, errors.JSONError) + return nil, nil, errors.New(errors.APIClientError, errors.JSONError) } - cert, ok := result["certificate"].(string) + var ca, cert string + if returnBundle { + bundle, okBundle := result["bundle"].(map[string]interface{}) + if !okBundle { + return nil, nil, errors.New(errors.APIClientError, errors.JSONError) + } + cert, ok = bundle["bundle"].(string) + // The API docs are not clear if root is always returned. + // So make sure to not panic and return the bundle even if root is not returned. + ca, _ = bundle["root"].(string) + } else { + cert, ok = result["certificate"].(string) + } if !ok { - return nil, errors.New(errors.APIClientError, errors.JSONError) + return nil, nil, errors.New(errors.APIClientError, errors.JSONError) } - return []byte(cert), nil + return []byte(ca), []byte(cert), nil } // Sign sends a signature request to the remote CFSSL server, // receiving a signed certificate or an error in response. // It takes the serialized JSON request to send. func (srv *server) Sign(jsonData []byte) ([]byte, error) { - return srv.request(jsonData, "sign") + _, cert, err := srv.request(jsonData, "sign", false) + return cert, err +} + +// BundleSign sends a signature request to the remote CFSSL server, +// receiving a signed certificate in an "optimal" bundle, the root CA +// (if provided by the API) or an error in response. +// It takes the serialized JSON request to send which is required to +// have the bundle parameter set to true. +func (srv *server) BundleSign(jsonData []byte) ([]byte, []byte, error) { + return srv.request(jsonData, "sign", true) } // Info sends an info request to the remote CFSSL server, receiving a @@ -294,18 +330,33 @@ func (srv *server) getResultMap(jsonData []byte, target string) (result map[stri } // request performs the common logic for Sign and Info, performing the actual -// request and returning the resultant certificate. -func (srv *server) request(jsonData []byte, target string) ([]byte, error) { +// request and returning the resultant certificate or bundle. +func (srv *server) request(jsonData []byte, target string, returnBundle bool) ([]byte, []byte, error) { result, err := srv.getResultMap(jsonData, target) if err != nil { - return nil, err + return nil, nil, err + } + + var ca, cert, key string + if returnBundle { + key = "bundle" + bundle, okBundle := result[key].(map[string]interface{}) + if okBundle { + cert = bundle[key].(string) + // The API docs are not clear if root is always returned. + // So make sure to not panic and return the bundle even if root is not returned. + ca, _ = bundle["root"].(string) + } + } else { + key = "certificate" + cert = result[key].(string) } - cert := result["certificate"].(string) + if cert != "" { - return []byte(cert), nil + return []byte(ca), []byte(cert), nil } - return nil, errors.Wrap(errors.APIClientError, errors.ClientHTTPError, stderr.New("response doesn't contain certificate.")) + return nil, nil, errors.Wrap(errors.APIClientError, errors.ClientHTTPError, stderr.New(fmt.Sprintf("response doesn't contain %s.", key))) } // AuthRemote acts as a Remote with a default Provider for AuthSign. @@ -329,6 +380,11 @@ func (ar *AuthRemote) Sign(req []byte) ([]byte, error) { return ar.AuthSign(req, nil, ar.provider) } +// BundleSign is overloaded to perform an BundleAuthSign request using the default auth provider. +func (ar *AuthRemote) BundleSign(req []byte) ([]byte, []byte, error) { + return ar.BundleAuthSign(req, nil, ar.provider) +} + // normalizeURL checks for http/https protocol, appends "http" as default protocol if not defined in url func normalizeURL(addr string) (*url.URL, error) { addr = strings.TrimSpace(addr) diff --git a/api/client/client_test.go b/api/client/client_test.go index 0bb6e5e58..2f62f2f10 100644 --- a/api/client/client_test.go +++ b/api/client/client_test.go @@ -2,11 +2,12 @@ package client import ( "crypto/tls" - "github.com/cloudflare/cfssl/auth" - "github.com/cloudflare/cfssl/helpers" "net" "strings" "testing" + + "github.com/cloudflare/cfssl/auth" + "github.com/cloudflare/cfssl/helpers" ) var ( @@ -60,8 +61,8 @@ func TestInvalidPort(t *testing.T) { } func TestAuthSign(t *testing.T) { - s := NewServer(".X") testProvider, _ = auth.New(testKey, nil) + s := NewAuthServer(".X", nil, testProvider) testRequest := []byte(`testing 1 2 3`) as, err := s.AuthSign(testRequest, testAD, testProvider) if as != nil || err == nil { @@ -69,6 +70,16 @@ func TestAuthSign(t *testing.T) { } } +func TestBundleAuthSign(t *testing.T) { + testProvider, _ = auth.New(testKey, nil) + s := NewAuthServer(".X", nil, testProvider) + testRequest := []byte(`testing 1 2 3`) + _, as, err := s.BundleAuthSign(testRequest, testAD, testProvider) + if as != nil || err == nil { + t.Fatal("expected error with auth sign function") + } +} + func TestDefaultAuthSign(t *testing.T) { testProvider, _ = auth.New(testKey, nil) s := NewAuthServer(".X", nil, testProvider) @@ -87,6 +98,14 @@ func TestSign(t *testing.T) { } } +func TestBundleSign(t *testing.T) { + s := NewServer(".X") + _, sign, err := s.BundleSign([]byte{5, 5, 5, 5}) + if sign != nil || err == nil { + t.Fatalf("expected error with sign function") + } +} + func TestNewMutualTLSServer(t *testing.T) { cert, _ := helpers.LoadClientCertificate("../../helpers/testdata/ca.pem", "../../helpers/testdata/ca_key.pem") s := NewServerTLS("https://nohost:8888", helpers.CreateTLSConfig(nil, cert)) diff --git a/api/client/group.go b/api/client/group.go index d402dca85..563402656 100644 --- a/api/client/group.go +++ b/api/client/group.go @@ -104,6 +104,17 @@ func (g *orderedListGroup) AuthSign(req, id []byte, provider auth.Provider) (res return nil, err } +func (g *orderedListGroup) BundleAuthSign(req, id []byte, provider auth.Provider) (ca []byte, cert []byte, err error) { + for i := range g.remotes { + ca, cert, err = g.remotes[i].BundleAuthSign(req, id, provider) + if err == nil { + return ca, cert, nil + } + } + + return nil, nil, err +} + func (g *orderedListGroup) Sign(jsonData []byte) (resp []byte, err error) { for i := range g.remotes { resp, err = g.remotes[i].Sign(jsonData) @@ -115,6 +126,17 @@ func (g *orderedListGroup) Sign(jsonData []byte) (resp []byte, err error) { return nil, err } +func (g *orderedListGroup) BundleSign(jsonData []byte) (ca []byte, cert []byte, err error) { + for i := range g.remotes { + ca, cert, err = g.remotes[i].BundleSign(jsonData) + if err == nil { + return ca, cert, nil + } + } + + return nil, nil, err +} + func (g *orderedListGroup) Info(jsonData []byte) (resp *info.Resp, err error) { for i := range g.remotes { resp, err = g.remotes[i].Info(jsonData)