Skip to content

Commit

Permalink
DO NOT MERGE IRRESPONSIBLE Add Cosign verification support
Browse files Browse the repository at this point in the history
THIS MUST HAVE TOTAL CODE COVERAGE.

type: cosignSigned, with the usual keyData/keyPath.
Fulcio/Rekor is plausible _for the off-line rekor
log entry proofs_, but not currently implemented. Tests first.

NOTE: This only allows a single public key, not a keyring,
unlike simple signing. That seems problematic, there are
known users of that. But we can fix that later by adding
keyDirectory and the like.

NOTE: Cosign interoperability requires use of
signedIdentity: matchRepository. The fairly useful
signedIdentity: remapIdentity has no repository-match
functionality.

Signed-off-by: Miloslav Trmač <[email protected]>
  • Loading branch information
mtrmac committed Jul 7, 2022
1 parent bdb2613 commit 3f44c07
Show file tree
Hide file tree
Showing 9 changed files with 533 additions and 5 deletions.
37 changes: 35 additions & 2 deletions docs/containers-policy.json.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ This requirement rejects every image, and every signature.

### `signedBy`

This requirement requires an image to be signed with an expected identity, or accepts a signature if it is using an expected identity and key.
This requirement requires an image to be signed using “simple signing” with an expected identity, or accepts a signature if it is using an expected identity and key.

```js
{
Expand Down Expand Up @@ -236,6 +236,24 @@ used with `exactReference` or `exactRepository`.

<!-- ### `signedBaseLayer` -->


### `cosignSigned`

This requirement requires an image to be signed using a Cosign signature with an expected identity and key.

```js
{
"type": "cosignSigned",
"keyPath": "/path/to/local/keyring/file",
"keyData": "base64-encoded-keyring-data",
"signedIdentity": identity_requirement
}
```
Exactly one of `keyPath` and `keyData` must be present, containing a Cosign public key. Only signatures made by this key is accepted.

The `signedIdentity` field has the same semantics as in the `signedBy` requirement described above.
Note that `cosign`-created signatures only contain a repository, so only `matchRepository` and `exactRepository` can be used to accept them (and that does not protect against substitution of a signed image with an unexpected tag).

## Examples

It is *strongly* recommended to set the `default` policy to `reject`, and then
Expand All @@ -257,7 +275,22 @@ selectively allow individual transports and scopes as desired.
form, with the explicit /library/, must be used. */
"docker.io/library/busybox": [{"type": "insecureAcceptAnything"}],
/* Allow installing images from all subdomains */
"*.temporary-project.example.com": [{"type": "insecureAcceptAnything"}]
"*.temporary-project.example.com": [{"type": "insecureAcceptAnything"}],
/* A Cosign-signed repository */
"hostname:5000/myns/cosign-signed-with-full-references": [
{
"type": "cosignSigned",
"keyPath": "/path/to/cosign-pubkey.key"
}
],
/* A Cosign-signed repository, accepts signatures by /usr/bin/cosign */
"hostname:5000/myns/cosign-signed-risky": [
{
"type": "cosignSigned",
"keyPath": "/path/to/cosign-pubkey.key",
"signedIdentity": {"type": "matchRepository"}
}
]
/* Other docker: images use the global default policy and are rejected */
},
"dir": {
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ require (
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211

)

require (
Expand Down
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
44 changes: 44 additions & 0 deletions signature/internal/cosign_payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ 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 @@ -151,3 +153,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
}
103 changes: 103 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,107 @@ 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 {
// Coverage: This cannot currently happen, creating a prCosignSigned only fails
// if signedIdentity is nil, which we replace with a default above.
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 3f44c07

Please sign in to comment.