From 91a23ffc3934e40208471916e2772117de6ff661 Mon Sep 17 00:00:00 2001 From: Simon Ott Date: Tue, 13 Feb 2024 19:00:48 +0000 Subject: [PATCH] testtool: Added attested HTTPS demo Introduced mode serve to establish an attested HTTPS server as well as mode request to perform example attested HTTPS GET requests. The addresses can be specified via the addr flag. Multiple requests to the same server will be performed within the same attested TLS session, while requests to different servers trigger an attestation per server. Signed-off-by: Simon Ott --- README.md | 20 ++ attestedhttp/client.go | 212 +++++++++++++++++++++ attestedhttp/server.go | 79 ++++++++ attestedtls/attestation.go | 32 ++-- attestedtls/coap.go | 38 ++-- attestedtls/config.go | 120 ++++++------ attestedtls/dialer.go | 22 ++- attestedtls/grpc.go | 34 ++-- attestedtls/key.go | 22 +-- attestedtls/libapi.go | 34 ++-- attestedtls/listener.go | 18 +- attestedtls/socket.go | 38 ++-- doc/configuration.md | 7 +- testtool/certs.go | 49 +++++ testtool/coap.go | 10 +- testtool/commands.go | 8 + testtool/config.go | 21 +++ testtool/grpc.go | 8 + testtool/http.go | 312 +++++++++++++++++++++++++++++++ testtool/hub.go | 2 +- testtool/libapi.go | 26 +++ testtool/main.go | 2 + testtool/publish.go | 127 +++++++++++++ testtool/socket.go | 8 + testtool/{internal.go => tls.go} | 131 +------------ 25 files changed, 1079 insertions(+), 301 deletions(-) create mode 100644 attestedhttp/client.go create mode 100644 attestedhttp/server.go create mode 100644 testtool/certs.go create mode 100644 testtool/http.go create mode 100644 testtool/publish.go rename testtool/{internal.go => tls.go} (68%) diff --git a/README.md b/README.md index 50fd5217..6b64622f 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,26 @@ be used to establish attested TLS connections: ./testtool -mode dial -addr localhost:4443 -ca $CMC_ROOT/cmc-data/pki/ca.pem -mtls ``` +### Establish Attested HTTPS Connection + +The `testtool` can also perform user-specified attested HTTPS requests and act as an attested HTTPS demo server, respectively. + +```sh +# Run two attested HTTPS servers +./testtool -config $CMC_ROOT/testtool-config.json -addr 0.0.0.0:8081 -mode serve +./testtool -config $CMC_ROOT/testtool-config.json -addr 0.0.0.0:8082 -mode serve + +# Perform multiple user-specified attested HTTPS requests to both servers. Each connection is attested, while multiple requests to the same server use the established attested TLS connections +./testtool \ + -config ../../cmc-data/testtool-lib-config.json \ + -addr https://localhost:8081/post,https://localhost:8082/post \ + -mode request \ + -method POST \ + -data "hello from attested HTTPS client" \ + -header "Content-Type: text/plain" +``` + + **Note**: The *cmcd* TPM provisioning process includes the verification of the TPM's EK certificate chain. In the example setup, this verification is turned off, as the database might not contain the certificate chain for the TPM of the machine the *cmcd* is running on. Instead, simply a diff --git a/attestedhttp/client.go b/attestedhttp/client.go new file mode 100644 index 00000000..a80555e9 --- /dev/null +++ b/attestedhttp/client.go @@ -0,0 +1,212 @@ +// Copyright (c) 2021 Fraunhofer AISEC +// Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. +// +// 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 attestedhttp + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "time" + + "github.com/sirupsen/logrus" + + ar "github.com/Fraunhofer-AISEC/cmc/attestationreport" + atls "github.com/Fraunhofer-AISEC/cmc/attestedtls" + "github.com/Fraunhofer-AISEC/cmc/cmc" +) + +var log = logrus.WithField("service", "ahttps") + +// Wrapper for net/http Transport +type Transport struct { + + // Wrapped http.Transport parameters + TLSClientConfig *tls.Config + TLSHandshakeTimeout time.Duration + DisableKeepAlives bool + DisableCompression bool + MaxIdleConns int + MaxIdleConnsPerHost int + MaxConnsPerHost int + IdleConnTimeout time.Duration + ResponseHeaderTimeout time.Duration + ExpectContinueTimeout time.Duration + MaxResponseHeaderBytes int64 + WriteBufferSize int + ReadBufferSize int + + // Some http.Transport parameters such as dial hooks are not exposed, + // as we enforce aTLS as the underlying transport protocol + + // Additional aTLS parameters + Attest string + MutualTls bool + CmcAddr string + CmcApi atls.CmcApiSelect + CmcNetwork string + Cmc *cmc.Cmc + CmcPolicies []byte + Ca []byte + ReadTimeout time.Duration +} + +// Wrapper for net/http Client +type Client struct { + Transport *Transport + CheckRedirect func(req *http.Request, via []*http.Request) error + Jar http.CookieJar + Timeout time.Duration + + client *http.Client +} + +// Wrapper for net/http client.Get() +func (c *Client) Get(url string) (resp *http.Response, err error) { + + err = prepareClient(c) + if err != nil { + return nil, fmt.Errorf("failed to prepare client: %w", err) + } + + log.Debugf("Performing aHTTPS GET to %v", url) + + return c.client.Get(url) +} + +// Wrapper for net/http client.Do() +func (c *Client) Do(req *http.Request) (*http.Response, error) { + + err := prepareClient(c) + if err != nil { + return nil, fmt.Errorf("failed to prepare client: %w", err) + } + + log.Debugf("Performing aHTTPS %v to %v", req.Method, req.URL) + + return c.client.Do(req) +} + +// Wrapper for net/http client.Post() +func (c *Client) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + + err = prepareClient(c) + if err != nil { + return nil, fmt.Errorf("failed to prepare client: %w", err) + } + + log.Debugf("Performing aHTTPS POST with content type %v to %v", contentType, url) + + return c.client.Post(url, contentType, body) +} + +// Wrapper for net/http client.Head() +func (c *Client) Head(url string) (resp *http.Response, err error) { + + err = prepareClient(c) + if err != nil { + return nil, fmt.Errorf("failed to prepare client: %w", err) + } + + log.Debugf("Performing aHTTPS HEAD to %v", url) + + return c.client.Head(url) +} + +// Wrapper for net/http client.CloseIdleConnections() +func (c *Client) CloseIdleConnections() { + + if c.client == nil { + log.Warn("Cannot close idle connections: client not initialized") + } + + log.Debug("Cloding idle connections") + + c.client.CloseIdleConnections() +} + +// Wrapper for net/http client.PostForm() +func (c *Client) PostForm(url string, data url.Values) (resp *http.Response, err error) { + return c.client.PostForm(url, data) +} + +// Prepare the client's underlying attested TLS connection +func prepareClient(c *Client) error { + var cmcConfig atls.CmcConfig + if c.client == nil { + + // Store aTLS and CMC configuration + cmcConfig.Attest = atls.GetAttestMode(c.Transport.Attest) + cmcConfig.Ca = c.Transport.Ca + cmcConfig.Cmc = c.Transport.Cmc + cmcConfig.CmcAddr = c.Transport.CmcAddr + cmcConfig.CmcApi = atls.CmcApis[c.Transport.CmcApi] + cmcConfig.Mtls = c.Transport.MutualTls + cmcConfig.Network = c.Transport.Cmc.Network + cmcConfig.Policies = c.Transport.CmcPolicies + + log.Tracef("Initializing new HTTP client") + // Create http.Transport from wrapper + transport := &http.Transport{ + // User-specified HTTPS values + TLSHandshakeTimeout: c.Transport.TLSHandshakeTimeout, // ignored because of custom DialContext + DisableKeepAlives: c.Transport.DisableKeepAlives, + DisableCompression: c.Transport.DisableCompression, + MaxIdleConns: c.Transport.MaxIdleConns, + MaxIdleConnsPerHost: c.Transport.MaxIdleConnsPerHost, + MaxConnsPerHost: c.Transport.MaxConnsPerHost, + ResponseHeaderTimeout: c.Transport.ResponseHeaderTimeout, + ExpectContinueTimeout: c.Transport.ExpectContinueTimeout, + MaxResponseHeaderBytes: c.Transport.MaxResponseHeaderBytes, + WriteBufferSize: c.Transport.WriteBufferSize, + ReadBufferSize: c.Transport.ReadBufferSize, + TLSClientConfig: c.Transport.TLSClientConfig, // Ignored becuase of custom DialContext + IdleConnTimeout: c.Transport.IdleConnTimeout, + // Fixed custom TLS dial function to enforce aHTTPS + DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + if c.Transport.TLSClientConfig == nil { + return nil, errors.New("failed to dial internal: no TLS config provided") + } + + log.Debugf("Dialing TLS address: %v", addr) + + verificationResult := new(ar.VerificationResult) + + conn, err := atls.Dial("tcp", addr, c.Transport.TLSClientConfig, + atls.WithCmcConfig(&cmcConfig), + atls.WithResult(verificationResult)) + if err != nil { + return nil, fmt.Errorf("failed to dial server: %w", err) + } + if c.Transport.ReadTimeout != 0 { + _ = conn.SetReadDeadline(time.Now().Add(c.Transport.ReadTimeout)) + } + // TODO store verification result for retrieval + + log.Debugf("aHTTPS connection established") + + return conn, err + }, + } + c.client = &http.Client{Transport: transport} + } + + return nil +} diff --git a/attestedhttp/server.go b/attestedhttp/server.go new file mode 100644 index 00000000..a874d458 --- /dev/null +++ b/attestedhttp/server.go @@ -0,0 +1,79 @@ +// Copyright (c) 2021 Fraunhofer AISEC +// Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. +// +// 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 attestedhttp + +import ( + "errors" + "fmt" + "net/http" + + ar "github.com/Fraunhofer-AISEC/cmc/attestationreport" + atls "github.com/Fraunhofer-AISEC/cmc/attestedtls" + "github.com/Fraunhofer-AISEC/cmc/cmc" +) + +// Wrapper for http.Server +type Server struct { + Server *http.Server + + // Additional aTLS parameters + Attest string + MutualTls bool + CmcAddr string + CmcApi atls.CmcApiSelect + CmcNetwork string + Cmc *cmc.Cmc + CmcPolicies []byte + Ca []byte +} + +func (s *Server) ListenAndServe() error { + + if s.Server.TLSConfig == nil { + return errors.New("failed to listen: no TLS config provided") + } + + verificationResult := new(ar.VerificationResult) + + // Listen: TLS connection + ln, err := atls.Listen("tcp", s.Server.Addr, s.Server.TLSConfig, + atls.WithCmcAddr(s.CmcAddr), + atls.WithCmcCa(s.Ca), + atls.WithCmcPolicies(s.CmcPolicies), + atls.WithCmcApi(s.CmcApi), + atls.WithMtls(s.MutualTls), + atls.WithAttest(s.Attest), + atls.WithCmcNetwork(s.CmcNetwork), + atls.WithResult(verificationResult), + atls.WithCmc(s.Cmc)) + if err != nil { + log.Fatalf("Failed to listen for connections: %v", err) + } + defer ln.Close() + + // TODO store verification result + + log.Infof("Serving HTTPS under %v", s.Server.Addr) + + err = s.Server.Serve(ln) + if err != nil { + return fmt.Errorf("failed to serve: %w", err) + } + + log.Info("Finished serving HTTPS") + + return nil +} diff --git a/attestedtls/attestation.go b/attestedtls/attestation.go index 392a67c4..68308bbf 100644 --- a/attestedtls/attestation.go +++ b/attestedtls/attestation.go @@ -26,13 +26,13 @@ var id = "0000" var log = logrus.WithField("service", "atls") -func attestDialer(conn *tls.Conn, chbindings []byte, cc cmcConfig) error { +func attestDialer(conn *tls.Conn, chbindings []byte, cc CmcConfig) error { //optional: attest Client - if cc.attest == Attest_Mutual || cc.attest == Attest_Client { + if cc.Attest == Attest_Mutual || cc.Attest == Attest_Client { log.Debug("Attesting the Client") // Obtain attestation report from local cmcd - resp, err := cc.cmcApi.obtainAR(cc, chbindings) + resp, err := cc.CmcApi.obtainAR(cc, chbindings) if err != nil { return fmt.Errorf("could not obtain dialer AR: %w", err) } @@ -40,31 +40,31 @@ func attestDialer(conn *tls.Conn, chbindings []byte, cc cmcConfig) error { // Send created attestation report to listener log.Tracef("Sending attestation report length %v to listener", len(resp)) - err = Write(append([]byte{byte(cc.attest)}, resp...), conn) + err = Write(append([]byte{byte(cc.Attest)}, resp...), conn) if err != nil { return fmt.Errorf("failed to send AR: %w", err) } log.Trace("Sent AR") } else { //if not sending attestation report, send the attestation mode - err := Write([]byte{byte(cc.attest)}, conn) + err := Write([]byte{byte(cc.Attest)}, conn) if err != nil { return fmt.Errorf("failed to send skip client Attestation: %w", err) } log.Debug("Skipping client-side attestation") } - readvalue, err := readValue(conn, cc.attest, true) + readvalue, err := readValue(conn, cc.Attest, true) if err != nil { return err } //optional: Wait for attestation report from Server - if cc.attest == Attest_Mutual || cc.attest == Attest_Server { + if cc.Attest == Attest_Mutual || cc.Attest == Attest_Server { report := readvalue // Verify AR from listener with own channel bindings log.Trace("Verifying attestation report from listener") - err = cc.cmcApi.verifyAR(chbindings, report, cc) + err = cc.CmcApi.verifyAR(chbindings, report, cc) if err != nil { return err } @@ -77,42 +77,42 @@ func attestDialer(conn *tls.Conn, chbindings []byte, cc cmcConfig) error { return nil } -func attestListener(conn *tls.Conn, chbindings []byte, cc cmcConfig) error { +func attestListener(conn *tls.Conn, chbindings []byte, cc CmcConfig) error { // optional: attest server - if cc.attest == Attest_Mutual || cc.attest == Attest_Server { + if cc.Attest == Attest_Mutual || cc.Attest == Attest_Server { // Obtain own attestation report from local cmcd log.Trace("Attesting the Server") - resp, err := cc.cmcApi.obtainAR(cc, chbindings) + resp, err := cc.CmcApi.obtainAR(cc, chbindings) if err != nil { return fmt.Errorf("could not obtain AR of Listener : %w", err) } // Send own attestation report to dialer log.Trace("Sending own attestation report") - err = Write(append([]byte{byte(cc.attest)}, resp...), conn) + err = Write(append([]byte{byte(cc.Attest)}, resp...), conn) if err != nil { return fmt.Errorf("failed to send AR: %w", err) } } else { //if not sending attestation report, send the attestation mode - err := Write([]byte{byte(cc.attest)}, conn) + err := Write([]byte{byte(cc.Attest)}, conn) if err != nil { return fmt.Errorf("failed to send skip client Attestation: %w", err) } log.Debug("Skipping server-side attestation") } - readValue, err := readValue(conn, cc.attest, false) + readValue, err := readValue(conn, cc.Attest, false) if err != nil { return err } // optional: Wait for attestation report from client - if cc.attest == Attest_Mutual || cc.attest == Attest_Client { + if cc.Attest == Attest_Mutual || cc.Attest == Attest_Client { report := readValue // Verify AR from dialer with own channel bindings log.Trace("Verifying attestation report from dialer...") - err = cc.cmcApi.verifyAR(chbindings, report, cc) + err = cc.CmcApi.verifyAR(chbindings, report, cc) if err != nil { return err } diff --git a/attestedtls/coap.go b/attestedtls/coap.go index d2d5d040..adb18fec 100644 --- a/attestedtls/coap.go +++ b/attestedtls/coap.go @@ -40,17 +40,17 @@ type CoapApi struct{} func init() { log.Trace("Adding CoAP API to APIs") - cmcApis[CmcApi_COAP] = CoapApi{} + CmcApis[CmcApi_COAP] = CoapApi{} } // Obtains attestation report from cmcd -func (a CoapApi) obtainAR(cc cmcConfig, chbindings []byte) ([]byte, error) { +func (a CoapApi) obtainAR(cc CmcConfig, chbindings []byte) ([]byte, error) { path := "/Attest" // Establish connection - log.Tracef("Contacting cmcd via coap on %v", cc.cmcAddr) - conn, err := udp.Dial(cc.cmcAddr) + log.Tracef("Contacting cmcd via coap on %v", cc.CmcAddr) + conn, err := udp.Dial(cc.CmcAddr) if err != nil { return nil, fmt.Errorf("error dialing: %w", err) } @@ -91,13 +91,13 @@ func (a CoapApi) obtainAR(cc cmcConfig, chbindings []byte) ([]byte, error) { } // Sends attestationreport to cmcd for verification -func (a CoapApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { +func (a CoapApi) verifyAR(chbindings, report []byte, cc CmcConfig) error { path := "/Verify" // Establish connection - log.Tracef("Contacting cmcd via coap on %v", cc.cmcAddr) - conn, err := udp.Dial(cc.cmcAddr) + log.Tracef("Contacting cmcd via coap on %v", cc.CmcAddr) + conn, err := udp.Dial(cc.CmcAddr) if err != nil { return fmt.Errorf("error dialing: %w", err) } @@ -108,8 +108,8 @@ func (a CoapApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { req := &api.VerificationRequest{ Nonce: chbindings, AttestationReport: report, - Ca: cc.ca, - Policies: cc.policies, + Ca: cc.Ca, + Policies: cc.Policies, } payload, err := cbor.Marshal(req) if err != nil { @@ -134,29 +134,29 @@ func (a CoapApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { } // Parse VerificationResult - if cc.result == nil { - cc.result = new(ar.VerificationResult) + if cc.Result == nil { + cc.Result = new(ar.VerificationResult) } - err = json.Unmarshal(verifyResp.VerificationResult, cc.result) + err = json.Unmarshal(verifyResp.VerificationResult, cc.Result) if err != nil { return fmt.Errorf("could not parse verification result: %w", err) } // check results - if !cc.result.Success { + if !cc.Result.Success { return errors.New("attestation report verification failed") } return nil } -func (a CoapApi) fetchSignature(cc cmcConfig, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +func (a CoapApi) fetchSignature(cc CmcConfig, digest []byte, opts crypto.SignerOpts) ([]byte, error) { path := "/TLSSign" // Establish connection - log.Tracef("Contacting cmcd via coap on %v", cc.cmcAddr) - conn, err := udp.Dial(cc.cmcAddr) + log.Tracef("Contacting cmcd via coap on %v", cc.CmcAddr) + conn, err := udp.Dial(cc.CmcAddr) if err != nil { return nil, fmt.Errorf("error dialing: %w", err) } @@ -204,13 +204,13 @@ func (a CoapApi) fetchSignature(cc cmcConfig, digest []byte, opts crypto.SignerO return signResp.SignedContent, nil } -func (a CoapApi) fetchCerts(cc cmcConfig) ([][]byte, error) { +func (a CoapApi) fetchCerts(cc CmcConfig) ([][]byte, error) { path := "/TLSCert" // Establish connection - log.Tracef("Contacting cmcd via coap on %v", cc.cmcAddr) - conn, err := udp.Dial(cc.cmcAddr) + log.Tracef("Contacting cmcd via coap on %v", cc.CmcAddr) + conn, err := udp.Dial(cc.CmcAddr) if err != nil { return nil, fmt.Errorf("error dialing: %w", err) } diff --git a/attestedtls/config.go b/attestedtls/config.go index 22c083ae..790dc539 100644 --- a/attestedtls/config.go +++ b/attestedtls/config.go @@ -20,7 +20,6 @@ import ( ar "github.com/Fraunhofer-AISEC/cmc/attestationreport" "github.com/Fraunhofer-AISEC/cmc/cmc" - "github.com/sirupsen/logrus" ) type CmcApiSelect uint32 @@ -47,79 +46,108 @@ const ( // Struct that holds information on cmc address and port // to be used by Listener and DialConfig -type cmcConfig struct { - cmcAddr string - cmcApi CmcApi - network string - ca []byte - policies []byte - mtls bool - attest AttestSelect - result *ar.VerificationResult - cmc *cmc.Cmc +type CmcConfig struct { + CmcAddr string + CmcApi CmcApi + Network string + Ca []byte + Policies []byte + Mtls bool + Attest AttestSelect + Result *ar.VerificationResult + Cmc *cmc.Cmc } type CmcApi interface { - obtainAR(cc cmcConfig, chbindings []byte) ([]byte, error) - verifyAR(chbindings, report []byte, cc cmcConfig) error - fetchSignature(cc cmcConfig, digest []byte, opts crypto.SignerOpts) ([]byte, error) - fetchCerts(cc cmcConfig) ([][]byte, error) + obtainAR(cc CmcConfig, chbindings []byte) ([]byte, error) + verifyAR(chbindings, report []byte, cc CmcConfig) error + fetchSignature(cc CmcConfig, digest []byte, opts crypto.SignerOpts) ([]byte, error) + fetchCerts(cc CmcConfig) ([][]byte, error) } -var cmcApis = map[CmcApiSelect]CmcApi{} +var CmcApis = map[CmcApiSelect]CmcApi{} type ConnectionOption[T any] func(*T) // WithCmcAddress sets the address with which to contact the CMC. // If not specified, default is "localhost" -func WithCmcAddr(address string) ConnectionOption[cmcConfig] { - return func(c *cmcConfig) { - c.cmcAddr = address +func WithCmcAddr(address string) ConnectionOption[CmcConfig] { + return func(c *CmcConfig) { + c.CmcAddr = address } } // WithCmcApi specifies the API to be used to connect to the cmcd // If not specified, default is grpc -func WithCmcApi(api CmcApiSelect) ConnectionOption[cmcConfig] { - return func(c *cmcConfig) { - c.cmcApi = cmcApis[api] +func WithCmcApi(api CmcApiSelect) ConnectionOption[CmcConfig] { + return func(c *CmcConfig) { + c.CmcApi = CmcApis[api] } } // WithCmcNetwork specifies the network type to be used to connect // to the cmcd in case the socket API is selected -func WithCmcNetwork(network string) ConnectionOption[cmcConfig] { - return func(c *cmcConfig) { - c.network = network +func WithCmcNetwork(network string) ConnectionOption[CmcConfig] { + return func(c *CmcConfig) { + c.Network = network } } // WithCmcCa specifies the CA the attestation report should be verified against // in PEM format -func WithCmcCa(pem []byte) ConnectionOption[cmcConfig] { - return func(c *cmcConfig) { - c.ca = pem +func WithCmcCa(pem []byte) ConnectionOption[CmcConfig] { + return func(c *CmcConfig) { + c.Ca = pem } } // WithCmcPolicies specifies optional custom policies the attestation report should // be verified against -func WithCmcPolicies(policies []byte) ConnectionOption[cmcConfig] { - return func(c *cmcConfig) { - c.policies = policies +func WithCmcPolicies(policies []byte) ConnectionOption[CmcConfig] { + return func(c *CmcConfig) { + c.Policies = policies } } // WithMtls specifies whether to perform mutual TLS with mutual attestation // or server-side authentication and attestation only -func WithMtls(mtls bool) ConnectionOption[cmcConfig] { - return func(c *cmcConfig) { - c.mtls = mtls +func WithMtls(mtls bool) ConnectionOption[CmcConfig] { + return func(c *CmcConfig) { + c.Mtls = mtls } } // WithAttest specifies whether to perform mutual, dialer only, or listener only attestation -func WithAttest(mAttest string) ConnectionOption[cmcConfig] { +func WithAttest(mAttest string) ConnectionOption[CmcConfig] { + return func(c *CmcConfig) { + c.Attest = GetAttestMode(mAttest) + } +} + +// WithResult takes an attestation result by reference as input parameter +// and writes the attestation result +func WithResult(result *ar.VerificationResult) ConnectionOption[CmcConfig] { + return func(c *CmcConfig) { + c.Result = result + } +} + +// WithCmc takes a CMC object. This is only required for the Lib API, where +// the CMC is integrated directly into binary (instead of using the cmcd) +func WithCmc(cmc *cmc.Cmc) ConnectionOption[CmcConfig] { + return func(c *CmcConfig) { + c.Cmc = cmc + } +} + +// WithCmc specifies an entire CMC configuration +func WithCmcConfig(cmcConfig *CmcConfig) ConnectionOption[CmcConfig] { + return func(c *CmcConfig) { + *c = *cmcConfig + } +} + +func GetAttestMode(mAttest string) AttestSelect { var selection AttestSelect switch mAttest { case "mutual": @@ -131,26 +159,8 @@ func WithAttest(mAttest string) ConnectionOption[cmcConfig] { case "none": selection = Attest_None default: - logrus.Infoln("No mattest flag set, running default mutual attestation") + log.Info("No mattest flag set, running default mutual attestation") selection = Attest_Mutual } - return func(c *cmcConfig) { - c.attest = selection - } -} - -// WithResult takes an attestation result by reference as input parameter -// and writes the attestation result -func WithResult(result *ar.VerificationResult) ConnectionOption[cmcConfig] { - return func(c *cmcConfig) { - c.result = result - } -} - -// WithCmc takes a CMC object. This is only required for the Lib API, where -// the CMC is integrated directly into binary (instead of using the cmcd) -func WithCmc(cmc *cmc.Cmc) ConnectionOption[cmcConfig] { - return func(c *cmcConfig) { - c.cmc = cmc - } + return selection } diff --git a/attestedtls/dialer.go b/attestedtls/dialer.go index 7712cc2f..a08da411 100644 --- a/attestedtls/dialer.go +++ b/attestedtls/dialer.go @@ -17,6 +17,7 @@ package attestedtls import ( "crypto/tls" + "errors" "fmt" "net" "time" @@ -25,7 +26,11 @@ import ( // Wraps tls.Dial // Additionally performs remote attestation // before returning the established connection. -func Dial(network string, addr string, config *tls.Config, moreConfigs ...ConnectionOption[cmcConfig]) (*tls.Conn, error) { +func Dial(network string, addr string, config *tls.Config, moreConfigs ...ConnectionOption[CmcConfig]) (*tls.Conn, error) { + + if config == nil { + return nil, errors.New("failed to dial. TLS configuration not provided") + } // Create TLS connection var dialer net.Dialer @@ -41,24 +46,27 @@ func Dial(network string, addr string, config *tls.Config, moreConfigs ...Connec } cs := conn.ConnectionState() - log.Tracef("TLS Handshake Complete: %v, generating channel bindings", cs.HandshakeComplete) + if !cs.HandshakeComplete { + return nil, errors.New("internal error: handshake not complete") + } + log.Trace("TLS handshake complete, generating channel bindings") chbindings, err := cs.ExportKeyingMaterial("EXPORTER-Channel-Binding", nil, 32) if err != nil { return nil, fmt.Errorf("failed to export keying material for channel binding: %w", err) } // Get cmc Config: start with defaults - cc := cmcConfig{ - cmcAddr: cmcAddrDefault, - cmcApi: cmcApis[cmcApiSelectDefault], - attest: attestDefault, + cc := CmcConfig{ + CmcAddr: cmcAddrDefault, + CmcApi: CmcApis[cmcApiSelectDefault], + Attest: attestDefault, } for _, c := range moreConfigs { c(&cc) } // Check that selected API is implemented - if cc.cmcApi == nil { + if cc.CmcApi == nil { return nil, fmt.Errorf("selected CMC API is not implemented") } diff --git a/attestedtls/grpc.go b/attestedtls/grpc.go index 3e237571..8d1819e9 100644 --- a/attestedtls/grpc.go +++ b/attestedtls/grpc.go @@ -37,13 +37,13 @@ type GrpcApi struct{} func init() { log.Trace("Adding gRPC API to APIs") - cmcApis[CmcApi_GRPC] = GrpcApi{} + CmcApis[CmcApi_GRPC] = GrpcApi{} } // Creates connection with cmcd at specified address -func getCMCServiceConn(cc cmcConfig) (api.CMCServiceClient, *grpc.ClientConn, context.CancelFunc) { +func getCMCServiceConn(cc CmcConfig) (api.CMCServiceClient, *grpc.ClientConn, context.CancelFunc) { ctx, cancel := context.WithTimeout(context.Background(), timeoutSec*time.Second) - conn, err := grpc.DialContext(ctx, cc.cmcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) + conn, err := grpc.DialContext(ctx, cc.CmcAddr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithBlock()) if err != nil { log.Errorf("failed to connect: %v", err) cancel() @@ -54,10 +54,10 @@ func getCMCServiceConn(cc cmcConfig) (api.CMCServiceClient, *grpc.ClientConn, co } // Obtains attestation report from CMCd -func (a GrpcApi) obtainAR(cc cmcConfig, chbindings []byte) ([]byte, error) { +func (a GrpcApi) obtainAR(cc CmcConfig, chbindings []byte) ([]byte, error) { // Get backend connection - log.Tracef("Obtaining AR from local cmcd on %v", cc.cmcAddr) + log.Tracef("Obtaining AR from local cmcd on %v", cc.CmcAddr) cmcClient, cmcconn, cancel := getCMCServiceConn(cc) if cmcClient == nil { return nil, errors.New("failed to establish connection to obtain AR") @@ -82,9 +82,9 @@ func (a GrpcApi) obtainAR(cc cmcConfig, chbindings []byte) ([]byte, error) { } // Checks Attestation report by calling the CMC to Verify and checking its status response -func (a GrpcApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { +func (a GrpcApi) verifyAR(chbindings, report []byte, cc CmcConfig) error { // Get backend connection - log.Tracef("Verifying remote AR via local cmcd on %v", cc.cmcAddr) + log.Tracef("Verifying remote AR via local cmcd on %v", cc.CmcAddr) cmcClient, conn, cancel := getCMCServiceConn(cc) if cmcClient == nil { return errors.New("failed to establish connection to obtain attestation result") @@ -97,8 +97,8 @@ func (a GrpcApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { req := api.VerificationRequest{ Nonce: chbindings, AttestationReport: report, - Ca: cc.ca, - Policies: cc.policies, + Ca: cc.Ca, + Policies: cc.Policies, } // Perform Verify request resp, err := cmcClient.Verify(context.Background(), &req) @@ -111,24 +111,24 @@ func (a GrpcApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { } // Parse VerificationResult - if cc.result == nil { - cc.result = new(ar.VerificationResult) + if cc.Result == nil { + cc.Result = new(ar.VerificationResult) } - err = json.Unmarshal(resp.GetVerificationResult(), cc.result) + err = json.Unmarshal(resp.GetVerificationResult(), cc.Result) if err != nil { return fmt.Errorf("could not parse verification result: %w", err) } // check results - if !cc.result.Success { + if !cc.Result.Success { return errors.New("attestation report verification failed") } return nil } -func (a GrpcApi) fetchSignature(cc cmcConfig, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +func (a GrpcApi) fetchSignature(cc CmcConfig, digest []byte, opts crypto.SignerOpts) ([]byte, error) { // Get backend connection - log.Tracef("Fetching signature from local cmcd on %v", cc.cmcAddr) + log.Tracef("Fetching signature from local cmcd on %v", cc.CmcAddr) cmcClient, conn, cancel := getCMCServiceConn(cc) if cmcClient == nil { return nil, errors.New("connection failed. No signing performed") @@ -167,9 +167,9 @@ func (a GrpcApi) fetchSignature(cc cmcConfig, digest []byte, opts crypto.SignerO return resp.GetSignedDigest(), nil } -func (a GrpcApi) fetchCerts(cc cmcConfig) ([][]byte, error) { +func (a GrpcApi) fetchCerts(cc CmcConfig) ([][]byte, error) { // Get backend connection - log.Tracef("Fetching certificates from local cmcd on %v", cc.cmcAddr) + log.Tracef("Fetching certificates from local cmcd on %v", cc.CmcAddr) cmcClient, cmcconn, cancel := getCMCServiceConn(cc) if cmcClient == nil { return nil, errors.New("failed to establish connection to cmcd") diff --git a/attestedtls/key.go b/attestedtls/key.go index f8e14057..003ea913 100644 --- a/attestedtls/key.go +++ b/attestedtls/key.go @@ -28,17 +28,17 @@ import ( // PrivateKey Wrapper Implementing crypto.Signer interface // Used to contact cmcd for signing operations type PrivateKey struct { - cmcConfig // embedded struct + CmcConfig // embedded struct pubKey crypto.PublicKey } // Implementation of Sign() in crypto.Signer iface // Contacts cmcd for sign operation and returns received signature func (priv PrivateKey) Sign(random io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { - if priv.cmcConfig.cmcApi == nil { + if priv.CmcConfig.CmcApi == nil { return nil, errors.New("failed to get CMC API: nil") } - return priv.cmcConfig.cmcApi.fetchSignature(priv.cmcConfig, digest, opts) + return priv.CmcConfig.CmcApi.fetchSignature(priv.CmcConfig, digest, opts) } func (priv PrivateKey) Public() crypto.PublicKey { @@ -46,25 +46,25 @@ func (priv PrivateKey) Public() crypto.PublicKey { } // Obtains Certificate for the Identity Key (IK) used for the connection from cmcd -func GetCert(moreConfigs ...ConnectionOption[cmcConfig]) (tls.Certificate, error) { +func GetCert(moreConfigs ...ConnectionOption[CmcConfig]) (tls.Certificate, error) { var tlsCert tls.Certificate // Get cmc Config: start with defaults - cc := cmcConfig{ - cmcAddr: cmcAddrDefault, - cmcApi: cmcApis[cmcApiSelectDefault], - attest: attestDefault, + cc := CmcConfig{ + CmcAddr: cmcAddrDefault, + CmcApi: CmcApis[cmcApiSelectDefault], + Attest: attestDefault, } for _, c := range moreConfigs { c(&cc) } // Check that selected API is implemented - if cc.cmcApi == nil { + if cc.CmcApi == nil { return tls.Certificate{}, fmt.Errorf("selected CMC API is not implemented") } - certs, err := cc.cmcApi.fetchCerts(cc) + certs, err := cc.CmcApi.fetchCerts(cc) if err != nil { return tls.Certificate{}, fmt.Errorf("failed to fetch certificate from cmc: %w", err) } @@ -100,7 +100,7 @@ func GetCert(moreConfigs ...ConnectionOption[cmcConfig]) (tls.Certificate, error tlsCert.Leaf = x509Cert tlsCert.PrivateKey = PrivateKey{ pubKey: x509Cert.PublicKey, - cmcConfig: cc, + CmcConfig: cc, } // Return cert diff --git a/attestedtls/libapi.go b/attestedtls/libapi.go index 7a904157..8ad8f678 100644 --- a/attestedtls/libapi.go +++ b/attestedtls/libapi.go @@ -32,29 +32,29 @@ type LibApi struct{} func init() { log.Trace("Adding Lib API to APIs") - cmcApis[CmcApi_Lib] = LibApi{} + CmcApis[CmcApi_Lib] = LibApi{} } // Obtains attestation report from CMCd -func (a LibApi) obtainAR(cc cmcConfig, chbindings []byte) ([]byte, error) { +func (a LibApi) obtainAR(cc CmcConfig, chbindings []byte) ([]byte, error) { - if cc.cmc == nil { + if cc.Cmc == nil { return nil, errors.New("internal error: cmc is nil") } - if len(cc.cmc.Drivers) == 0 { + if len(cc.Cmc.Drivers) == 0 { return nil, errors.New("no drivers configured") } log.Debug("Prover: Generating Attestation Report with nonce: ", hex.EncodeToString(chbindings)) - report, err := ar.Generate(chbindings, cc.cmc.Metadata, cc.cmc.Drivers, cc.cmc.Serializer) + report, err := ar.Generate(chbindings, cc.Cmc.Metadata, cc.Cmc.Drivers, cc.Cmc.Serializer) if err != nil { return nil, fmt.Errorf("failed to generate attestation report: %w", err) } log.Debug("Prover: Signing Attestation Report") - signedReport, err := ar.Sign(report, cc.cmc.Drivers[0], cc.cmc.Serializer) + signedReport, err := ar.Sign(report, cc.Cmc.Drivers[0], cc.Cmc.Serializer) if err != nil { return nil, errors.New("prover: failed to sign Attestion Report ") } @@ -63,13 +63,13 @@ func (a LibApi) obtainAR(cc cmcConfig, chbindings []byte) ([]byte, error) { } // Checks Attestation report by calling the CMC to Verify and checking its status response -func (a LibApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { +func (a LibApi) verifyAR(chbindings, report []byte, cc CmcConfig) error { log.Debug("Verifier: Verifying Attestation Report") - result := ar.Verify(report, chbindings, cc.ca, nil, cc.cmc.PolicyEngineSelect, cc.cmc.IntelStorage) + result := ar.Verify(report, chbindings, cc.Ca, nil, cc.Cmc.PolicyEngineSelect, cc.Cmc.IntelStorage) - if cc.result != nil { - *cc.result = result + if cc.Result != nil { + *cc.Result = result } if !result.Success { @@ -78,14 +78,14 @@ func (a LibApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { return nil } -func (a LibApi) fetchSignature(cc cmcConfig, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +func (a LibApi) fetchSignature(cc CmcConfig, digest []byte, opts crypto.SignerOpts) ([]byte, error) { - if len(cc.cmc.Drivers) == 0 { + if len(cc.Cmc.Drivers) == 0 { return nil, errors.New("no drivers configured") } // Get key handle from (hardware) interface - tlsKeyPriv, _, err := cc.cmc.Drivers[0].GetSigningKeys() + tlsKeyPriv, _, err := cc.Cmc.Drivers[0].GetSigningKeys() if err != nil { return nil, fmt.Errorf("failed to get IK: %w", err) } @@ -100,17 +100,17 @@ func (a LibApi) fetchSignature(cc cmcConfig, digest []byte, opts crypto.SignerOp return signature, nil } -func (a LibApi) fetchCerts(cc cmcConfig) ([][]byte, error) { +func (a LibApi) fetchCerts(cc CmcConfig) ([][]byte, error) { - if cc.cmc == nil { + if cc.Cmc == nil { return nil, errors.New("internal error: cmc is nil") } - if len(cc.cmc.Drivers) == 0 { + if len(cc.Cmc.Drivers) == 0 { return nil, errors.New("no drivers configured") } - certChain, err := cc.cmc.Drivers[0].GetCertChain() + certChain, err := cc.Cmc.Drivers[0].GetCertChain() if err != nil { return nil, fmt.Errorf("failed to get cert chain: %w", err) } diff --git a/attestedtls/listener.go b/attestedtls/listener.go index 26a51297..c82df5d4 100644 --- a/attestedtls/listener.go +++ b/attestedtls/listener.go @@ -29,7 +29,7 @@ const timeout = 10 * time.Second * holds net.Listener and adds additional functionality to it */ type Listener struct { net.Listener // embedded interface - cmcConfig // embedded struct + CmcConfig // embedded struct *tls.Config // embedded struct } @@ -74,7 +74,7 @@ func (ln Listener) Accept() (net.Conn, error) { // Perform remote attestation with unique channel binding as specified in RFC5056, // RFC5705, and RFC9266 - err = attestListener(tlsConn, chbindings, ln.cmcConfig) + err = attestListener(tlsConn, chbindings, ln.CmcConfig) if err != nil { return nil, fmt.Errorf("remote attestation failed: %w", err) } @@ -99,24 +99,24 @@ func (ln Listener) Addr() net.Addr { // Wrapper for tls.Listen // Returns custom Listener that will perform additional remote attestation // operations right after successful TLS connection establishment -func Listen(network, laddr string, config *tls.Config, moreConfigs ...ConnectionOption[cmcConfig]) (net.Listener, error) { +func Listen(network, laddr string, config *tls.Config, moreConfigs ...ConnectionOption[CmcConfig]) (net.Listener, error) { // Default listener listener := Listener{ - cmcConfig: cmcConfig{ - cmcAddr: cmcAddrDefault, - cmcApi: cmcApis[cmcApiSelectDefault], - attest: attestDefault, + CmcConfig: CmcConfig{ + CmcAddr: cmcAddrDefault, + CmcApi: CmcApis[cmcApiSelectDefault], + Attest: attestDefault, }, } // Apply all additional (CMC) configs for _, c := range moreConfigs { - c(&listener.cmcConfig) + c(&listener.CmcConfig) } // Check that selected API is implemented - if listener.cmcConfig.cmcApi == nil { + if listener.CmcConfig.CmcApi == nil { return listener, fmt.Errorf("selected CMC API is not implemented") } diff --git a/attestedtls/socket.go b/attestedtls/socket.go index af3748f2..25304eb7 100644 --- a/attestedtls/socket.go +++ b/attestedtls/socket.go @@ -36,15 +36,15 @@ type SocketApi struct{} func init() { log.Trace("Adding Socket API to APIs") - cmcApis[CmcApi_Socket] = SocketApi{} + CmcApis[CmcApi_Socket] = SocketApi{} } // Obtains attestation report from cmcd -func (a SocketApi) obtainAR(cc cmcConfig, chbindings []byte) ([]byte, error) { +func (a SocketApi) obtainAR(cc CmcConfig, chbindings []byte) ([]byte, error) { // Establish connection - log.Tracef("Contacting cmcd via %v on %v", cc.network, cc.cmcAddr) - conn, err := net.Dial(cc.network, cc.cmcAddr) + log.Tracef("Contacting cmcd via %v on %v", cc.Network, cc.CmcAddr) + conn, err := net.Dial(cc.Network, cc.CmcAddr) if err != nil { return nil, fmt.Errorf("error dialing: %w", err) } @@ -82,11 +82,11 @@ func (a SocketApi) obtainAR(cc cmcConfig, chbindings []byte) ([]byte, error) { } // Sends attestationreport to cmcd for verification -func (a SocketApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { +func (a SocketApi) verifyAR(chbindings, report []byte, cc CmcConfig) error { // Establish connection - log.Tracef("Contacting cmcd via %v on %v", cc.network, cc.cmcAddr) - conn, err := net.Dial(cc.network, cc.cmcAddr) + log.Tracef("Contacting cmcd via %v on %v", cc.Network, cc.CmcAddr) + conn, err := net.Dial(cc.Network, cc.CmcAddr) if err != nil { return fmt.Errorf("error dialing: %w", err) } @@ -95,8 +95,8 @@ func (a SocketApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { req := &api.VerificationRequest{ Nonce: chbindings, AttestationReport: report, - Ca: cc.ca, - Policies: cc.policies, + Ca: cc.Ca, + Policies: cc.Policies, } payload, err := cbor.Marshal(req) if err != nil { @@ -123,26 +123,26 @@ func (a SocketApi) verifyAR(chbindings, report []byte, cc cmcConfig) error { } // Parse VerificationResult - if cc.result == nil { - cc.result = new(ar.VerificationResult) + if cc.Result == nil { + cc.Result = new(ar.VerificationResult) } - err = json.Unmarshal(verifyResp.VerificationResult, cc.result) + err = json.Unmarshal(verifyResp.VerificationResult, cc.Result) if err != nil { return fmt.Errorf("could not parse verification result: %w", err) } // Check results - if !cc.result.Success { + if !cc.Result.Success { return errors.New("attestation report verification failed") } return nil } -func (a SocketApi) fetchSignature(cc cmcConfig, digest []byte, opts crypto.SignerOpts) ([]byte, error) { +func (a SocketApi) fetchSignature(cc CmcConfig, digest []byte, opts crypto.SignerOpts) ([]byte, error) { // Establish connection - log.Tracef("Contacting cmcd via %v on %v", cc.network, cc.cmcAddr) - conn, err := net.Dial(cc.network, cc.cmcAddr) + log.Tracef("Contacting cmcd via %v on %v", cc.Network, cc.CmcAddr) + conn, err := net.Dial(cc.Network, cc.CmcAddr) if err != nil { return nil, fmt.Errorf("error dialing: %w", err) } @@ -190,11 +190,11 @@ func (a SocketApi) fetchSignature(cc cmcConfig, digest []byte, opts crypto.Signe return signResp.SignedContent, nil } -func (a SocketApi) fetchCerts(cc cmcConfig) ([][]byte, error) { +func (a SocketApi) fetchCerts(cc CmcConfig) ([][]byte, error) { // Establish connection - log.Tracef("Contacting cmcd via %v on %v", cc.network, cc.cmcAddr) - conn, err := net.Dial(cc.network, cc.cmcAddr) + log.Tracef("Contacting cmcd via %v on %v", cc.Network, cc.CmcAddr) + conn, err := net.Dial(cc.Network, cc.CmcAddr) if err != nil { return nil, fmt.Errorf("error dialing: %w", err) } diff --git a/doc/configuration.md b/doc/configuration.md index fafd95c9..9c7730d9 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -64,7 +64,7 @@ relevant if vcekOfflineCaching is set to true) ## Testtool Configuration -- **mode**: The mode to run. Possible are generate, verify, dial, listen, cacerts and iothub +- **mode**: The mode to run. Possible are generate, verify, dial, listen, request, serve, cacerts and iothub. See below for an explanation of these modes - **addr**: List of addresses to connect to in mode dial and anddress to serve in mode listen. - **cmc**: The address of the CMC server - **report**: The file to store the attestation report in (mode generate) or to retrieve @@ -82,6 +82,9 @@ from (mode verify) The interval format has to be in accordance with the input format of Go's [`time.Duration`](https://pkg.go.dev/time#ParseDuration). - **publish**: Optional HTTP address to publish attestation results to +-**header**: Only for mode `request`. One or multiple (comma-separated) HTTP headers can be specified in the format `key: value`, e.g. *Content-Type: application/json,Content-Transfer-Encoding: base64* +-**method**: Only for mode `request`. Specifies the HTTP method. Possible are `GET`, `POST`, `PUT` and `HEADER` +-**data**: Only for mode `request` with `POST` or `PUT` method. Specifies data to send to the demo server as a string Further configuration options are only relevant if the testtool is operated with the `lib` API, i.e., standalone without the *cmcd* running as a separate binary: @@ -108,6 +111,8 @@ TPM or software keys. In case of the TPM, the TPM *Credential Activation* proces - **verify**: Verifies a previously generated attestation report - **dial**: Run attestedTLS client application - **listen**: Serve as a attestedTLS echo server +- **request**: Performs one or multiple attested HTTPS requests (client) +- **serve**: Run attested HTTPS demo server ## Platform Configuration diff --git a/testtool/certs.go b/testtool/certs.go new file mode 100644 index 00000000..4a97e9ab --- /dev/null +++ b/testtool/certs.go @@ -0,0 +1,49 @@ +// Copyright (c) 2021 Fraunhofer AISEC +// Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. +// +// 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 main + +// Install github packages with "go get [url]" +import ( + "os" + + // local modules + + est "github.com/Fraunhofer-AISEC/cmc/est/estclient" + "github.com/Fraunhofer-AISEC/cmc/internal" +) + +func getCaCertsInternal(c *config) { + addr := "" + if len(c.Addr) > 0 { + addr = c.Addr[0] + } + + log.Info("Retrieving CA certs") + client := est.NewClient(nil) + certs, err := client.CaCerts(addr) + if err != nil { + log.Fatalf("Failed to retrieve certificates: %v", err) + } + log.Debug("Received certs:") + for _, c := range certs { + log.Debugf("\t%v", c.Subject.CommonName) + } + // Store CA certificate + err = os.WriteFile(c.CaFile, internal.WriteCertPem(certs[len(certs)-1]), 0644) + if err != nil { + log.Fatalf("Failed to store CA certificate: %v", err) + } +} diff --git a/testtool/coap.go b/testtool/coap.go index 58c708da..8e9b080d 100644 --- a/testtool/coap.go +++ b/testtool/coap.go @@ -146,13 +146,21 @@ func (a CoapApi) listen(c *config) { listenInternal(c, attestedtls.CmcApi_COAP, nil) } +func (a CoapApi) request(c *config) { + requestInternal(c, attestedtls.CmcApi_COAP, nil) +} + +func (a CoapApi) serve(c *config) { + serveInternal(c, attestedtls.CmcApi_COAP, nil) +} + func (a CoapApi) cacerts(c *config) { getCaCertsInternal(c) } func (a CoapApi) iothub(c *config) { // Start coap server - err := serve(c) + err := serveHub(c) if err != nil { log.Fatalf("Failed to serve: %v", err) } diff --git a/testtool/commands.go b/testtool/commands.go index 7838c678..626c3761 100644 --- a/testtool/commands.go +++ b/testtool/commands.go @@ -35,6 +35,14 @@ func listen(c *config) { c.api.listen(c) } +func request(c *config) { + c.api.request(c) +} + +func serve(c *config) { + c.api.serve(c) +} + func getCaCerts(c *config) { c.api.cacerts(c) } diff --git a/testtool/config.go b/testtool/config.go index 9f886c03..cf797020 100644 --- a/testtool/config.go +++ b/testtool/config.go @@ -58,6 +58,8 @@ type Api interface { listen(c *config) iothub(c *config) cacerts(c *config) + request(c *config) + serve(c *config) } type config struct { @@ -82,6 +84,9 @@ type config struct { Storage string `json:"storage"` Cache string `json:"cache"` MeasurementLog bool `json:"measurementLog"` + Header []string `json:"header"` + Method string `json:"method"` + Data string `json:"data"` ca []byte policies []byte @@ -112,6 +117,9 @@ const ( storageFlag = "storage" cacheFlag = "cache" measurementLogFlag = "measurementLog" + headerFlag = "header" + methodFlag = "method" + dataFlag = "data" ) func getConfig() *config { @@ -153,6 +161,9 @@ func getConfig() *config { cache := flag.String(cacheFlag, "", "Optional folder to cache metadata for offline backup (only for libapi)") measurementLog := flag.Bool(measurementLogFlag, false, "Indicates whether to include measured events in measurement and validation report") + headers := flag.String(headerFlag, "", "Set header for HTTP POST requests") + method := flag.String(methodFlag, "", "Set HTTP request method (GET, POST, PUT, HEADER)") + data := flag.String(dataFlag, "", "Set HTTP body for POST and PUT requests") flag.Parse() @@ -167,6 +178,7 @@ func getConfig() *config { Api: "grpc", LogLevel: "info", IntervalStr: "0s", + Method: "GET", } // Obtain custom configuration from file if specified @@ -247,6 +259,15 @@ func getConfig() *config { if internal.FlagPassed(measurementLogFlag) { c.MeasurementLog = *measurementLog } + if internal.FlagPassed(headerFlag) { + c.Header = strings.Split(*headers, ",") + } + if internal.FlagPassed(methodFlag) { + c.Method = *method + } + if internal.FlagPassed(dataFlag) { + c.Data = *data + } intervalDuration, err := time.ParseDuration(c.IntervalStr) if err != nil { diff --git a/testtool/grpc.go b/testtool/grpc.go index 76bb3606..d23fa35d 100644 --- a/testtool/grpc.go +++ b/testtool/grpc.go @@ -142,6 +142,14 @@ func (a GrpcApi) listen(c *config) { listenInternal(c, attestedtls.CmcApi_GRPC, nil) } +func (a GrpcApi) request(c *config) { + requestInternal(c, attestedtls.CmcApi_GRPC, nil) +} + +func (a GrpcApi) serve(c *config) { + serveInternal(c, attestedtls.CmcApi_GRPC, nil) +} + func (a GrpcApi) cacerts(c *config) { getCaCertsInternal(c) } diff --git a/testtool/http.go b/testtool/http.go new file mode 100644 index 00000000..d15575ed --- /dev/null +++ b/testtool/http.go @@ -0,0 +1,312 @@ +// Copyright (c) 2021 Fraunhofer AISEC +// Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. +// +// 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 main + +// Install github packages with "go get [url]" +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "fmt" + "io" + "net/http" + "strings" + "time" + + // local modules + + ahttp "github.com/Fraunhofer-AISEC/cmc/attestedhttp" + atls "github.com/Fraunhofer-AISEC/cmc/attestedtls" + "github.com/Fraunhofer-AISEC/cmc/cmc" + "github.com/Fraunhofer-AISEC/cmc/internal" +) + +// HTTP header constants +const ( + ContentTypeHeader = "Content-Type" +) + +// MIME type constants. +const ( + MimeParamBoundary = "boundary" + MimeParamSMIMEType = "smime-type" + MimeTypeJSON = "application/json" + MimeTypeOctetStream = "application/octet-stream" + MimeTypeTextPlain = "text/plain" + MimeTypeTextPlainUTF8 = "text/plain; charset=utf-8" +) + +// Creates an attested HTTPS connection and performs the specified requests +func requestInternal(c *config, api atls.CmcApiSelect, cmc *cmc.Cmc) error { + + // Add root CA + roots := x509.NewCertPool() + success := roots.AppendCertsFromPEM(c.ca) + if !success { + log.Fatal("Could not add cert to root CAs") + } + + // Load certificate from CMC if mutual TLS is activated and + // create basic TLS configuration for aTLS + var tlsConfig *tls.Config + log.Debug("Creating aTLS configuration") + if c.Mtls { + cert, err := atls.GetCert( + atls.WithCmcAddr(c.CmcAddr), + atls.WithCmcApi(api), + atls.WithCmcNetwork(c.Network), + atls.WithCmc(cmc)) + if err != nil { + log.Fatalf("failed to get TLS Certificate: %v", err) + } + // Create TLS config with root CA and own certificate for authentication + tlsConfig = &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: roots, + } + } else { + // Create TLS config with root CA only + tlsConfig = &tls.Config{ + RootCAs: roots, + Renegotiation: tls.RenegotiateNever, + } + } + internal.PrintTlsConfig(tlsConfig, c.ca) + + // Create an attested HTTP Transport structure. This is a wrapper around http.Transport, + // look for the descriptions of the parameters there. Additionally, the aTLS parameters + // must be configured + transport := &ahttp.Transport{ + IdleConnTimeout: 60 * time.Second, + TLSClientConfig: tlsConfig, + + Attest: c.Attest, + MutualTls: c.Mtls, + CmcAddr: c.CmcAddr, + CmcApi: api, + CmcNetwork: c.Network, + Cmc: cmc, + Ca: c.ca, + CmcPolicies: c.policies, + } + + // Create an attested HTTPS Client + client := &ahttp.Client{Transport: transport} + + for _, addr := range c.Addr { + + log.Debugf("HTTP %v request to %v", c.Method, addr) + + // Create a new HTTP request and add body in case of POST or PUT request + var body io.Reader + if strings.EqualFold(c.Method, "POST") || strings.EqualFold(c.Method, "PUT") { + body = io.NopCloser(bytes.NewBuffer([]byte(c.Data))) + } + req, err := http.NewRequest(c.Method, addr, body) + if err != nil { + return fmt.Errorf("failed to make new HTTP request: %w", err) + } + + // Set the user specified HTTP headers + log.Tracef("Number of specified headers: %v", len(c.Header)) + for _, h := range c.Header { + s := strings.SplitN(h, ":", 2) + if len(s) != 2 { + return fmt.Errorf("invalid header %v", h) + } + log.Tracef("Setting header '%v: %v'", s[0], s[1]) + req.Header.Set(s[0], s[1]) + } + + // Perform the actual, user specified request + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("HTTP %v failed: %w", c.Method, err) + } + defer resp.Body.Close() + + log.Debug("Response Status: ", resp.Status) + if resp.StatusCode != 200 { + log.Warnf("HTTP request returned status %v", resp.Status) + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + log.Warn("Failed to read response") + } + log.Trace("Content: ", string(content)) + } + + return nil +} + +func serveInternal(c *config, api atls.CmcApiSelect, cmc *cmc.Cmc) { + + // Add root CA + roots := x509.NewCertPool() + success := roots.AppendCertsFromPEM(c.ca) + if !success { + log.Fatal("Could not add cert to root CAs") + } + + // Load certificate from CMC + cert, err := atls.GetCert( + atls.WithCmcAddr(c.CmcAddr), + atls.WithCmcApi(api), + atls.WithCmcNetwork(c.Network), + atls.WithCmc(cmc)) + if err != nil { + log.Fatalf("failed to get TLS Certificate: %v", err) + } + + var clientAuth tls.ClientAuthType + if c.Mtls { + // Mandate client authentication + clientAuth = tls.RequireAndVerifyClientCert + } else { + // Make client authentication optional + clientAuth = tls.VerifyClientCertIfGiven + } + + // Overwrite specified TLS config to enforce aTLS as configured + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: clientAuth, + ClientCAs: roots, + Renegotiation: tls.RenegotiateNever, + } + + internal.PrintTlsConfig(tlsConfig, c.ca) + + // Config allows to specify more than one address for dialing, + // always use first address for listening + addr := "" + if len(c.Addr) > 0 { + addr = c.Addr[0] + } + + // Create an attested HTTP server. The inner http.Server can be + // configured as usual. Additionally, aTLS parameters must be + // specified + server := &ahttp.Server{ + Server: &http.Server{ + Addr: addr, + TLSConfig: tlsConfig, + }, + Attest: c.Attest, + MutualTls: c.Mtls, + CmcAddr: c.CmcAddr, + CmcApi: api, + CmcNetwork: c.Network, + Cmc: cmc, + Ca: c.ca, + CmcPolicies: c.policies, + } + + // Use the golang net/http module functions to configure the server + // as usual + http.HandleFunc("/", handleRootRequest) + http.HandleFunc("/data", handleDataRequest) + http.HandleFunc("/post", handlePostRequest) + + // Call the attested ListenAndServe method from the attested HTTP server + // to run the server + err = server.ListenAndServe() + if err != nil { + log.Fatalf("Failed to serve HTTPS: %v", err) + } +} + +// Just an example HTTP handler +func handleRootRequest(w http.ResponseWriter, req *http.Request) { + log.Tracef("Received '/' %v request from %v", req.Method, req.RemoteAddr) + w.Header().Set("Content-Type", "text/html") + + log.Tracef("Sending HTTP response") + fmt.Fprintf(w, "

