Skip to content

Commit

Permalink
Adding a signature interface and its implementations to parse cosign-…
Browse files Browse the repository at this point in the history
…generated signatures from OCI image object

Fix lient issues
  • Loading branch information
yawangwang committed Jul 14, 2023
1 parent 9d4df6b commit 2a678b0
Show file tree
Hide file tree
Showing 7 changed files with 689 additions and 6 deletions.
60 changes: 60 additions & 0 deletions launcher/internal/oci/cosign/payload.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package cosign

import (
"encoding/json"
"fmt"

digest "github.com/opencontainers/go-digest"
)

// CosignCriticalType is the value of `critical.type` in a simple signing format payload specified in
// https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#simple-signing
const CosignCriticalType = "cosign container image signature"

// Payload follows the simple signing format specified in
// https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#simple-signing
type Payload struct {
Critical Critical `json:"critical"`
Optional map[string]interface{} `json:"optional"`
}

// Critical contains data critical to correctly evaluating the validity of a signature.
type Critical struct {
Identity Identity `json:"identity"`
Image Image `json:"image"`
Type string `json:"type"`
}

// Identity identifies the claimed identity of the image.
type Identity struct {
DockerReference string `json:"docker-reference"`
}

// Image identifies the container image this signature applies to.
type Image struct {
DockerManifestDigest string `json:"docker-manifest-digest"`
}

// Valid returns error if the payload does not conform to simple signing format.
// https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#simple-signing
func (p *Payload) Valid() error {
if p.Critical.Type != CosignCriticalType {
return fmt.Errorf("unknown critical type for Cosign signature payload: %s", p.Critical.Type)
}
if _, err := digest.Parse(p.Critical.Image.DockerManifestDigest); err != nil {
return fmt.Errorf("cannot parse image digest: %w", err)
}
return nil
}

// UnmarshalPayload unmarshals a byte slice to a payload and performs checks on the payload.
func UnmarshalPayload(data []byte) (*Payload, error) {
var payload Payload
if err := json.Unmarshal(data, &payload); err != nil {
return nil, err
}
if err := payload.Valid(); err != nil {
return nil, err
}
return &payload, nil
}
91 changes: 91 additions & 0 deletions launcher/internal/oci/cosign/payload_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package cosign

import (
"testing"

"github.com/google/go-cmp/cmp"
)

