diff --git a/directory/directory_test.go b/directory/directory_test.go index c9154561b4..8c80262ad4 100644 --- a/directory/directory_test.go +++ b/directory/directory_test.go @@ -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) diff --git a/internal/signature/cosign.go b/internal/signature/cosign.go new file mode 100644 index 0000000000..43185c997a --- /dev/null +++ b/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) +} diff --git a/internal/signature/signature.go b/internal/signature/signature.go index 50dca23933..04f569d9e7 100644 --- a/internal/signature/signature.go +++ b/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? @@ -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