Hello from HTML

") +} + +// Just an example HTTP handler +func handleDataRequest(w http.ResponseWriter, req *http.Request) { + + log.Tracef("Received '/data' %v request from %v", req.Method, req.RemoteAddr) + + if strings.Compare(req.Method, "GET") != 0 { + writeHttpErrorf(w, "Method %v not implemented for /post request", req.Method) + return + } + + for k, v := range req.Header { + log.Tracef("HTTP Header %v: %v", k, v) + } + + payload := []byte(`{"test": "hello from json"}`) + + w.Header().Set(ContentTypeHeader, MimeTypeJSON) + w.WriteHeader(http.StatusOK) + + log.Tracef("Sending HTTP response") + n, err := w.Write(payload) + if err != nil { + log.Warnf("failed to write HTTP: %v", err) + } + if n != len(payload) { + log.Warnf("Failed to handle cacerts request: Only %v of %v bytes sent", n, len(payload)) + } +} + +// Just an example HTTP handler +func handlePostRequest(w http.ResponseWriter, req *http.Request) { + + log.Tracef("Received '/post' %v request from %v", req.Method, req.RemoteAddr) + + if strings.Compare(req.Method, "POST") != 0 { + writeHttpErrorf(w, "Method %v not implemented for /post request", req.Method) + return + } + + for k, v := range req.Header { + log.Tracef("HTTP Header %v: %v", k, v) + } + + payload, err := io.ReadAll(req.Body) + if err != nil { + writeHttpErrorf(w, "failed to read payload from request: %v", err) + return + } + + log.Tracef("Received from client: %v", string(payload)) + + payload = []byte(`{"test": "hello from json"}`) + + w.Header().Set(ContentTypeHeader, MimeTypeJSON) + w.WriteHeader(http.StatusOK) + + log.Tracef("Sending HTTP response") + n, err := w.Write(payload) + if err != nil { + log.Warnf("failed to write HTTP: %v", err) + } + if n != len(payload) { + log.Warnf("Failed to handle cacerts request: Only %v of %v bytes sent", n, len(payload)) + } +} + +func writeHttpErrorf(w http.ResponseWriter, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + log.Warn(msg) + http.Error(w, msg, http.StatusBadRequest) +} diff --git a/testtool/hub.go b/testtool/hub.go index b51685d7..0b12a07d 100644 --- a/testtool/hub.go +++ b/testtool/hub.go @@ -44,7 +44,7 @@ func loggingMiddleware(next mux.Handler) mux.Handler { }) } -func serve(c *config) error { +func serveHub(c *config) error { conf = c diff --git a/testtool/libapi.go b/testtool/libapi.go index f7db8c2f..8bdc43aa 100644 --- a/testtool/libapi.go +++ b/testtool/libapi.go @@ -161,6 +161,32 @@ func (a LibApi) listen(c *config) { listenInternal(c, attestedtls.CmcApi_Lib, a.cmc) } +func (a LibApi) request(c *config) { + if a.cmc == nil { + cmc, err := initialize(c) + if err != nil { + log.Errorf("failed to initialize CMC: %v", err) + return + } + a.cmc = cmc + } + + requestInternal(c, attestedtls.CmcApi_Lib, a.cmc) +} + +func (a LibApi) serve(c *config) { + if a.cmc == nil { + cmc, err := initialize(c) + if err != nil { + log.Errorf("failed to initialize CMC: %v", err) + return + } + a.cmc = cmc + } + + serveInternal(c, attestedtls.CmcApi_Lib, a.cmc) +} + func (a LibApi) iothub(c *config) { log.Fatalf("IoT hub not implemented for lib API") } diff --git a/testtool/main.go b/testtool/main.go index b6b589af..5d370d06 100644 --- a/testtool/main.go +++ b/testtool/main.go @@ -27,6 +27,8 @@ var ( "verify": verify, // Verify an attestation report "dial": dial, // Act as client to establish an attested TLS connection "listen": listen, // Act as server in etsblishing attested TLS connections + "request": request, // Perform an attested HTTPS request + "serve": serve, // Establish an attested HTTPS server "cacerts": getCaCerts, // Retrieve CA certs from EST server "iothub": iothub, // Simulate an IoT hub for Cortex-M IAS Attestation Demo } diff --git a/testtool/publish.go b/testtool/publish.go new file mode 100644 index 00000000..65a00b3b --- /dev/null +++ b/testtool/publish.go @@ -0,0 +1,127 @@ +// Copyright (c) 2021 Fraunhofer AISEC +// Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. +// +// 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 main + +// Install github packages with "go get [url]" +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "sync" + "time" + + // local modules + ar "github.com/Fraunhofer-AISEC/cmc/attestationreport" +) + +func publishResultAsync(addr string, result *ar.VerificationResult, wg *sync.WaitGroup) { + defer wg.Done() + publishResult(addr, result) +} + +func publishResult(addr string, result *ar.VerificationResult) { + + if result.Prover == "" { + log.Trace("Will not publish result: prover is empty (this happens if connection could not be established)") + return + } + if addr == "" { + log.Trace("Will not publish: no address specified") + return + } + + log.Tracef("Publishing result to '%v'", addr) + + data, err := json.Marshal(*result) + if err != nil { + log.Tracef("Failed to marshal result: %v", err) + return + } + + err = publish(addr, data) + if err != nil { + log.Tracef("Failed to publish: %v", err) + return + } +} + +func publish(addr string, result []byte) error { + + if addr == "" { + return nil + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, addr, bytes.NewBuffer(result)) + if err != nil { + return fmt.Errorf("failed to create new http request with context: %w", err) + } + req.Header.Set("Content-Type", "application/json") + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return fmt.Errorf("http post request failed: %w", err) + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != 201 { + return fmt.Errorf("failed to publish result: server responded with %v: %v", + resp.Status, string(data)) + } + + log.Tracef("Successfully published result: server responded with %v", resp.Status) + + return nil +} + +func saveResult(file, addr string, result []byte) error { + + // Convert to human readable + var out bytes.Buffer + json.Indent(&out, result, "", " ") + + // Save the Attestation Result to file + if file != "" { + os.WriteFile(file, out.Bytes(), 0644) + fmt.Println("Wrote file ", file) + } else { + log.Debug("No config file specified: will not save attestation report") + } + + // Publish the attestation result if publishing address was specified + if addr != "" { + err := publish(addr, result) + if err != nil { + log.Warnf("failed to publish result: %v", err) + } + log.Debug("Published attestation report") + } else { + log.Debug("No publish address specified: will not publish attestation report") + } + + return nil +} diff --git a/testtool/socket.go b/testtool/socket.go index a842aacc..4e8584c3 100644 --- a/testtool/socket.go +++ b/testtool/socket.go @@ -138,6 +138,14 @@ func (a SocketApi) listen(c *config) { listenInternal(c, attestedtls.CmcApi_Socket, nil) } +func (a SocketApi) request(c *config) { + requestInternal(c, attestedtls.CmcApi_Socket, nil) +} + +func (a SocketApi) serve(c *config) { + serveInternal(c, attestedtls.CmcApi_Socket, nil) +} + func (a SocketApi) cacerts(c *config) { getCaCertsInternal(c) } diff --git a/testtool/internal.go b/testtool/tls.go similarity index 68% rename from testtool/internal.go rename to testtool/tls.go index ad855d7e..76080079 100644 --- a/testtool/internal.go +++ b/testtool/tls.go @@ -18,16 +18,10 @@ package main // Install github packages with "go get [url]" import ( "bufio" - "bytes" - "context" "crypto/tls" "crypto/x509" - "encoding/json" "fmt" - "io" "net" - "net/http" - "os" "sync" "time" @@ -35,7 +29,6 @@ import ( ar "github.com/Fraunhofer-AISEC/cmc/attestationreport" atls "github.com/Fraunhofer-AISEC/cmc/attestedtls" "github.com/Fraunhofer-AISEC/cmc/cmc" - est "github.com/Fraunhofer-AISEC/cmc/est/estclient" "github.com/Fraunhofer-AISEC/cmc/internal" ) @@ -61,9 +54,9 @@ func dialInternalAddr(c *config, api atls.CmcApiSelect, addr string, tlsConf *tl wg.Add(1) defer wg.Wait() go publishResultAsync(c.Publish, verificationResult, wg) - if err != nil { - return fmt.Errorf("failed to dial server: %v", err) - } + } + if err != nil { + return fmt.Errorf("failed to dial server: %v", err) } defer conn.Close() _ = conn.SetReadDeadline(time.Now().Add(timeoutSec * time.Second)) @@ -240,29 +233,6 @@ func listenInternal(c *config, api atls.CmcApiSelect, cmc *cmc.Cmc) { } } -func getCaCertsInternal(c *config) { - addr := "" - if len(c.Addr) > 0 { - addr = c.Addr[0] - } - - log.Info("Retrieving CA certs") - client := est.NewClient(nil) - certs, err := client.CaCerts(addr) - if err != nil { - log.Fatalf("Failed to retrieve certificates: %v", err) - } - log.Debug("Received certs:") - for _, c := range certs { - log.Debugf("\t%v", c.Subject.CommonName) - } - // Store CA certificate - err = os.WriteFile(c.CaFile, internal.WriteCertPem(certs[len(certs)-1]), 0644) - if err != nil { - log.Fatalf("Failed to store CA certificate: %v", err) - } -} - // Simply acts as an echo server and returns the received string func handleConnection(conn net.Conn) { defer conn.Close() @@ -280,98 +250,3 @@ func handleConnection(conn net.Conn) { log.Errorf("Failed to write: %v", err) } } - -func publishResultAsync(addr string, result *ar.VerificationResult, wg *sync.WaitGroup) { - defer wg.Done() - publishResult(addr, result) -} - -func publishResult(addr string, result *ar.VerificationResult) { - - if result.Prover == "" { - log.Trace("Will not publish result: prover is empty (this happens if connection could not be established)") - return - } - if addr == "" { - log.Trace("Will not publish: no address specified") - return - } - - log.Tracef("Publishing result to '%v'", addr) - - data, err := json.Marshal(*result) - if err != nil { - log.Tracef("Failed to marshal result: %v", err) - return - } - - err = publish(addr, data) - if err != nil { - log.Tracef("Failed to publish: %v", err) - return - } -} - -func publish(addr string, result []byte) error { - - if addr == "" { - return nil - } - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, http.MethodPost, addr, bytes.NewBuffer(result)) - if err != nil { - return fmt.Errorf("failed to create new http request with context: %w", err) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return fmt.Errorf("http post request failed: %w", err) - } - defer resp.Body.Close() - - data, err := io.ReadAll(resp.Body) - if err != nil { - return fmt.Errorf("failed to read response: %w", err) - } - - if resp.StatusCode != 201 { - return fmt.Errorf("failed to publish result: server responded with %v: %v", - resp.Status, string(data)) - } - - log.Tracef("Successfully published result: server responded with %v", resp.Status) - - return nil -} - -func saveResult(file, addr string, result []byte) error { - - // Convert to human readable - var out bytes.Buffer - json.Indent(&out, result, "", " ") - - // Save the Attestation Result to file - if file != "" { - os.WriteFile(file, out.Bytes(), 0644) - fmt.Println("Wrote file ", file) - } else { - log.Debug("No config file specified: will not save attestation report") - } - - // Publish the attestation result if publishing address was specified - if addr != "" { - err := publish(addr, result) - if err != nil { - log.Warnf("failed to publish result: %v", err) - } - log.Debug("Published attestation report") - } else { - log.Debug("No publish address specified: will not publish attestation report") - } - - return nil -}