-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding a signature interface and its implementations to parse cosign-…
…generated signatures from OCI image object Fix lient issues
- Loading branch information
1 parent
9d4df6b
commit 2a678b0
Showing
7 changed files
with
689 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.