github.com/google/trillian-examples@v0.0.0-20240520080811-0d40d35cef0e/binary_transparency/firmware/internal/client/client.go (about)

     1  // Copyright 2020 Google LLC. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package client
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/base64"
    20  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"mime/multipart"
    24  	"net/http"
    25  	"net/textproto"
    26  	"net/url"
    27  
    28  	"github.com/golang/glog"
    29  	"github.com/google/trillian-examples/binary_transparency/firmware/api"
    30  	"github.com/transparency-dev/merkle/rfc6962"
    31  	"golang.org/x/mod/sumdb/note"
    32  	"google.golang.org/grpc/status"
    33  )
    34  
    35  // ReadonlyClient is an HTTP client for the FT personality.
    36  //
    37  // TODO(al): split this into Client and SubmitClient.
    38  type ReadonlyClient struct {
    39  	// LogURL is the base URL for the FT log.
    40  	LogURL *url.URL
    41  
    42  	LogSigVerifier note.Verifier
    43  }
    44  
    45  // SubmitClient extends ReadonlyClient to also know how to submit entries
    46  type SubmitClient struct {
    47  	*ReadonlyClient
    48  }
    49  
    50  // PublishFirmware sends a firmware manifest and corresponding image to the log server.
    51  func (c SubmitClient) PublishFirmware(manifest, image []byte) error {
    52  	u, err := c.LogURL.Parse(api.HTTPAddFirmware)
    53  	if err != nil {
    54  		return err
    55  	}
    56  	glog.V(1).Infof("Submitting to %v", u.String())
    57  	var b bytes.Buffer
    58  	w := multipart.NewWriter(&b)
    59  
    60  	// Write the manifest JSON part
    61  	mh := make(textproto.MIMEHeader)
    62  	mh.Set("Content-Type", "application/json")
    63  	partWriter, err := w.CreatePart(mh)
    64  	if err != nil {
    65  		return err
    66  	}
    67  	if _, err := io.Copy(partWriter, bytes.NewReader(manifest)); err != nil {
    68  		return err
    69  	}
    70  
    71  	// Write the binary FW image part
    72  	mh = make(textproto.MIMEHeader)
    73  	mh.Set("Content-Type", "application/octet-stream")
    74  	partWriter, err = w.CreatePart(mh)
    75  	if err != nil {
    76  		return err
    77  	}
    78  	if _, err := io.Copy(partWriter, bytes.NewReader(image)); err != nil {
    79  		return err
    80  	}
    81  
    82  	// Finish off the multipart request
    83  	if err := w.Close(); err != nil {
    84  		return err
    85  	}
    86  
    87  	// Turn this into an HTTP POST request
    88  	req, err := http.NewRequest("POST", u.String(), &b)
    89  	if err != nil {
    90  		return err
    91  	}
    92  	req.Header.Set("Content-Type", w.FormDataContentType())
    93  
    94  	// And finally, submit the request to the log
    95  	r, err := http.DefaultClient.Do(req)
    96  	if err != nil {
    97  		return fmt.Errorf("failed to publish to log endpoint (%s): %w", u, err)
    98  	}
    99  	if r.Request.Method != "POST" {
   100  		// https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections#permanent_redirections
   101  		return fmt.Errorf("POST request to %q was converted to %s request to %q", u.String(), r.Request.Method, r.Request.URL)
   102  	}
   103  	if r.StatusCode != http.StatusOK {
   104  		return errFromResponse("failed to submit to log", r)
   105  	}
   106  	return nil
   107  }
   108  
   109  // PublishAnnotationMalware publishes the serialized annotation to the log.
   110  func (c SubmitClient) PublishAnnotationMalware(stmt []byte) error {
   111  	u, err := c.LogURL.Parse(api.HTTPAddAnnotationMalware)
   112  	if err != nil {
   113  		return err
   114  	}
   115  	glog.V(1).Infof("Submitting to %v", u.String())
   116  	r, err := http.Post(u.String(), "application/json", bytes.NewBuffer(stmt))
   117  	if err != nil {
   118  		return fmt.Errorf("failed to publish to log endpoint (%s): %w", u, err)
   119  	}
   120  	if r.StatusCode != http.StatusOK {
   121  		return errFromResponse("failed to submit to log", r)
   122  	}
   123  	return nil
   124  }
   125  
   126  // GetCheckpoint returns a new LogCheckPoint from the server.
   127  func (c ReadonlyClient) GetCheckpoint() (*api.LogCheckpoint, error) {
   128  	u, err := c.LogURL.Parse(api.HTTPGetRoot)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	r, err := http.Get(u.String())
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	defer func() {
   137  		if err := r.Body.Close(); err != nil {
   138  			glog.Errorf("r.Body.Close(): %v", err)
   139  		}
   140  	}()
   141  	if r.StatusCode != 200 {
   142  		return &api.LogCheckpoint{}, errFromResponse("failed to fetch checkpoint", r)
   143  	}
   144  
   145  	b, err := io.ReadAll(r.Body)
   146  	if err != nil {
   147  		return nil, fmt.Errorf("failed to read body: %w", err)
   148  	}
   149  
   150  	return api.ParseCheckpoint(b, c.LogSigVerifier)
   151  }
   152  
   153  // GetInclusion returns an inclusion proof for the statement under the given checkpoint.
   154  func (c ReadonlyClient) GetInclusion(statement []byte, cp api.LogCheckpoint) (api.InclusionProof, error) {
   155  	hash := rfc6962.DefaultHasher.HashLeaf(statement)
   156  	u, err := c.LogURL.Parse(fmt.Sprintf("%s/for-leaf-hash/%s/in-tree-of/%d", api.HTTPGetInclusion, base64.URLEncoding.EncodeToString(hash), cp.Size))
   157  	if err != nil {
   158  		return api.InclusionProof{}, err
   159  	}
   160  	glog.V(2).Infof("Fetching inclusion proof from %q", u.String())
   161  	r, err := http.Get(u.String())
   162  	if err != nil {
   163  		return api.InclusionProof{}, err
   164  	}
   165  	if r.StatusCode != 200 {
   166  		return api.InclusionProof{}, errFromResponse("failed to fetch inclusion proof", r)
   167  	}
   168  
   169  	var ip api.InclusionProof
   170  	err = json.NewDecoder(r.Body).Decode(&ip)
   171  	return ip, err
   172  }
   173  
   174  // GetManifestEntryAndProof returns the manifest and proof from the server, for given Index and TreeSize
   175  // TODO(mhutchinson): Rename this as leaf values can also be annotations.
   176  func (c ReadonlyClient) GetManifestEntryAndProof(request api.GetFirmwareManifestRequest) (*api.InclusionProof, error) {
   177  	url := fmt.Sprintf("%s/at/%d/in-tree-of/%d", api.HTTPGetManifestEntryAndProof, request.Index, request.TreeSize)
   178  
   179  	u, err := c.LogURL.Parse(url)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	r, err := http.Get(u.String())
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	if r.StatusCode != 200 {
   189  		return nil, errFromResponse("failed to fetch entry and proof", r)
   190  	}
   191  
   192  	var mr api.InclusionProof
   193  	if err := json.NewDecoder(r.Body).Decode(&mr); err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	return &mr, nil
   198  }
   199  
   200  // GetConsistencyProof returns the Consistency Proof from the server, for the two given snapshots
   201  func (c ReadonlyClient) GetConsistencyProof(request api.GetConsistencyRequest) (*api.ConsistencyProof, error) {
   202  	url := fmt.Sprintf("%s/from/%d/to/%d", api.HTTPGetConsistency, request.From, request.To)
   203  	u, err := c.LogURL.Parse(url)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	r, err := http.Get(u.String())
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  	if r.StatusCode != 200 {
   213  		return nil, errFromResponse("failed to fetch consistency proof", r)
   214  	}
   215  
   216  	var cp api.ConsistencyProof
   217  	if err := json.NewDecoder(r.Body).Decode(&cp); err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	return &cp, nil
   222  }
   223  
   224  // GetFirmwareImage returns the firmware image with the corresponding hash from the personality CAS.
   225  func (c ReadonlyClient) GetFirmwareImage(hash []byte) ([]byte, error) {
   226  	url := fmt.Sprintf("%s/with-hash/%s", api.HTTPGetFirmwareImage, base64.URLEncoding.EncodeToString(hash))
   227  
   228  	u, err := c.LogURL.Parse(url)
   229  	if err != nil {
   230  		return nil, err
   231  	}
   232  
   233  	r, err := http.Get(u.String())
   234  	if err != nil {
   235  		return nil, err
   236  	}
   237  	if r.StatusCode != 200 {
   238  		return nil, errFromResponse("failed to fetch firmware image", r)
   239  	}
   240  
   241  	b, err := io.ReadAll(r.Body)
   242  	if err != nil {
   243  		return nil, fmt.Errorf("failed to read firmware image from response: %w", err)
   244  	}
   245  
   246  	return b, nil
   247  }
   248  
   249  func errFromResponse(m string, r *http.Response) error {
   250  	if r.StatusCode == 200 {
   251  		return nil
   252  	}
   253  
   254  	b, _ := io.ReadAll(r.Body) // Ignore any error, we want to ensure we return the right status code which we already know.
   255  
   256  	msg := fmt.Sprintf("%s: %s", m, string(b))
   257  	return status.New(codeFromHTTPResponse(r.StatusCode), msg).Err()
   258  }