Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions internal/signature/cosign.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package signature

import "encoding/json"

const (
// from sigstore/cosign/pkg/types.SimpleSigningMediaType
CosignSignatureMIMEType = "application/vnd.dev.cosign.simplesigning.v1+json"
// from sigstore/cosign/pkg/oci/static.SignatureAnnotationKey
CosignSignatureAnnotationKey = "dev.cosignproject.cosign/signature"
)

// Cosign is a github.com/Cosign/cosign signature.
// For the persistent-storage format used for blobChunk(), we want
// a degree of forward compatibility against unexpected field changes
// (as has happened before), which is why this data type
// contains just a payload + annotations (including annotations
// that we don’t recognize or support), instead of individual fields
// for the known annotations.
type Cosign struct {
untrustedMIMEType string
untrustedPayload []byte
untrustedAnnotations map[string]string
}

// cosignJSONRepresentation needs the files to be public, which we don’t want for
// the main Cosign type.
type cosignJSONRepresentation struct {
UntrustedMIMEType string `json:"mimeType"`
UntrustedPayload []byte `json:"payload"`
UntrustedAnnotations map[string]string `json:"annotations"`
}

// CosignFromComponents returns a Cosign object from its components.
func CosignFromComponents(untrustedMimeType string, untrustedPayload []byte, untrustedAnnotations map[string]string) Cosign {
return Cosign{
untrustedMIMEType: untrustedMimeType,
untrustedPayload: copyByteSlice(untrustedPayload),
untrustedAnnotations: copyStringMap(untrustedAnnotations),
}
}

// cosignFromBlobChunk converts a Cosign signature, as returned by Cosign.blobChunk, into a Cosign object.
func cosignFromBlobChunk(blobChunk []byte) (Cosign, error) {
var v cosignJSONRepresentation
if err := json.Unmarshal(blobChunk, &v); err != nil {
return Cosign{}, err
}
return CosignFromComponents(v.UntrustedMIMEType,
v.UntrustedPayload,
v.UntrustedAnnotations), nil
}

func (s Cosign) FormatID() FormatID {
return CosignFormat
}

// blobChunk returns a representation of signature as a []byte, suitable for long-term storage.
// Almost everyone should use signature.Blob() instead.
func (s Cosign) blobChunk() ([]byte, error) {
return json.Marshal(cosignJSONRepresentation{
UntrustedMIMEType: s.UntrustedMIMEType(),
UntrustedPayload: s.UntrustedPayload(),
UntrustedAnnotations: s.UntrustedAnnotations(),
})
}

func (s Cosign) UntrustedMIMEType() string {
return s.untrustedMIMEType
}
func (s Cosign) UntrustedPayload() []byte {
return copyByteSlice(s.untrustedPayload)
}

func (s Cosign) UntrustedAnnotations() map[string]string {
return copyStringMap(s.untrustedAnnotations)
}

func copyStringMap(m map[string]string) map[string]string {
res := map[string]string{}
for k, v := range m {
res[k] = v
}
return res
}
71 changes: 71 additions & 0 deletions internal/signature/cosign_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package signature

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCosignFromComponents(t *testing.T) {
const mimeType = "mime-type"
payload := []byte("payload")
annotations := map[string]string{"a": "b", "c": "d"}

sig := CosignFromComponents(mimeType, payload, annotations)
assert.Equal(t, Cosign{
untrustedMIMEType: mimeType,
untrustedPayload: payload,
untrustedAnnotations: annotations,
}, sig)
}

func TestCosignFromBlobChunk(t *testing.T) {
// Success
json := []byte(`{"mimeType":"mime-type","payload":"cGF5bG9hZA==", "annotations":{"a":"b","c":"d"}}`)
res, err := cosignFromBlobChunk(json)
require.NoError(t, err)
assert.Equal(t, "mime-type", res.UntrustedMIMEType())
assert.Equal(t, []byte("payload"), res.UntrustedPayload())
assert.Equal(t, map[string]string{"a": "b", "c": "d"}, res.UntrustedAnnotations())

// Invalid JSON
_, err = cosignFromBlobChunk([]byte("&"))
assert.Error(t, err)
}

func TestCosignFormatID(t *testing.T) {
sig := CosignFromComponents("mime-type", []byte("payload"),
map[string]string{"a": "b", "c": "d"})
assert.Equal(t, CosignFormat, sig.FormatID())
}

