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 }