-
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.
[Signature discovery] Add a signature interface and a library to pars…
…e signature from image manifest (#328) * Adding a signature interface and its implementations to parse cosign-generated signatures from OCI image object
- Loading branch information
1 parent
bf3ecd5
commit 9e1f35d
Showing
5 changed files
with
328 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,70 @@ | ||
// 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 ( | ||
"encoding/base64" | ||
"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-generated signatures. | ||
type Sig struct { | ||
// Layer represents a layer descriptor for OCI image manifest. | ||
// This contains the simple signing payload digest and Cosign signature, | ||
// collected from the OCI image manifest object found using https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#tag-based-discovery. | ||
Layer v1.Descriptor | ||
// Blob represents the opaque data uploaded to OCI registry associated with the layer. | ||
// This contains the Simple Signing Payload as described in https://github.com/sigstore/cosign/blob/main/specs/SIGNATURE_SPEC.md#tag-based-discovery. | ||
Blob []byte | ||
} | ||
|
||
// CosignSigKey is the key of the cosign-generated signature embedded in OCI image manifest. | ||
const CosignSigKey = "dev.cosignproject.cosign/signature" | ||
|
||
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) { | ||
// The payload bytes are uploaded to an OCI registry as blob, and are referenced by digest. | ||
// This digiest is embedded into the OCI image manifest as a layer via a descriptor (see https://github.com/opencontainers/image-spec/blob/main/descriptor.md). | ||
// Here we compare the digest of the blob data with the layer digest to verify if this blob is associated with the layer. | ||
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. | ||
// Since public key is attached to the `optional` field of payload, we don't actually implement this method. | ||
// Instead we send payload directly to the Attestation service and let the service parse the payload. | ||
func (s Sig) PublicKey() ([]byte, error) { | ||
return nil, fmt.Errorf("not implemented") | ||
} | ||
|
||
// SigningAlgorithm implements oci.Signature interface. | ||
// Since signing algorithm is attached to the `optional` field of payload, we don't actually implement this method. | ||
// Instead we send payload directly to the Attestation service and let the service parse the payload. | ||
func (s Sig) SigningAlgorithm() (oci.SigningAlgorithm, error) { | ||
return "", fmt.Errorf("not implemented") | ||
} |
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,140 @@ | ||
package cosign | ||
|
||
import ( | ||
"bytes" | ||
"crypto/rand" | ||
"testing" | ||
|
||
"github.com/opencontainers/go-digest" | ||
v1 "github.com/opencontainers/image-spec/specs-go/v1" | ||
) | ||
|
||
func TestPayload(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
blob []byte | ||
wantDigest digest.Digest | ||
wantPayload []byte | ||
wantPass bool | ||
}{ | ||
{ | ||
name: "cosign signature Payload() success", | ||
blob: []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}`), | ||
wantDigest: "sha256:d1e44a76902409836227b982beb920189949927c2011f196594bd34c5bb8f8b1", | ||
wantPayload: []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}`), | ||
wantPass: true, | ||
}, | ||
{ | ||
name: "cosign signature Payload() failed with unmatched digest", | ||
blob: []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}`), | ||
wantDigest: "sha256:unmatched digest", | ||
wantPayload: []byte{}, | ||
wantPass: false, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
sig := &Sig{ | ||
Layer: v1.Descriptor{ | ||
Digest: tc.wantDigest, | ||
}, | ||
Blob: tc.blob, | ||
} | ||
gotPayload, err := sig.Payload() | ||
if err != nil && tc.wantPass { | ||
t.Errorf("Payload() failed for test case %v: %v", tc.name, err) | ||
} | ||
if !bytes.Equal(gotPayload, tc.wantPayload) { | ||
t.Errorf("Payload() failed for test case %v: got %v, but want %v", tc.name, gotPayload, tc.wantPayload) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestBase64Encoded(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
wantSignatureKey string | ||
wantSignature string | ||
wantPass bool | ||
}{ | ||
{ | ||
name: "cosign signature Base64Encoded() success", | ||
wantSignatureKey: CosignSigKey, | ||
wantSignature: randomBase64EncodedString(32), | ||
wantPass: true, | ||
}, | ||
{ | ||
name: "cosign signature Base64Encoded() failed with mismatched signature key", | ||
wantSignatureKey: "mismatched signature key", | ||
wantSignature: "", | ||
wantPass: false, | ||
}, | ||
{ | ||
name: "cosign signature Base64Encoded() failed with invalid base64 encoded signature", | ||
wantSignatureKey: CosignSigKey, | ||
wantSignature: "", | ||
wantPass: false, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
sig := &Sig{ | ||
Layer: v1.Descriptor{ | ||
Annotations: map[string]string{ | ||
tc.wantSignatureKey: tc.wantSignature, | ||
}, | ||
}, | ||
} | ||
gotSignature, err := sig.Base64Encoded() | ||
if err != nil && tc.wantPass { | ||
t.Errorf("Base64Encoded() failed for test case %v: %v", tc.name, err) | ||
} | ||
if gotSignature != tc.wantSignature { | ||
t.Errorf("Base64Encoded() failed for test case %v: got %v, but want %v", tc.name, gotSignature, tc.wantSignature) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestWorkflow(t *testing.T) { | ||
wantSig := randomBase64EncodedString(32) | ||
blob := []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}`) | ||
|
||
sig := &Sig{ | ||
Layer: v1.Descriptor{ | ||
Digest: digest.FromBytes(blob), | ||
Annotations: map[string]string{ | ||
CosignSigKey: wantSig, | ||
}, | ||
}, | ||
Blob: blob, | ||
} | ||
|
||
gotPayload, err := sig.Payload() | ||
if err != nil { | ||
t.Errorf("Payload() failed: %v", err) | ||
} | ||
if !bytes.Equal(gotPayload, blob) { | ||
t.Errorf("Payload() failed: got %v, but want %v", gotPayload, blob) | ||
} | ||
|
||
gotSig, err := sig.Base64Encoded() | ||
if err != nil { | ||
t.Errorf("Base64Encoded() failed: %v", err) | ||
} | ||
if gotSig != wantSig { | ||
t.Errorf("Base64Encoded() failed, got %s, but want %s", gotSig, wantSig) | ||
} | ||
} | ||
|
||
func randomBase64EncodedString(n int) string { | ||
b := make([]byte, n) | ||
_, err := rand.Read(b) | ||
if err != nil { | ||
return "" | ||
} | ||
return encoding.EncodeToString(b) | ||
} |
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,45 @@ | ||
// Package oci contains functionalities to interact with OCI image signatures. | ||
// https://github.com/opencontainers/image-spec/tree/main#readme. | ||
package oci | ||
|
||
// SigningAlgorithm is a specific type for string constants used for sigature signing and verification. | ||
type SigningAlgorithm string | ||
|
||
const ( | ||
// RSASSAPSS2048SHA256 is RSASSA-PSS 2048 bit key with a SHA256 digest supported for cosign sign. | ||
RSASSAPSS2048SHA256 SigningAlgorithm = "RSASSA_PSS_SHA256" | ||
// RSASSAPSS3072SHA256 is RSASSA-PSS 3072 bit key with a SHA256 digest supported for cosign sign. | ||
RSASSAPSS3072SHA256 SigningAlgorithm = "RSASSA_PSS_SHA256" | ||
// RSASSAPSS4096SHA256 is RSASSA-PSS 4096 bit key with a SHA256 digest supported for cosign sign. | ||
RSASSAPSS4096SHA256 SigningAlgorithm = "RSASSA_PSS_SHA256" | ||
// RSASSAPKCS1V152048SHA256 is RSASSA-PKCS1 v1.5 2048 bit key with a SHA256 digest supported for cosign sign. | ||
RSASSAPKCS1V152048SHA256 SigningAlgorithm = "RSASSA_PKCS1V15_SHA256" | ||
// RSASSAPKCS1V153072SHA256 is RSASSA-PKCS1 v1.5 3072 bit key with a SHA256 digest supported for cosign sign. | ||
RSASSAPKCS1V153072SHA256 SigningAlgorithm = "RSASSA_PKCS1V15_SHA256" | ||
// RSASSAPKCS1V154096SHA256 is RSASSA-PKCS1 v1.5 4096 bit key with a SHA256 digest supported for cosign sign. | ||
RSASSAPKCS1V154096SHA256 SigningAlgorithm = "RSASSA_PKCS1V15_SHA256" | ||
// ECDSAP256SHA256 is ECDSA on the P-256 Curve with a SHA256 digest supported for cosign sign. | ||
ECDSAP256SHA256 SigningAlgorithm = "ECDSA_P256_SHA256" | ||
) | ||
|
||
// Signature represents a single OCI image signature. | ||
type Signature interface { | ||
// Payload returns the blob data associated with a signature uploaded to an OCI registry. | ||
Payload() ([]byte, error) | ||
|
||
// Base64Encoded returns the base64-encoded signature of the signed payload. | ||
Base64Encoded() (string, error) | ||
|
||
// PublicKey returns a public key in the format of PEM-encoded byte slice. | ||
PublicKey() ([]byte, error) | ||
|
||
// SigningAlgorithm returns the signing algorithm specifications in the format of: | ||
// 1. RSASSAPSS2048SHA256 (RSASSA algorithm with PSS padding 2048 bit key with a SHA256 digest) | ||
// 2. RSASSAPSS3072SHA256 (RSASSA algorithm with PSS padding 3072 bit key with a SHA256 digest) | ||
// 3. RSASSAPSS4096SHA256 (RSASSA algorithm with PSS padding 4096 bit key with a SHA256 digest) | ||
// 4. RSASSAPKCS1V152048SHA256 (RSASSA algorithm with PKCS #1 v1.5 padding 2048 bit key with a SHA256 digest) | ||
// 5. RSASSAPKCS1V153072SHA256 (RSASSA algorithm with PKCS #1 v1.5 padding 3072 bit key with a SHA256 digest) | ||
// 6. RSASSAPKCS1V154096SHA256 (RSASSA algorithm with PKCS #1 v1.5 padding 4096 bit key with a SHA256 digest) | ||
// 7. ECDSAP256SHA256 (ECDSA on the P-256 Curve with a SHA256 digest) | ||
SigningAlgorithm() (SigningAlgorithm, error) | ||
} |
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
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