Skip to content

Commit

Permalink
Merge branch 'cosign-verify' into cosign-integration
Browse files Browse the repository at this point in the history
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
  • Loading branch information
mtrmac committed Jul 6, 2022
2 parents fc4612a + 6c19e68 commit 640ae0a
Show file tree
Hide file tree
Showing 21 changed files with 602 additions and 47 deletions.
6 changes: 0 additions & 6 deletions internal/image/sourced.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package image
import (
"context"

"github.com/containers/image/v5/internal/signature"
"github.com/containers/image/v5/types"
)

Expand Down Expand Up @@ -130,11 +129,6 @@ func (i *SourcedImage) Manifest(ctx context.Context) ([]byte, string, error) {
return i.ManifestBlob, i.ManifestMIMEType, nil
}

// SignaturesWithFormat is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need.
func (i *SourcedImage) SignaturesWithFormat(ctx context.Context) ([]signature.Signature, error) {
return i.UnparsedImage.signaturesWithFormat(ctx)
}

func (i *SourcedImage) LayerInfosForCopy(ctx context.Context) ([]types.BlobInfo, error) {
return i.UnparsedImage.src.LayerInfosForCopy(ctx, i.UnparsedImage.instanceDigest)
}
8 changes: 5 additions & 3 deletions internal/image/unparsed.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ func (i *UnparsedImage) expectedManifestDigest() (digest.Digest, bool) {

// Signatures is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need.
func (i *UnparsedImage) Signatures(ctx context.Context) ([][]byte, error) {
sigs, err := i.signaturesWithFormat(ctx)
// It would be consistent to make this an internal/unparsedimage/impl.Compat wrapper,
// but this is very likely to be the only implementation ever.
sigs, err := i.SignaturesWithFormat(ctx)
if err != nil {
return nil, err
}
Expand All @@ -105,8 +107,8 @@ func (i *UnparsedImage) Signatures(ctx context.Context) ([][]byte, error) {
return simpleSigs, nil
}

// signaturesWithFormat is like ImageSource.GetSignatures, but the result is cached; it is OK to call this however often you need.
func (i *UnparsedImage) signaturesWithFormat(ctx context.Context) ([]signature.Signature, error) {
// SignaturesWithFormat is like ImageSource.GetSignaturesWithFormat, but the result is cached; it is OK to call this however often you need.
func (i *UnparsedImage) SignaturesWithFormat(ctx context.Context) ([]signature.Signature, error) {
if i.cachedSignatures == nil {
sigs, err := i.src.GetSignaturesWithFormat(ctx, i.instanceDigest)
if err != nil {
Expand Down
7 changes: 7 additions & 0 deletions internal/private/private.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,10 @@ type BadPartialRequestError struct {
func (e BadPartialRequestError) Error() string {
return e.Status
}

// UnparsedImage is an internal extension to the types.UnparsedImage interface.
type UnparsedImage interface {
types.UnparsedImage
// SignaturesWithFormat is like ImageSource.GetSignaturesWithFormat, but the result is cached; it is OK to call this however often you need.
SignaturesWithFormat(ctx context.Context) ([]signature.Signature, error)
}
7 changes: 6 additions & 1 deletion internal/signature/cosign.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ package signature

import "encoding/json"

const CosignSignatureMIMEType = "application/vnd.dev.cosign.simplesigning.v1+json"
const (
// from sigstore/cosign/pkg/types.SimpleSigningMediaType
CosignSignatureMIMEType = "application/vnd.dev.cosign.simplesigning.v1+json"
// from sigstore/cosign/pkg/oci/static.SignatureAnnotationKey
CosignSignatureAnnotationKey = "dev.cosignproject.cosign/signature"
)

// Cosign is a github.com/Cosign/cosign signature.
// For the persistent-storage format used for blobChunk(), we want
Expand Down
6 changes: 6 additions & 0 deletions internal/testing/mocks/unparsed_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package mocks
import (
"context"

"github.com/containers/image/v5/internal/signature"
"github.com/containers/image/v5/types"
)

Expand All @@ -23,3 +24,8 @@ func (ref ForbiddenUnparsedImage) Manifest(ctx context.Context) ([]byte, string,
func (ref ForbiddenUnparsedImage) Signatures(context.Context) ([][]byte, error) {
panic("unexpected call to a mock function")
}

// SignaturesWithFormat is a mock that panics.
func (ref ForbiddenUnparsedImage) SignaturesWithFormat(ctx context.Context) ([]signature.Signature, error) {
panic("unexpected call to a mock function")
}
38 changes: 38 additions & 0 deletions internal/unparsedimage/wrapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package unparsedimage

import (
"context"

"github.com/containers/image/v5/internal/private"
"github.com/containers/image/v5/internal/signature"
"github.com/containers/image/v5/types"
)

// wrapped provides the private.UnparsedImage operations
// for an object that only implements types.UnparsedImage
type wrapped struct {
types.UnparsedImage
}

// FromPublic(unparsed) returns an object that provides the private.UnparsedImage API
func FromPublic(unparsed types.UnparsedImage) private.UnparsedImage {
if unparsed2, ok := unparsed.(private.UnparsedImage); ok {
return unparsed2
}
return &wrapped{
UnparsedImage: unparsed,
}
}

// SignaturesWithFormat is like ImageSource.GetSignaturesWithFormat, but the result is cached; it is OK to call this however often you need.
func (w *wrapped) SignaturesWithFormat(ctx context.Context) ([]signature.Signature, error) {
sigs, err := w.Signatures(ctx)
if err != nil {
return nil, err
}
res := []signature.Signature{}
for _, sig := range sigs {
res = append(res, signature.SimpleSigningFromBlob(sig))
}
return res, nil
}
15 changes: 15 additions & 0 deletions signature/fixtures/policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@
"dockerReference": "registry.access.redhat.com/rhel7/rhel:latest"
}
}
],
"example.com/cosign/key-data-example": [
{
"type": "cosignSigned",
"keyData": "bm9uc2Vuc2U="
}
],
"example.com/cosign/key-Path-example": [
{
"type": "cosignSigned",
"keyPath": "/keys/public-key",
"signedIdentity": {
"type": "matchRepository"
}
}
]
}
}
Expand Down
45 changes: 45 additions & 0 deletions signature/internal/cosign_payload.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package internal

import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"time"

"github.com/containers/image/v5/version"
digest "github.com/opencontainers/go-digest"
cosignSignature "github.com/sigstore/sigstore/pkg/signature"
)

const (
Expand Down Expand Up @@ -147,3 +150,45 @@ func (s *UntrustedCosignPayload) strictUnmarshalJSON(data []byte) error {
"docker-reference": &s.UntrustedDockerReference,
})
}

// CosignPayloadAcceptanceRules specifies how to decide whether an untrusted payload is acceptable.
// We centralize the actual parsing and data extraction in VerifyCosignPayload; this supplies
// the policy. We use an object instead of supplying func parameters to verifyAndExtractSignature
// because the functions have the same or similar types, so there is a risk of exchanging the functions;
// named members of this struct are more explicit.
type CosignPayloadAcceptanceRules struct {
ValidateSignedDockerReference func(string) error
ValidateSignedDockerManifestDigest func(digest.Digest) error
}

// VerifyCosignPayload verifies that unverifiedPayload has been signed by unverifiedBase64Signature, and that its principal components
// match expected values, both as specified by rules, and returns it.
// We return an *UntrustedCosignPayload, although nothing actually uses it,
// just to double-check against stupid typos.
func VerifyCosignPayload(verifier cosignSignature.Verifier, unverifiedPayload []byte, unverifiedBase64Signature string, rules CosignPayloadAcceptanceRules) (*UntrustedCosignPayload, error) {
// FIXME: THIS MUST HAVE TOTAL TEST COVERAGE.
unverifiedSignature, err := base64.StdEncoding.DecodeString(unverifiedBase64Signature)
if err != nil {
return nil, NewInvalidSignatureError(fmt.Sprintf("base64 decoding: %v", err))
}
// FIXME FIXME: Should we support multiple equally-acceptable public keys,
// like we do with simple signing keyrings?
// github.com/sigstore/cosign/pkg/cosign.verifyOCISignature uses signatureoptions.WithContext(),
// which seems to be not used by anything. So we don’t bother.
if err := verifier.VerifySignature(bytes.NewReader(unverifiedSignature), bytes.NewReader(unverifiedPayload)); err != nil {
return nil, NewInvalidSignatureError(fmt.Sprintf("cryptographic signature verification failed: %v", err))
}

var unmatchedPayload UntrustedCosignPayload
if err := json.Unmarshal(unverifiedPayload, &unmatchedPayload); err != nil {
return nil, NewInvalidSignatureError(err.Error())
}
if err := rules.ValidateSignedDockerManifestDigest(unmatchedPayload.UntrustedDockerManifestDigest); err != nil {
return nil, err
}
if err := rules.ValidateSignedDockerReference(unmatchedPayload.UntrustedDockerReference); err != nil {
return nil, err
}
// CosignPayloadAcceptanceRules have accepted this value.
return &unmatchedPayload, nil
}
101 changes: 101 additions & 0 deletions signature/policy_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ func newPolicyRequirementFromJSON(data []byte) (PolicyRequirement, error) {
res = &prSignedBy{}
case prTypeSignedBaseLayer:
res = &prSignedBaseLayer{}
case prTypeCosignSigned:
res = &prCosignSigned{}
default:
return nil, InvalidPolicyFormatError(fmt.Sprintf("Unknown policy requirement type \"%s\"", typeField.Type))
}
Expand Down Expand Up @@ -493,6 +495,105 @@ func (pr *prSignedBaseLayer) UnmarshalJSON(data []byte) error {
return nil
}

// newPRCosignSigned returns a new prCosignSigned if parameters are valid.
func newPRCosignSigned(keyPath string, keyData []byte, signedIdentity PolicyReferenceMatch) (*prCosignSigned, error) {
if len(keyPath) > 0 && len(keyData) > 0 {
return nil, InvalidPolicyFormatError("keyType and keyData cannot be used simultaneously")
}
if signedIdentity == nil {
return nil, InvalidPolicyFormatError("signedIdentity not specified")
}
return &prCosignSigned{
prCommon: prCommon{Type: prTypeCosignSigned},
KeyPath: keyPath,
KeyData: keyData,
SignedIdentity: signedIdentity,
}, nil
}

// newPRCosignSignedKeyPath is NewPRCosignSignedKeyPath, except it returns the private type.
func newPRCosignSignedKeyPath(keyPath string, signedIdentity PolicyReferenceMatch) (*prCosignSigned, error) {
return newPRCosignSigned(keyPath, nil, signedIdentity)
}

// NewPRCosignSignedKeyPath returns a new "Cosignsigned" PolicyRequirement using a KeyPath
func NewPRCosignSignedKeyPath(keyPath string, signedIdentity PolicyReferenceMatch) (PolicyRequirement, error) {
return newPRCosignSignedKeyPath(keyPath, signedIdentity)
}

// newPRCosignSignedKeyData is NewPRCosignSignedKeyData, except it returns the private type.
func newPRCosignSignedKeyData(keyData []byte, signedIdentity PolicyReferenceMatch) (*prCosignSigned, error) {
return newPRCosignSigned("", keyData, signedIdentity)
}

// NewPRCosignSignedKeyData returns a new "Cosignsigned" PolicyRequirement using a KeyData
func NewPRCosignSignedKeyData(keyData []byte, signedIdentity PolicyReferenceMatch) (PolicyRequirement, error) {
return newPRCosignSignedKeyData(keyData, signedIdentity)
}

// Compile-time check that prCosignSigned implements json.Unmarshaler.
var _ json.Unmarshaler = (*prCosignSigned)(nil)

// UnmarshalJSON implements the json.Unmarshaler interface.
func (pr *prCosignSigned) UnmarshalJSON(data []byte) error {
*pr = prCosignSigned{}
var tmp prCosignSigned
var gotKeyPath, gotKeyData = false, false
var signedIdentity json.RawMessage
if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) interface{} {
switch key {
case "type":
return &tmp.Type
case "keyPath":
gotKeyPath = true
return &tmp.KeyPath
case "keyData":
gotKeyData = true
return &tmp.KeyData
case "signedIdentity":
return &signedIdentity
default:
return nil
}
}); err != nil {
return err
}

