Skip to content

Commit

Permalink
Introduce signature.Cosign as a format
Browse files Browse the repository at this point in the history
Currently, this just allows serializing and deserializing
it as a blob.

NOTE: This makes an implementation decision about the blob format:
we use OpenPGP signatures with no marker, any new formats will
start with a zero byte and an ASCII line identifying the format of the rest

Signed-off-by: Miloslav Trmač <mitr@redhat.com>
  • Loading branch information
mtrmac committed Jul 6, 2022
1 parent e79999a commit 2f6f7a5
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 7 deletions.
9 changes: 5 additions & 4 deletions directory/directory_test.go
Expand Up @@ -158,13 +158,14 @@ func TestGetPutSignatures(t *testing.T) {
list := []byte("test-manifest-list")
md, err := manifest.Digest(man)
require.NoError(t, err)
// These signatures are completely invalid; start with 0xA3 just to be minimally plausible to signature.FromBlob.
signatures := [][]byte{
[]byte("sig1"),
[]byte("sig2"),
[]byte("\xA3sig1"),
[]byte("\xA3sig2"),
}
listSignatures := [][]byte{
[]byte("sig3"),
[]byte("sig4"),
[]byte("\xA3sig3"),
[]byte("\xA3sig4"),
}

dest, err := ref.NewImageDestination(context.Background(), nil)
Expand Down
38 changes: 38 additions & 0 deletions internal/signature/cosign.go
@@ -0,0 +1,38 @@
package signature

import "encoding/json"

const CosignSignatureMIMEType = "application/vnd.dev.cosign.simplesigning.v1+json"

// 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 `json:"mimeType"`
UntrustedPayload []byte `json:"payload"`
UntrustedAnnotations map[string]string `json:"annotations"`
}

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

// FIXME FIXME: MIME type? Int? String?
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(s)
}
50 changes: 47 additions & 3 deletions internal/signature/signature.go
@@ -1,6 +1,10 @@
package signature

import "fmt"
import (
"bytes"
"errors"
"fmt"
)

// FIXME FIXME: MIME type? Int? String?
// An interface with a name, parse methods?
Expand Down Expand Up @@ -54,8 +58,48 @@ func Blob(sig Signature) ([]byte, error) {

// FromBlob returns a signature from parsing a blob created by signature.Blob.
func FromBlob(blob []byte) (Signature, error) {
// FIXME FIXME: read format ID, detect other values.
return SimpleSigningFromBlob(blob), nil
if len(blob) == 0 {
return nil, errors.New("empty signature blob")
}
// Historically we’ve just been using GPG with no identification; try to auto-detect that.
switch blob[0] {
// OpenPGP "compressed data" wrapping the message
case 0xA0, 0xA1, 0xA2, 0xA3, // bit 7 = 1; bit 6 = 0 (old packet format); bits 5…2 = 8 (tag: compressed data packet); bits 1…0 = length-type (any)
0xC8, // bit 7 = 1; bit 6 = 1 (new packet format); bits 5…0 = 8 (tag: compressed data packet)
// OpenPGP “one-pass signature” starting a signature
0x90, 0x91, 0x92, 0x3d, // bit 7 = 1; bit 6 = 0 (old packet format); bits 5…2 = 4 (tag: one-pass signature packet); bits 1…0 = length-type (any)
0xC4, // bit 7 = 1; bit 6 = 1 (new packet format); bits 5…0 = 4 (tag: one-pass signature packet)
// OpenPGP signature packet signing the following data
0x88, 0x89, 0x8A, 0x8B, // bit 7 = 1; bit 6 = 0 (old packet format); bits 5…2 = 2 (tag: signature packet); bits 1…0 = length-type (any)
0xC2: // bit 7 = 1; bit 6 = 1 (new packet format); bits 5…0 = 2 (tag: signature packet)
return SimpleSigningFromBlob(blob), nil

// The newer format: binary 0, format name, newline, data
case 0x00:
newline := bytes.IndexByte(blob, '\n')
if newline == -1 {
return nil, fmt.Errorf("invalid signature format, missing newline")
}
formatBytes := blob[:newline]
for _, b := range formatBytes {
if b < 32 || b >= 0x7F {
return nil, fmt.Errorf("invalid signature format, non-ASCII byte %#x", b)
}
}
blobChunk := blob[newline+1:]
switch {
case bytes.Equal(formatBytes, []byte(SimpleSigningFormat)):
return SimpleSigningFromBlob(blobChunk), nil
case bytes.Equal(formatBytes, []byte(CosignFormat)):
return cosignFromBlobChunk(blobChunk)
default:
return nil, fmt.Errorf("unrecognized signature format %q", string(formatBytes))
}

default:
return nil, fmt.Errorf("unrecognized signature format, starting with binary %#x", blob[0])
}

}

// copyByteSlice returns a guaranteed-fresh copy of a byte slice
Expand Down

0 comments on commit 2f6f7a5

Please sign in to comment.