package rest import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "github.com/slsa-framework/slsa-verifier/v2/options" "github.com/slsa-framework/slsa-verifier/v2/verifiers" ) var errInvalid = errors.New("invalid") type v1Query struct { // Compulsory fields. Source string `json:"source"` ArtifactHash string `json:"artifactHash"` DsseEnvelope string `json:"provenanceContent"` // Optional fields. BuilderID *string `json:"builderID"` Tag *string `json:"tag"` Branch *string `json:"branch"` VersionedTag *string `json:"versionedTag"` PrintProvenance *bool `json:"printProvenance"` } type validation string var ( validationSuccess = validation("success") validationFailure = validation("failure") ) type v1Result struct { Version uint `json:"version"` Error *string `json:"error,omitempty"` Validation validation `json:"validation"` BuilderID string `json:"builderID"` IntotoStatement *string `json:"provenanceContent,omitempty"` } func VerifyHandlerV1(w http.ResponseWriter, r *http.Request) { if r == nil { http.Error(w, "empty request", http.StatusInternalServerError) return } results := verifyHandlerV1(r) encoder := json.NewEncoder(w) if err := encoder.Encode(results); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } func toStringPtr(e error) *string { if e != nil { s := e.Error() return &s } return nil } func v1ResultNew() *v1Result { return &v1Result{ Version: 1, Error: nil, Validation: validationFailure, IntotoStatement: nil, } } func (r *v1Result) withError(e error) *v1Result { r.Error = toStringPtr(e) return r } func (r *v1Result) withValidation(v validation) *v1Result { r.Validation = v return r } func (r *v1Result) withBuilderID(id string) *v1Result { r.BuilderID = id return r } func (r *v1Result) withIntotoStatement(c []byte) *v1Result { b := base64.StdEncoding.EncodeToString(c) r.IntotoStatement = &b return r } func verifyHandlerV1(r *http.Request) *v1Result { results := v1ResultNew() body, err := io.ReadAll(r.Body) if err != nil { return results.withError(err) } r.Body.Close() // Create a query. query, err := queryFromString(body) if err != nil { return results.withError(err) } // Validate it. if err := query.validate(); err != nil { return results.withError(err) } // Run the verification. provenanceOpts := &options.ProvenanceOpts{ ExpectedSourceURI: query.Source, ExpectedBranch: query.Branch, ExpectedDigest: query.ArtifactHash, ExpectedVersionedTag: query.VersionedTag, ExpectedTag: query.Tag, } builderOpts := &options.BuilderOpts{ ExpectedID: query.BuilderID, } ctx := context.Background() p, builderID, err := verifiers.VerifyArtifact(ctx, []byte(query.DsseEnvelope), query.ArtifactHash, provenanceOpts, builderOpts) if err != nil { return results.withError(err) } if query.PrintProvenance != nil && *query.PrintProvenance { results = results.withIntotoStatement(p) } return results.withBuilderID(builderID.String()).withValidation(validationSuccess) } func queryFromString(content []byte) (*v1Query, error) { var query v1Query err := json.Unmarshal(content, &query) if err != nil { return nil, err } env, err := base64.StdEncoding.DecodeString(query.DsseEnvelope) if err != nil { return nil, fmt.Errorf("%w: decoding payload", errInvalid) } query.DsseEnvelope = string(env) return &query, nil } func (q *v1Query) validate() error { if q.Source == "" { return fmt.Errorf("%w: empty source", errInvalid) } if q.ArtifactHash == "" { return fmt.Errorf("%w: empty artifactHash", errInvalid) } if q.DsseEnvelope == "" { return fmt.Errorf("%w: empty dsseEnvelope", errInvalid) } if q.Tag != nil && q.VersionedTag != nil { return fmt.Errorf("%w: tag and versionedTag are mutually exclusive", errInvalid) } // BuilderID is optional, so not additional validation needed. return nil }