blob: 339cd0343f2dd0ed0c8be185ba177ec29ebb90f4 [file] [log] [blame]
package types
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"regexp"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
)
const (
// ErrBadImageFormat is the error returned by Client.Verify when the format
// of the given image is not correct.
ErrBadImageFormat = "expected image of the form gcr.io/project/repository@sha256:digest"
)
// IsErrBadImageFormat returns true if the given error is ErrBadImageFormat.
func IsErrBadImageFormat(err error) bool {
return err != nil && skerr.Unwrap(err).Error() == ErrBadImageFormat
}
var validAttestorRegex = regexp.MustCompile(`^projects\/[0-9A-Za-z_-]+\/attestors\/[0-9A-Za-z_-]+$`)
// ValidateAttestor returns an error if the given attestor does not appear to be
// a fully-qualified resource name.
func ValidateAttestor(attestor string) error {
if !validAttestorRegex.MatchString(attestor) {
return skerr.Fmt("expected a fully-qualified resource name for attestor")
}
return nil
}
var validImageRegex = regexp.MustCompile(`^[0-9A-Za-z_.]+\/[0-9A-Za-z_-]+\/[0-9A-Za-z_-]+@sha256:[0-9a-f]{64}$`)
// ValidateImageID returns ErrBadImageFormat if the given image ID does not have
// the expected format.
func ValidateImageID(imageID string) error {
if !validImageRegex.MatchString(imageID) {
return skerr.Fmt(ErrBadImageFormat)
}
return nil
}
// Client performs validation of Docker images.
type Client interface {
// Verify finds and validates the attestation for the given Docker image ID.
// It returns true if any attestation exists with a valid signature and
// false if no such attestation exists, or an error if any of the required
// API calls failed.
Verify(ctx context.Context, imageID string) (bool, error)
}
// HttpClient implements Client by communicating with the attest service.
type HttpClient struct {
host string
c *http.Client
}
// NewHttpClient returns an HttpClient instance.
func NewHttpClient(host string, c *http.Client) *HttpClient {
return &HttpClient{
host: host,
c: c,
}
}
// VerifyRequest is the body of an HTTP request to Client.Verify.
type VerifyRequest struct {
ImageID string `json:"imageID"`
}
// VerifyRequest is the body of an HTTP response from Client.Verify.
type VerifyResponse struct {
Verified bool `json:"verifiedAttestation"`
}
// Verify implements Client.
func (c *HttpClient) Verify(ctx context.Context, imageID string) (bool, error) {
// Validate the imageID before sending any requests.
if err := ValidateImageID(imageID); err != nil {
return false, skerr.Wrap(err)
}
// Create the request.
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(VerifyRequest{
ImageID: imageID,
}); err != nil {
return false, skerr.Wrapf(err, "failed to encode request body")
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.host, &buf)
if err != nil {
return false, skerr.Wrapf(err, "failed to create request")
}
req.Header.Set("Content-Type", "application/json")
// Execute the request.
resp, err := c.c.Do(req)
if err != nil {
return false, skerr.Wrapf(err, "failed to execute request")
}
defer util.Close(resp.Body)
// Decode the response and return.
b, err := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return false, skerr.Fmt("request failed with status %s: %s", resp.Status, string(b))
}
var result VerifyResponse
if err := json.NewDecoder(bytes.NewReader(b)).Decode(&result); err != nil {
return false, skerr.Wrapf(err, "failed to decode response body")
}
return result.Verified, nil
}
var _ Client = &HttpClient{}
// Server wraps a Client and serves HTTP requests.
type Server struct {
wrappedClient Client
}
func NewServer(wrapped Client) *Server {
return &Server{
wrappedClient: wrapped,
}
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Decode the request.
var req VerifyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "failed to decode request body", http.StatusBadRequest)
return
}
// Verify the image.
verified, err := s.wrappedClient.Verify(r.Context(), req.ImageID)
if IsErrBadImageFormat(err) {
http.Error(w, skerr.Unwrap(err).Error(), http.StatusBadRequest)
return
} else if err != nil {
sklog.Errorf("Failed checking attestation of %s: %s", req.ImageID, err)
http.Error(w, "internal server error", http.StatusInternalServerError)
return
}
// Encode the response.
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(VerifyResponse{
Verified: verified,
}); err != nil {
http.Error(w, "failed to encode response body", http.StatusInternalServerError)
return
}
}