if tmp.Type != prTypeCosignSigned {
return InvalidPolicyFormatError(fmt.Sprintf("Unexpected policy requirement type \"%s\"", tmp.Type))
}
if signedIdentity == nil {
tmp.SignedIdentity = NewPRMMatchRepoDigestOrExact()
} else {
si, err := newPolicyReferenceMatchFromJSON(signedIdentity)
if err != nil {
return err
}
tmp.SignedIdentity = si
}

var res *prCosignSigned
var err error
switch {
case gotKeyPath && gotKeyData:
return InvalidPolicyFormatError("keyPath and keyData cannot be used simultaneously")
case gotKeyPath && !gotKeyData:
res, err = newPRCosignSignedKeyPath(tmp.KeyPath, tmp.SignedIdentity)
case !gotKeyPath && gotKeyData:
res, err = newPRCosignSignedKeyData(tmp.KeyData, tmp.SignedIdentity)
case !gotKeyPath && !gotKeyData:
return InvalidPolicyFormatError("At least one of keyPath and keyData mus be specified")
default: // Coverage: This should never happen
return fmt.Errorf("Impossible keyPath/keyData presence combination!?")
}
if err != nil {
return err
}
*pr = *res

return nil
}

// newPolicyReferenceMatchFromJSON parses JSON data into a PolicyReferenceMatch implementation.
func newPolicyReferenceMatchFromJSON(data []byte) (PolicyReferenceMatch, error) {
var typeField prmCommon
Expand Down
Loading

0 comments on commit 640ae0a

Please sign in to comment.