func TestValidPayload(t *testing.T) {
testCases := []struct {
name string
payload *Payload
wantPass bool
}{
{
name: "valid cosign payload format",
payload: &Payload{
Critical: Critical{
Identity: Identity{
DockerReference: "us-docker.pkg.dev/confidential-space-images-dev/cs-cosign-tests/base",
},
Image: Image{
DockerManifestDigest: "sha256:9494e567c7c44e8b9f8808c1658a47c9b7979ef3cceef10f48754fc2706802ba",
},
Type: CosignCriticalType,
},
},
wantPass: true,
},
{
name: "invalid cosign payload format with invalid type",
payload: &Payload{
Critical: Critical{
Identity: Identity{
DockerReference: "us-docker.pkg.dev/confidential-space-images-dev/cs-cosign-tests/base",
},
Image: Image{
DockerManifestDigest: "sha256:9494e567c7c44e8b9f8808c1658a47c9b7979ef3cceef10f48754fc2706802ba",
},
Type: "invalid type",
},
},
wantPass: false,
},
{
name: "invalid cosign payload format with invalid manifest digest",
payload: &Payload{
Critical: Critical{
Identity: Identity{
DockerReference: "us-docker.pkg.dev/confidential-space-images-dev/cs-cosign-tests/base",
},
Image: Image{
DockerManifestDigest: "sha256:invalid manifest digest",
},
Type: CosignCriticalType,
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.payload.Valid() == nil; got != tc.wantPass {
t.Errorf("cosign payload Valid() failed for test case %v: got %v, but want %v", tc.name, got, tc.wantPass)
}
})
}
}

func TestUnmarshalPayload(t *testing.T) {
payloadBytes := []byte(`{"critical":{"identity":{"docker-reference":"us-docker.pkg.dev/confidential-space-images-dev/cs-cosign-tests/base"},"image":{"docker-manifest-digest":"sha256:9494e567c7c44e8b9f8808c1658a47c9b7979ef3cceef10f48754fc2706802ba"},"type":"cosign container image signature"},"optional":null}`)
wantPayload := &Payload{
Critical: Critical{
Identity: Identity{
DockerReference: "us-docker.pkg.dev/confidential-space-images-dev/cs-cosign-tests/base",
},
Image: Image{
DockerManifestDigest: "sha256:9494e567c7c44e8b9f8808c1658a47c9b7979ef3cceef10f48754fc2706802ba",
},
Type: CosignCriticalType,
},
}
gotPayload, err := UnmarshalPayload(payloadBytes)
if err != nil {
t.Errorf("UnmarshalPayload() failed: %v", err)
}

if !cmp.Equal(gotPayload, wantPayload) {
t.Errorf("UnmarshalPayload() failed, got %v, but want %v", gotPayload, wantPayload)
}
}
119 changes: 119 additions & 0 deletions launcher/internal/oci/cosign/signature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Package cosign contains functionalities to interact with signatures generated by cosign.
// https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md.
package cosign

import (
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"

"github.com/google/go-tpm-tools/launcher/internal/oci"
"github.com/opencontainers/go-digest"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
)

// Sig implements oci.Signature interface for cosign-gernated signatures.
type Sig struct {
// Layer represents a layer descriptor for OCI image manifest.
Layer v1.Descriptor
// Blob represents the opaque data uploaded to OCI registory associated with the layer.
Blob []byte
}

const (
// CosignSigKey is the key of the cosign-generated signature embedded in OCI image manifest.
CosignSigKey = "dev.cosignproject.cosign/signature"
// CosignPubKey is the key of the public key for signature verification attached to the cosign-generated payload.
CosignPubKey = "dev.cosignproject.cosign/pub"
// CosignSigningAlgo is the key of the signing algorithm attached to the cosign-generated payload.
CosignSigningAlgo = "dev.cosignproject.cosign/signingalgo"
)

var (
// Verify that our Sig struct implements the expected public interface.
_ oci.Signature = Sig{}
encoding = base64.StdEncoding
)

// Payload implements oci.Signature interface.
func (s Sig) Payload() ([]byte, error) {
if digest.FromBytes(s.Blob) != s.Layer.Digest {
return nil, errors.New("an unmatched payload digest is paired with a layer descriptor digest")
}
return s.Blob, nil
}

// Base64Encoded implements oci.Signature interface.
func (s Sig) Base64Encoded() (string, error) {
sig, ok := s.Layer.Annotations[CosignSigKey]
if !ok {
return "", errors.New("cosign signature not found in the layer annotations")
}
if _, err := encoding.DecodeString(sig); err != nil {
return "", fmt.Errorf("invalid base64 encoded signature: %w", err)
}
return sig, nil
}

// PublicKey implements oci.Signature interface.
func (s Sig) PublicKey() ([]byte, error) {
payloadBytes, err := s.Payload()
if err != nil {
return nil, err
}
payload, err := UnmarshalPayload(payloadBytes)
if err != nil {
return nil, err
}
pub, ok := payload.Optional[CosignPubKey].(string)
if !ok {
return nil, fmt.Errorf("pub key not found in the Opotional field of payload: %v", payload)
}
pemBytes := []byte(pub)
// Verify if it is a valid PEM-encoded public key.
if _, err := unmarshalPEMToPub(pemBytes); err != nil {
return nil, fmt.Errorf("invalid PEM-encoded pub key: %w", err)
}
return pemBytes, nil
}

// SigningAlgorithm implements oci.Signature interface.
func (s Sig) SigningAlgorithm() (oci.SigningAlgorithm, error) {
payloadBytes, err := s.Payload()
if err != nil {
return "", err
}
payload, err := UnmarshalPayload(payloadBytes)
if err != nil {
return "", err
}
alg, ok := payload.Optional[CosignSigningAlgo].(string)
if !ok {
return "", fmt.Errorf("signing algorithm not found in the Opotional field of payload: %v", payload)
}
switch oci.SigningAlgorithm(alg) {
case oci.RsassaPssSha256, oci.RsassaPkcs1v15Sha256, oci.EcdsaP256Sha256:
return oci.SigningAlgorithm(alg), nil
default:
return "", errors.New("unsupported signing algorithm")
}
}

// unmarshalPEMToPub converts a PEM-encoded byte slice into a crypto.PublicKey.
func unmarshalPEMToPub(pemBytes []byte) (crypto.PublicKey, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, errors.New("no PEM data found, failed to decode PEM-encoded byte slice")
}
switch block.Type {
case "PUBLIC KEY":
return x509.ParsePKIXPublicKey(block.Bytes)
case "RSA PUBLIC KEY":
return x509.ParsePKCS1PublicKey(block.Bytes)
default:
return nil, fmt.Errorf("unsupported public key type: %v", block.Type)
}
}
Loading

0 comments on commit 2a678b0

Please sign in to comment.