func TestCosign_blobChunk(t *testing.T) {
sig := CosignFromComponents("mime-type", []byte("payload"),
map[string]string{"a": "b", "c": "d"})
res, err := sig.blobChunk()
require.NoError(t, err)

expectedJSON := []byte(`{"mimeType":"mime-type","payload":"cGF5bG9hZA==", "annotations":{"a":"b","c":"d"}}`)
// Don’t directly compare the JSON representation so that we don’t test for formatting differences, just verify that it contains exactly the expected data.
var raw, expectedRaw map[string]interface{}
err = json.Unmarshal(res, &raw)
require.NoError(t, err)
err = json.Unmarshal(expectedJSON, &expectedRaw)
require.NoError(t, err)
assert.Equal(t, expectedRaw, raw)
}

func TestCosign_UntrustedPayload(t *testing.T) {
var payload = []byte("payload")
sig := CosignFromComponents("mime-type", payload,
map[string]string{"a": "b", "c": "d"})
assert.Equal(t, payload, sig.UntrustedPayload())
}

func TestCosign_UntrustedAnnotations(t *testing.T) {
annotations := map[string]string{"a": "b", "c": "d"}
sig := CosignFromComponents("mime-type", []byte("payload"), annotations)
assert.Equal(t, annotations, sig.UntrustedAnnotations())
}
2 changes: 1 addition & 1 deletion internal/signature/signature.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func FromBlob(blob []byte) (Signature, error) {
case bytes.Equal(formatBytes, []byte(SimpleSigningFormat)):
return SimpleSigningFromBlob(blobChunk), nil
case bytes.Equal(formatBytes, []byte(CosignFormat)):
fallthrough
return cosignFromBlobChunk(blobChunk)
default:
return nil, fmt.Errorf("unrecognized signature format %q", string(formatBytes))
}
Expand Down
43 changes: 42 additions & 1 deletion internal/signature/signature_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package signature

import (
"bytes"
"fmt"
"os"
"testing"

Expand All @@ -22,6 +24,45 @@ func TestBlobSimpleSigning(t *testing.T) {
fromBlobSimple, ok := fromBlob.(SimpleSigning)
require.True(t, ok)
assert.Equal(t, simpleSigData, fromBlobSimple.UntrustedSignature())

// Using the newer format is accepted as well.
fromBlob, err = FromBlob(append([]byte("\x00simple-signing\n"), simpleSigData...))
require.NoError(t, err)
fromBlobSimple, ok = fromBlob.(SimpleSigning)
require.True(t, ok)
assert.Equal(t, simpleSigData, fromBlobSimple.UntrustedSignature())

}

func TestBlobCosign(t *testing.T) {
cosignSig := CosignFromComponents("mime-type", []byte("payload"),
map[string]string{"a": "b", "c": "d"})

cosignBlob, err := Blob(cosignSig)
require.NoError(t, err)
assert.True(t, bytes.HasPrefix(cosignBlob, []byte("\x00cosign-json\n{")))

fromBlob, err := FromBlob(cosignBlob)
require.NoError(t, err)
fromBlobCosign, ok := fromBlob.(Cosign)
require.True(t, ok)
assert.Equal(t, cosignSig.UntrustedMIMEType(), fromBlobCosign.UntrustedMIMEType())
assert.Equal(t, cosignSig.UntrustedPayload(), fromBlobCosign.UntrustedPayload())
assert.Equal(t, cosignSig.UntrustedAnnotations(), fromBlobCosign.UntrustedAnnotations())
}

func TestFromBlobInvalid(t *testing.T) {
// Round-tripping valid data has been tested in TestBlobSimpleSigning and TestBlobCosign above.
for _, c := range []string{
"", // Empty
"\xFFsimple-signing\nhello", // Invalid first byte
"\x00simple-signing", // No newline
"\x00format\xFFname\ndata", // Non-ASCII format value
"\x00unknown-format\ndata", // Unknown format
} {
_, err := FromBlob([]byte(c))
assert.Error(t, err, fmt.Sprintf("%#v", c))
}
}

// mockFormatSignature returns a specified format
Expand All @@ -44,7 +85,7 @@ func TestUnsuportedFormatError(t *testing.T) {
expected string
}{
{SimpleSigningFromBlob(nil), "unsupported signature format simple-signing"},
{mockFormatSignature{CosignFormat}, "unsupported signature format cosign-json"},
{CosignFromComponents("mime-type", nil, nil), "unsupported signature format cosign-json"},
{mockFormatSignature{FormatID("invalid")}, `unsupported, and unrecognized, signature format "invalid"`},
} {
res := UnsupportedFormatError(c.input)
Expand Down