diff --git a/common/client.go b/common/client.go index 08cebce..31dcb91 100644 --- a/common/client.go +++ b/common/client.go @@ -5,9 +5,12 @@ package common import ( "bytes" + "crypto/tls" + "crypto/x509" "fmt" "io" "net/http" + "os" "time" "github.com/veraison/apiclient/auth" @@ -22,11 +25,64 @@ type Client struct { } // NewClient instantiates a new Client with a fixed 5s timeout. The client will -// use the provided IAuthenticator for requests, if it is not nil +// use the provided IAuthenticator for requests, if it is not nil. func NewClient(a auth.IAuthenticator) *Client { + return NewClientWithTransport(a, nil) +} + +// NewInsecureTLSClient instantiates a new Client with a transport configured +// to accept TLS connections without verifying certs and a fixed 5s timeout. +// The client will use the provided IAuthenticator for requests, if it is not +// nil. +func NewInsecureTLSClient(a auth.IAuthenticator) *Client { + transport := http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + } + + return NewClientWithTransport(a, &transport) +} + +// NewTLSClient instantiates a new Client with a fixed 5s timeout and transport +// configured with the system certificate pool as well as any certs provided. +// The client will use the provided IAuthenticator for requests, if it is not +// nil. +func NewTLSClient(a auth.IAuthenticator, certPaths []string) (*Client, error) { + certPool, err := x509.SystemCertPool() + if err != nil { + return nil, err + } + + for _, certPath := range certPaths { + rawCert, err := os.ReadFile(certPath) + if err != nil { + return nil, fmt.Errorf("could not read cert: %w", err) + } + + + if ok := certPool.AppendCertsFromPEM(rawCert); !ok { + return nil, fmt.Errorf("invalid cert in %s", certPath) + } + } + + transport := http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + }, + } + + return NewClientWithTransport(a, &transport), nil +} + +// NewClientWithTransport instantiates a new Client with the specified transport and a fixed +// 5s timeout. The client will use the provided IAuthenticator for requests, if +// it is not nil. +func NewClientWithTransport(a auth.IAuthenticator, transport http.RoundTripper) *Client { return &Client{ HTTPClient: http.Client{ Timeout: 5 * time.Second, + Transport: transport, }, Auth: a, } diff --git a/management/management.go b/management/management.go index d93b833..19ea382 100644 --- a/management/management.go +++ b/management/management.go @@ -46,6 +46,39 @@ func NewService(uri string, a auth.IAuthenticator) (*Service, error) { return &m, nil } +// NewInsecureTLSService creates a new Service instance using the provided +// endpoint URI and an HTTPS client that does not verify certs. If the supplied +// IAuthenticator is not nil, that will be used to set the Authorization header +// in the service requests. +func NewInsecureTLSService(uri string, a auth.IAuthenticator) (*Service, error) { + m := Service{Client: common.NewInsecureTLSClient(a)} + + if err := m.doSetEndpointURI(uri, true); err != nil { + return nil, err + } + + return &m, nil +} + +// NewTLSService creates a new Service instance using the provided endpoint URI +// and an HTTPS client configured with the specified certs (in addition to the +// system certs). If the supplied IAuthenticator is not nil, that will be used +// to set the Authorization header in the service requests. +func NewTLSService(uri string, a auth.IAuthenticator, certPaths []string) (*Service, error) { + cli, err := common.NewTLSClient(a, certPaths) + if err != nil { + return nil, err + } + + m := Service{Client: cli} + + if err := m.doSetEndpointURI(uri, true); err != nil { + return nil, err + } + + return &m, nil +} + // SetClient sets the HTTP(s) client connection configuration func (o *Service) SetClient(client *common.Client) error { if client == nil { @@ -57,18 +90,7 @@ func (o *Service) SetClient(client *common.Client) error { // SetEndpointURI sets the URI if the Veraison services management endpoint. func (o *Service) SetEndpointURI(uri string) error { - u, err := url.Parse(uri) - if err != nil { - return fmt.Errorf("malformed URI: %w", err) - } - - if !u.IsAbs() { - return fmt.Errorf("URI is not absolute: %q", uri) - } - - o.EndPointURI = u - - return nil + return o.doSetEndpointURI(uri, false) } // CreateOPAPolicy is a wrapper around CreatePolicy that assumes the OPA media @@ -273,3 +295,22 @@ func policiesFromResponse(res *http.Response) ([]*Policy, error) { return policies, nil } + +func (o *Service) doSetEndpointURI(uri string, checkTLS bool) error { + u, err := url.Parse(uri) + if err != nil { + return fmt.Errorf("malformed URI: %w", err) + } + + if !u.IsAbs() { + return fmt.Errorf("URI is not absolute: %q", uri) + } + + if checkTLS && u.Scheme != "https" { + return fmt.Errorf("Expected HTTPS scheme, but got: %q", uri) + } + + o.EndPointURI = u + + return nil +} diff --git a/provisioning/provisioning.go b/provisioning/provisioning.go index f8675d5..9cf1bc5 100644 --- a/provisioning/provisioning.go +++ b/provisioning/provisioning.go @@ -33,6 +33,9 @@ type SubmitConfig struct { SubmitURI string // URI of the /submit endpoint DeleteSession bool // explicitly DELETE the session object after we are done Auth auth.IAuthenticator // when set, Auth supplies the Authorization header for requests + UseTLS bool // use TLS for server connectionections + IsInsecure bool // allow insecure server connections (only matters when UseTLS is true) + CACerts []string // paths to CA certs to be used in addtion to system certs for TLS connections } // SetClient sets the HTTP(s) client connection configuration @@ -58,6 +61,7 @@ func (cfg *SubmitConfig) SetSubmitURI(uri string) error { if !u.IsAbs() { return errors.New("uri is not absolute") } + cfg.UseTLS = u.Scheme == "https" cfg.SubmitURI = uri return nil } @@ -75,6 +79,16 @@ func (cfg *SubmitConfig) SetAuth(a auth.IAuthenticator) { } } +// SetIsInsecure sets the IsInsecure parameter using the supplied val +func (cfg *SubmitConfig) SetIsInsecure(val bool) { + cfg.IsInsecure = val +} + +// SetCerts sets the CACerts parameter to the specified paths +func (cfg *SubmitConfig) SetCerts(paths []string) { + cfg.CACerts = paths +} + // Run implements the endorsement submission API. If the session does not // complete synchronously, this call will block until either the session state // moves out of the processing state, or the MaxAttempts*PollPeriod threshold is @@ -84,8 +98,9 @@ func (cfg SubmitConfig) Run(endorsement []byte, mediaType string) error { return err } - if cfg.Client == nil { - cfg.Client = common.NewClient(cfg.Auth) + // Attach the default client if the user hasn't supplied one + if err := cfg.initClient(); err != nil { + return err } // POST endorsement to the /submit endpoint @@ -218,3 +233,25 @@ func sessionFromResponse(res *http.Response) (*SubmitSession, error) { return &j, nil } + +func (cfg *SubmitConfig) initClient() error { + if cfg.Client != nil { + return nil // client already initialized + } + + if !cfg.UseTLS { + cfg.Client = common.NewClient(cfg.Auth) + return nil + } + + if cfg.IsInsecure { + cfg.Client = common.NewInsecureTLSClient(cfg.Auth) + return nil + } + + var err error + + cfg.Client, err = common.NewTLSClient(cfg.Auth, cfg.CACerts) + + return err +} diff --git a/provisioning/provisioning_test.go b/provisioning/provisioning_test.go index cadaae4..e1730c5 100644 --- a/provisioning/provisioning_test.go +++ b/provisioning/provisioning_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/veraison/apiclient/auth" "github.com/veraison/apiclient/common" ) @@ -17,6 +18,7 @@ var ( testEndorsementMediaType = "application/corim+cbor" testSubmitURI = "http://veraison.example/endorsement-provisioning/v1/submit" testSessionURI = "http://veraison.example/endorsement-provisioning/v1/session/1234" + testCertPaths = []string{"/test/path1", "/test/path2"} ) func TestSubmitConfig_check_ok(t *testing.T) { @@ -373,3 +375,40 @@ func TestSubmitConfig_pollForSubmissionCompletion_success_status(t *testing.T) { t, responseCode, []byte(sessionBody), expectedErr, ) } + +func TestSubmitConfig_initClient(t *testing.T) { + cfg := SubmitConfig{SubmitURI: testSubmitURI} + cfg.initClient() + assert.Nil(t, cfg.Client.HTTPClient.Transport) + + cfg = SubmitConfig{SubmitURI: testSubmitURI, UseTLS: true} + cfg.initClient() + require.NotNil(t, cfg.Client.HTTPClient.Transport) + transport := cfg.Client.HTTPClient.Transport.(*http.Transport) + assert.False(t, transport.TLSClientConfig.InsecureSkipVerify) + + cfg = SubmitConfig{SubmitURI: testSubmitURI, UseTLS: true, IsInsecure: true} + cfg.initClient() + require.NotNil(t, cfg.Client.HTTPClient.Transport) + transport = cfg.Client.HTTPClient.Transport.(*http.Transport) + assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) +} + +func TestSubmitConfig_setters(t *testing.T) { + cfg := SubmitConfig{SubmitURI: testSubmitURI} + require.NoError(t, cfg.initClient()) + + cfg.SetDeleteSession(true) + assert.True(t, cfg.DeleteSession) + + a := &auth.NullAuthenticator{} + cfg.SetAuth(a) + assert.Equal(t, a, cfg.Auth) + assert.Equal(t, a, cfg.Client.Auth) + + cfg.SetIsInsecure(true) + assert.True(t, cfg.IsInsecure) + + cfg.SetCerts(testCertPaths) + assert.EqualValues(t, testCertPaths, cfg.CACerts) +} diff --git a/verification/challengeresponse.go b/verification/challengeresponse.go index 3617267..90577fd 100644 --- a/verification/challengeresponse.go +++ b/verification/challengeresponse.go @@ -47,6 +47,10 @@ type ChallengeResponseConfig struct { DeleteSession bool // explicitly DELETE the session object after we are done Wrap CmwWrap // when set, wrap the supplied evidence as a Conceptual Message Wrapper(CMW) Auth auth.IAuthenticator // when set, Auth supplies the Authorization header for requests + UseTLS bool // use TLS for server connectionections + IsInsecure bool // allow insecure server connections (only matters when UseTLS is true) + CACerts []string // paths to CA certs to be used in addtion to system certs for TLS connections + } // Blob wraps a base64 encoded value together with its media type @@ -103,10 +107,21 @@ func (cfg *ChallengeResponseConfig) SetSessionURI(uri string) error { if !u.IsAbs() { return errors.New("the supplied session URI is not in absolute form") } + cfg.UseTLS = u.Scheme == "https" cfg.NewSessionURI = uri return nil } +// SetIsInsecure sets the IsInsecure parameter using the supplied val +func (cfg *ChallengeResponseConfig) SetIsInsecure(val bool) { + cfg.IsInsecure = val +} + +// SetCerts sets the CACerts parameter to the specified paths +func (cfg *ChallengeResponseConfig) SetCerts(paths []string) { + cfg.CACerts = paths +} + // SetClient sets the HTTP(s) client connection configuration func (cfg *ChallengeResponseConfig) SetClient(client *common.Client) error { if client == nil { @@ -141,14 +156,14 @@ func (cfg *ChallengeResponseConfig) SetWrap(val CmwWrap) error { // Run implements the challenge-response protocol FSM invoking the user // callback. On success, the received Attestation Result is returned. -func (cfg ChallengeResponseConfig) Run() ([]byte, error) { +func (cfg *ChallengeResponseConfig) Run() ([]byte, error) { if err := cfg.check(true); err != nil { return nil, err } // Attach the default client if the user hasn't supplied one - if cfg.Client == nil { - cfg.Client = common.NewClient(cfg.Auth) + if err := cfg.initClient(); err != nil { + return nil, err } newSessionCtx, sessionURI, err := cfg.newSession() @@ -197,8 +212,8 @@ func (cfg ChallengeResponseConfig) NewSession() (*ChallengeResponseSession, stri } // Attach the default client if the user hasn't supplied one - if cfg.Client == nil { - cfg.Client = common.NewClient(cfg.Auth) + if err := cfg.initClient(); err != nil { + return nil, "", err } return cfg.newSession() @@ -397,3 +412,25 @@ func (cfg ChallengeResponseConfig) pollForAttestationResult(uri string) ([]byte, return nil, fmt.Errorf("polling attempts exhausted, session resource state still not complete") } + +func (cfg *ChallengeResponseConfig) initClient() error { + if cfg.Client != nil { + return nil // client already initialized + } + + if !cfg.UseTLS { + cfg.Client = common.NewClient(cfg.Auth) + return nil + } + + if cfg.IsInsecure { + cfg.Client = common.NewInsecureTLSClient(cfg.Auth) + return nil + } + + var err error + + cfg.Client, err = common.NewTLSClient(cfg.Auth, cfg.CACerts) + + return err +} diff --git a/verification/challengeresponse_test.go b/verification/challengeresponse_test.go index e3cd7cb..f43882c 100644 --- a/verification/challengeresponse_test.go +++ b/verification/challengeresponse_test.go @@ -28,6 +28,7 @@ var ( testSessionURI = testBaseURI + testRelSessionURI testNewSessionURI = testBaseURI + "/challenge-response/v1/newSession" testBadURI = `http://veraison.example:80challenge-response/v1/session/1` + testCertPaths = []string{"/test/path1", "/test/path2"} ) type testEvidenceBuilder struct{} @@ -897,3 +898,39 @@ func TestChallengeResponseConfig_Run_async_CMWWrap(t *testing.T) { assert.JSONEq(t, expectedResult, string(result)) } } + +func TestChallengeResponseConfig_initClient(t *testing.T) { + cfg := ChallengeResponseConfig{NewSessionURI: testNewSessionURI} + cfg.initClient() + assert.Nil(t, cfg.Client.HTTPClient.Transport) + + cfg = ChallengeResponseConfig{NewSessionURI: testNewSessionURI, UseTLS: true} + cfg.initClient() + require.NotNil(t, cfg.Client.HTTPClient.Transport) + transport := cfg.Client.HTTPClient.Transport.(*http.Transport) + assert.False(t, transport.TLSClientConfig.InsecureSkipVerify) + + cfg = ChallengeResponseConfig{ + NewSessionURI: testNewSessionURI, + UseTLS: true, + IsInsecure: true, + } + cfg.initClient() + require.NotNil(t, cfg.Client.HTTPClient.Transport) + transport = cfg.Client.HTTPClient.Transport.(*http.Transport) + assert.True(t, transport.TLSClientConfig.InsecureSkipVerify) +} + +func TestChallengeResponseConfig_setters(t *testing.T) { + cfg := ChallengeResponseConfig{NewSessionURI: testNewSessionURI} + require.NoError(t, cfg.initClient()) + + cfg.SetDeleteSession(true) + assert.True(t, cfg.DeleteSession) + + cfg.SetIsInsecure(true) + assert.True(t, cfg.IsInsecure) + + cfg.SetCerts(testCertPaths) + assert.EqualValues(t, testCertPaths, cfg.CACerts) +}