Repository: sigstore/sigstore-go (go)

Issue:
JSON marshal of VerificationResult yields incorrectly-formatted in-toto attestation
**Description**

Due to in-toto moving to protobuf, there is an issue where marshalling `VerificationResult` (with `json.Marshal`) will produce a JSON object with an incorrectly formatted in-toto statement.

Specifically, the `predicateType` field is named `predicate_type`, and other fields are made into snake_case instead of the expected `camelCase` as [required by spec.](https://github.com/in-toto/attestation/blob/main/spec/v1/predicate.md)

Pull request description:
Fix intoto unmarshal
<!--
Thanks for opening a pull request! Please do not just delete this text.  The three fields below are mandatory.

Please remember to:
- This PR requires an issue. If it is a new feature, the issue should proceed the PR and will have allowed sufficient time for discussions to take place. Please use
issue tags such as "Closes #XYZ" or "Resolves sigstore/repo-name#XYZ".
  [documentation](https://docs.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword)
- ensure your commits are signed-off, as sigstore uses the [DCO](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin) using `git commit -s`, or `git commit -s --amend` if you want to amend already existing commits
- lastly, ensure there are no merge commits!

Thank you :)
-->

#### Summary

This PR is a modified copy of @gillisandrew's https://github.com/sigstore/sigstore-go/pull/326 with some slight simplifications. It is API-compatible with the existing codebase.

The [attempt to upstream](https://github.com/in-toto/attestation/pull/403) the fix for this is taking longer than expected, so this fix will serve as a temporary solution.

Fixes #365 

#### Release Note
<!--
Add a release note for each of the following conditions:

* Config changes (additions, deletions, updates)
* API additions—new endpoint, new response fields, or newly accepted request parameters
* Database changes (any)
* Websocket additions or changes
* Anything noteworthy to an administrator running private sigstore instances (err on the side of over-communicating)
* New features and improvements, including behavioural changes, UI changes and CLI changes
* Bug fixes and fixes of previous known issues
* Deprecation warnings, breaking changes, or compatibility notes

If no release notes are required write NONE. Use past-tense.

-->

#### Documentation
<!--

Does this change require an update to documentation? How will users implement your new feature?

Please reference a PR within https://docs.sigstore.dev

-->


Patch:
diff --git a/pkg/verify/signed_entity.go b/pkg/verify/signed_entity.go
index fcdda92..857596d 100644
--- a/pkg/verify/signed_entity.go
+++ b/pkg/verify/signed_entity.go
@@ -17,6 +17,7 @@ package verify
 import (
 	"crypto/x509"
 	"encoding/asn1"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
@@ -26,6 +27,7 @@ import (
 	"github.com/sigstore/sigstore-go/pkg/fulcio/certificate"
 	"github.com/sigstore/sigstore-go/pkg/root"
 	"github.com/sigstore/sigstore/pkg/cryptoutils"
+	"google.golang.org/protobuf/encoding/protojson"
 )
 
 const (
@@ -221,6 +223,40 @@ func NewVerificationResult() *VerificationResult {
 	}
 }
 
+// MarshalJSON deals with protojson needed for the Statement.
+// Can be removed when https://github.com/in-toto/attestation/pull/403 is merged.
+func (b *VerificationResult) MarshalJSON() ([]byte, error) {
+	statement, err := protojson.Marshal(b.Statement)
+	if err != nil {
+		return nil, err
+	}
+	// creating a type alias to avoid infinite recursion, as MarshalJSON is
+	// not copied into the alias.
+	type Alias VerificationResult
+	return json.Marshal(struct {
+		Alias
+		Statement json.RawMessage `json:"statement,omitempty"`
+	}{
+		Alias:     Alias(*b),
+		Statement: statement,
+	})
+}
+
+func (b *VerificationResult) UnmarshalJSON(data []byte) error {
+	b.Statement = &in_toto.Statement{}
+	type Alias VerificationResult
+	aux := &struct {
+		Alias
+		Statement json.RawMessage `json:"statement,omitempty"`
+	}{
+		Alias: Alias(*b),
+	}
+	if err := json.Unmarshal(data, aux); err != nil {
+		return err
+	}
+	return protojson.Unmarshal(aux.Statement, b.Statement)
+}
+
 type PolicyOption func(*PolicyConfig) error
 type ArtifactPolicyOption func(*PolicyConfig) error
 


Test patch:
diff --git a/pkg/verify/signed_entity_test.go b/pkg/verify/signed_entity_test.go
index 70ca15a..d32de6b 100644
--- a/pkg/verify/signed_entity_test.go
+++ b/pkg/verify/signed_entity_test.go
@@ -23,9 +23,11 @@ import (
 	"encoding/hex"
 	"encoding/json"
 
+	in_toto "github.com/in-toto/attestation/go/v1"
 	"github.com/sigstore/sigstore-go/pkg/testing/data"
 	"github.com/sigstore/sigstore-go/pkg/verify"
 	"github.com/stretchr/testify/assert"
+	"google.golang.org/protobuf/types/known/structpb"
 )
 
 func TestSignedEntityVerifierInitialization(t *testing.T) {
@@ -422,3 +424,53 @@ func TestSigstoreBundle2Sig(t *testing.T) {
 	assert.True(t, errors.Is(err, verify.ErrDSSEInvalidSignatureCount))
 	assert.Nil(t, res)
 }
+
+func TestStatementSerializesToValidInTotoStatement(t *testing.T) {
+	statement := in_toto.Statement{}
+	statement.Type = "https://in-toto.io/Statement/v0.1"
+	statement.PredicateType = "https://example.org/predicate"
+	statement.Subject = []*in_toto.ResourceDescriptor{
+		{
+			Name: "artifact-name",
+			Digest: map[string]string{
+				"sha256": "artifact-digest",
+			},
+		},
+	}
+	statement.Predicate = &structpb.Struct{
+		Fields: map[string]*structpb.Value{},
+	}
+
+	result := verify.NewVerificationResult()
+	result.Statement = &statement
+
+	// marshal the statement to JSON
+	resultJSON, err := json.Marshal(result)
+	assert.NoError(t, err)
+	want := `
+	{
+		"mediaType": "application/vnd.dev.sigstore.verificationresult+json;version=0.1",
+		"verifiedTimestamps": null,
+		"statement": {
+			"_type": "https://in-toto.io/Statement/v0.1",
+			"predicateType": "https://example.org/predicate",
+			"subject": [
+				{
+					"name": "artifact-name",
+					"digest": {
+						"sha256": "artifact-digest"
+					}
+				}
+			],
+			"predicate": {}
+		}
+	}`
+	assert.JSONEq(t, want, string(resultJSON))
+
+	// unmarshal the JSON back to a VerificationResult
+	result2 := verify.NewVerificationResult()
+	err = json.Unmarshal(resultJSON, result2)
+	assert.NoError(t, err)
+	assert.Equal(t, result.MediaType, result2.MediaType)
+	assert.Equal(t, result.Statement, result2.Statement)
+}