Skip to content

Commit

Permalink
Add verify-blob-attestation command and tests (#2337)
Browse files Browse the repository at this point in the history
Signed-off-by: Priya Wadhwa <[email protected]>

Signed-off-by: Priya Wadhwa <[email protected]>
  • Loading branch information
priyawadhwa authored Oct 14, 2022
1 parent 4c00207 commit 797033c
Show file tree
Hide file tree
Showing 8 changed files with 435 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/cosign/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func New() *cobra.Command {
cmd.AddCommand(Verify())
cmd.AddCommand(VerifyAttestation())
cmd.AddCommand(VerifyBlob())
cmd.AddCommand(VerifyBlobAttestation())
cmd.AddCommand(Triangulate())
cmd.AddCommand(Env())
cmd.AddCommand(version.WithFont("starwars"))
Expand Down
20 changes: 20 additions & 0 deletions cmd/cosign/cli/options/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,23 @@ func (o *VerifyDockerfileOptions) AddFlags(cmd *cobra.Command) {
cmd.Flags().BoolVar(&o.BaseImageOnly, "base-image-only", false,
"only verify the base image (the last FROM image in the Dockerfile)")
}

// VerifyBlobOptions is the top level wrapper for the `verify blob` command.
type VerifyBlobAttestationOptions struct {
Key string
SignaturePath string
PredicateOptions
}

var _ Interface = (*VerifyBlobOptions)(nil)

// AddFlags implements Interface
func (o *VerifyBlobAttestationOptions) AddFlags(cmd *cobra.Command) {
o.PredicateOptions.AddFlags(cmd)

cmd.Flags().StringVar(&o.Key, "key", "",
"path to the public key file, KMS URI or Kubernetes Secret")

cmd.Flags().StringVar(&o.SignaturePath, "signature", "",
"path to base64-encoded signature over attestation in DSSE format")
}
36 changes: 36 additions & 0 deletions cmd/cosign/cli/verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,3 +279,39 @@ The blob may be specified as a path to a file or - for stdin.`,
o.AddFlags(cmd)
return cmd
}

func VerifyBlobAttestation() *cobra.Command {
o := &options.VerifyBlobAttestationOptions{}

cmd := &cobra.Command{
Use: "verify-blob-attestation",
Short: "Verify an attestation on the supplied blob",
Long: `Verify an attestation on the supplied blob input using the specified key reference.
You may specify either a key or a kms reference to verify against.
The signature may be specified as a path to a file or a base64 encoded string.
The blob may be specified as a path to a file.`,
Example: ` cosign verify-blob-attestastion (--key <key path>|<key url>|<kms uri>) --signature <sig> [path to BLOB]
# Verify a simple blob attestation with a DSSE style signature
cosign verify-blob-attestastion --key cosign.pub (--signature <sig path>|<sig url>)[path to BLOB]
`,

Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
v := verify.VerifyBlobAttestationCommand{
KeyRef: o.Key,
PredicateType: o.PredicateOptions.Type,
SignaturePath: o.SignaturePath,
}
if len(args) != 1 {
return fmt.Errorf("no path to blob passed in, run `cosign verify-blob-attestation -h` for more help")
}
return v.Exec(cmd.Context(), args[0])
},
}

o.AddFlags(cmd)
return cmd
}
167 changes: 167 additions & 0 deletions cmd/cosign/cli/verify/verify_blob_attestation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//
// Copyright 2022 the Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package verify

import (
"bytes"
"context"
"crypto"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/in-toto/in-toto-golang/in_toto"
ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse"

"github.com/sigstore/cosign/cmd/cosign/cli/options"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/sigstore/cosign/pkg/cosign/pkcs11key"
sigs "github.com/sigstore/cosign/pkg/signature"
"github.com/sigstore/cosign/pkg/types"
"github.com/sigstore/sigstore/pkg/signature"
"github.com/sigstore/sigstore/pkg/signature/dsse"
)

// VerifyBlobAttestationCommand verifies an attestation on a supplied blob
// nolint
type VerifyBlobAttestationCommand struct {
CheckClaims bool
KeyRef string
PredicateType string

SignaturePath string // Path to the signature
}

// Exec runs the verification command
func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath string) error {
if c.SignaturePath == "" {
return fmt.Errorf("please specify path to the base64 encoded DSSE envelope signature via --signature")
}

// TODO: Add support for security keys and keyless signing
if !options.OneOf(c.KeyRef) {
return &options.PubKeyParseError{}
}

var err error
co := &cosign.CheckOpts{}

if c.CheckClaims {
co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier
}

keyRef := c.KeyRef

// TODO: keyless signing
co.SigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, keyRef)
if err != nil {
return fmt.Errorf("loading public key: %w", err)
}
pkcs11Key, ok := co.SigVerifier.(*pkcs11key.Key)
if ok {
defer pkcs11Key.Close()
}

// Read the signature and decode it (it should be base64-encoded)
encodedSig, err := os.ReadFile(filepath.Clean(c.SignaturePath))
if err != nil {
return fmt.Errorf("reading %s: %w", c.SignaturePath, err)
}
decodedSig, err := base64.StdEncoding.DecodeString(string(encodedSig))
if err != nil {
return fmt.Errorf("decoding signature: %w", err)
}

// Verify the signature on the attestation against the provided public key
env := ssldsse.Envelope{}
if err := json.Unmarshal(decodedSig, &env); err != nil {
return fmt.Errorf("marshaling envelope: %w", err)
}

if env.PayloadType != types.IntotoPayloadType {
return cosign.NewVerificationError("invalid payloadType %s on envelope. Expected %s", env.PayloadType, types.IntotoPayloadType)
}
dssev, err := ssldsse.NewEnvelopeVerifier(&dsse.VerifierAdapter{SignatureVerifier: co.SigVerifier})
if err != nil {
return fmt.Errorf("new envelope verifier: %w", err)
}
if _, err := dssev.Verify(&env); err != nil {
return fmt.Errorf("dsse verify: %w", err)
}

// Verify the attestation is for the provided blob and the predicate type
if err := verifyBlobAttestation(env, artifactPath, c.PredicateType); err != nil {
return err
}

fmt.Fprintln(os.Stderr, "Verified OK")
return nil
}

func verifyBlobAttestation(env ssldsse.Envelope, blobPath, predicateType string) error {
artifact, err := os.ReadFile(blobPath)
if err != nil {
return fmt.Errorf("reading %s: %w", blobPath, err)
}

// Get the actual digest of the blob
digest, _, err := signature.ComputeDigestForSigning(bytes.NewReader(artifact), crypto.SHA256, []crypto.Hash{crypto.SHA256, crypto.SHA384})
if err != nil {
return err
}
actualDigest := strings.ToLower(hex.EncodeToString(digest))

// Get the expected digest from the attestation
decodedPredicate, err := base64.StdEncoding.DecodeString(env.Payload)
if err != nil {
return fmt.Errorf("decoding dsse payload: %w", err)
}
var statement in_toto.Statement
if err := json.Unmarshal(decodedPredicate, &statement); err != nil {
return fmt.Errorf("decoding predicate: %w", err)
}

// Compare the actual and expected
if statement.Subject == nil {
return fmt.Errorf("no subject in intoto statement")
}
if len(statement.Subject) != 1 {
return fmt.Errorf("expected one subject in intoto statement")
}
subject := statement.Subject[0]
sha256Digest, ok := subject.Digest["sha256"]
if !ok {
return fmt.Errorf("no sha256 digest available")
}

if sha256Digest != actualDigest {
return fmt.Errorf("expected digest %s but %s has a digest of %s", sha256Digest, blobPath, actualDigest)
}

// Check the predicate
parsedPredicateType, err := options.ParsePredicateType(predicateType)
if err != nil {
return fmt.Errorf("parsing predicate type %s: %w", predicateType, err)
}
if statement.PredicateType != parsedPredicateType {
return fmt.Errorf("expected predicate type %s but is %s: specify an expected predicate type with the --type flag", parsedPredicateType, statement.PredicateType)
}
return nil
}
102 changes: 102 additions & 0 deletions cmd/cosign/cli/verify/verify_blob_attestation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2022 the Sigstore Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package verify

import (
"encoding/base64"
"encoding/json"
"os"
"path/filepath"
"testing"

ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse"
)

const (
blobContents = "some-payload"
anotherBlobContents = "another-blob"
blobSLSAProvenanceSignature = "eyJwYXlsb2FkVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5pbi10b3RvK2pzb24iLCJwYXlsb2FkIjoiZXlKZmRIbHdaU0k2SW1oMGRIQnpPaTh2YVc0dGRHOTBieTVwYnk5VGRHRjBaVzFsYm5RdmRqQXVNU0lzSW5CeVpXUnBZMkYwWlZSNWNHVWlPaUpvZEhSd2N6b3ZMM05zYzJFdVpHVjJMM0J5YjNabGJtRnVZMlV2ZGpBdU1pSXNJbk4xWW1wbFkzUWlPbHQ3SW01aGJXVWlPaUppYkc5aUlpd2laR2xuWlhOMElqcDdJbk5vWVRJMU5pSTZJalkxT0RjNE1XTmtOR1ZrT1dKallUWXdaR0ZqWkRBNVpqZGlZamt4TkdKaU5URTFNREpsT0dJMVpEWXhPV1kxTjJZek9XRXhaRFkxTWpVNU5tTmpNalFpZlgxZExDSndjbVZrYVdOaGRHVWlPbnNpWW5WcGJHUmxjaUk2ZXlKcFpDSTZJaklpZlN3aVluVnBiR1JVZVhCbElqb2llQ0lzSW1sdWRtOWpZWFJwYjI0aU9uc2lZMjl1Wm1sblUyOTFjbU5sSWpwN2ZYMTlmUT09Iiwic2lnbmF0dXJlcyI6W3sia2V5aWQiOiIiLCJzaWciOiJNR1lDTVFEWHZhVVAwZmlYdXJUcmZNNmtQNjRPcERCM0pzSlEzbFFHZWE5UmZBOVBCY3JmWTJOc0dxK1J0MzdnMlpqaUpKOENNUUNNY3pzVy9wOGJiekZOSkRqeEhlOFNRdTRTazhBa3htTEdLMVE2R2lUazAzb2hHU3dsZkZRNXMrTWxRTFpGZXpBPSJ9XX0="
dssePredicateEmptySubject = "ewogICJwYXlsb2FkVHlwZSI6ICJhcHBsaWNhdGlvbi92bmQuaW4tdG90bytqc29uIiwKICAicGF5bG9hZCI6ICJld29nSUNKZmRIbHdaU0k2SUNKb2RIUndjem92TDJsdUxYUnZkRzh1YVc4dlUzUmhkR1Z0Wlc1MEwzWXdMakVpTEFvZ0lDSndjbVZrYVdOaGRHVlVlWEJsSWpvZ0ltaDBkSEJ6T2k4dmMyeHpZUzVrWlhZdmNISnZkbVZ1WVc1alpTOTJNQzR5SWl3S0lDQWljM1ZpYW1WamRDSTZJRnNLSUNCZExBb2dJQ0p3Y21Wa2FXTmhkR1VpT2lCN0NpQWdJQ0FpWW5WcGJHUmxjaUk2SUhzS0lDQWdJQ0FnSW1sa0lqb2dJaklpQ2lBZ0lDQjlMQW9nSUNBZ0ltSjFhV3hrVkhsd1pTSTZJQ0o0SWl3S0lDQWdJQ0pwYm5adlkyRjBhVzl1SWpvZ2V3b2dJQ0FnSUNBaVkyOXVabWxuVTI5MWNtTmxJam9nZTMwS0lDQWdJSDBLSUNCOUNuMEsiLAogICJzaWduYXR1cmVzIjogWwogICAgewogICAgICAia2V5aWQiOiAiIiwKICAgICAgInNpZyI6ICJNR1lDTVFEWHZhVVAwZmlYdXJUcmZNNmtQNjRPcERCM0pzSlEzbFFHZWE5UmZBOVBCY3JmWTJOc0dxK1J0MzdnMlpqaUpKOENNUUNNY3pzVy9wOGJiekZOSkRqeEhlOFNRdTRTazhBa3htTEdLMVE2R2lUazAzb2hHU3dsZkZRNXMrTWxRTFpGZXpBPSIKICAgIH0KICBdCn0K"
dssePredicateMissingSha256 = "ewogICJwYXlsb2FkVHlwZSI6ICJhcHBsaWNhdGlvbi92bmQuaW4tdG90bytqc29uIiwKICAicGF5bG9hZCI6ICJld29nSUNKZmRIbHdaU0k2SUNKb2RIUndjem92TDJsdUxYUnZkRzh1YVc4dlUzUmhkR1Z0Wlc1MEwzWXdMakVpTEFvZ0lDSndjbVZrYVdOaGRHVlVlWEJsSWpvZ0ltaDBkSEJ6T2k4dmMyeHpZUzVrWlhZdmNISnZkbVZ1WVc1alpTOTJNQzR5SWl3S0lDQWljM1ZpYW1WamRDSTZJRnNLSUNBZ0lIc0tJQ0FnSUNBZ0ltNWhiV1VpT2lBaVlteHZZaUlzQ2lBZ0lDQWdJQ0prYVdkbGMzUWlPaUI3Q2lBZ0lDQWdJQ0FnSW01dmRITm9ZVEkxTmlJNklDSTJOVGczT0RGalpEUmxaRGxpWTJFMk1HUmhZMlF3T1dZM1ltSTVNVFJpWWpVeE5UQXlaVGhpTldRMk1UbG1OVGRtTXpsaE1XUTJOVEkxT1Raall6STBJZ29nSUNBZ0lDQjlDaUFnSUNCOUNpQWdYU3dLSUNBaWNISmxaR2xqWVhSbElqb2dld29nSUNBZ0ltSjFhV3hrWlhJaU9pQjdDaUFnSUNBZ0lDSnBaQ0k2SUNJeUlnb2dJQ0FnZlN3S0lDQWdJQ0ppZFdsc1pGUjVjR1VpT2lBaWVDSXNDaUFnSUNBaWFXNTJiMk5oZEdsdmJpSTZJSHNLSUNBZ0lDQWdJbU52Ym1acFoxTnZkWEpqWlNJNklIdDlDaUFnSUNCOUNpQWdmUXA5Q2c9PSIsCiAgInNpZ25hdHVyZXMiOiBbCiAgICB7CiAgICAgICJrZXlpZCI6ICIiLAogICAgICAic2lnIjogIk1HWUNNUURYdmFVUDBmaVh1clRyZk02a1A2NE9wREIzSnNKUTNsUUdlYTlSZkE5UEJjcmZZMk5zR3ErUnQzN2cyWmppSko4Q01RQ01jenNXL3A4YmJ6Rk5KRGp4SGU4U1F1NFNrOEFreG1MR0sxUTZHaVRrMDNvaEdTd2xmRlE1cytNbFFMWkZlekE9IgogICAgfQogIF0KfQo="
)

func TestVerifyBlobAttestation(t *testing.T) {
tmpdir := t.TempDir()
blobPath := filepath.Join(tmpdir, "blob")
if err := os.WriteFile(blobPath, []byte(blobContents), 0755); err != nil {
t.Fatal(err)
}
anotherBlobPath := filepath.Join(tmpdir, "another-blob")
if err := os.WriteFile(anotherBlobPath, []byte(anotherBlobContents), 0755); err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpdir)

tests := []struct {
description string
blobPath string
signature string
predicateType string
shouldErr bool
}{
{
description: "verify a slsaprovenance predicate",
predicateType: "slsaprovenance",
blobPath: blobPath,
signature: blobSLSAProvenanceSignature,
}, {
description: "fail with incorrect predicate",
signature: blobSLSAProvenanceSignature,
blobPath: blobPath,
predicateType: "custom",
shouldErr: true,
}, {
description: "fail with incorrect blob",
signature: blobSLSAProvenanceSignature,
blobPath: anotherBlobPath,
shouldErr: true,
}, {
description: "dsse envelope predicate has no subject",
signature: dssePredicateEmptySubject,
blobPath: blobPath,
shouldErr: true,
}, {
description: "dsse envelope predicate missing sha256 digest",
signature: dssePredicateMissingSha256,
blobPath: blobPath,
shouldErr: true,
},
}

for _, test := range tests {
t.Run(test.description, func(t *testing.T) {
decodedSig, err := base64.StdEncoding.DecodeString(test.signature)
if err != nil {
t.Fatal(err)
}

// Verify the signature on the attestation against the provided public key
env := ssldsse.Envelope{}
if err := json.Unmarshal(decodedSig, &env); err != nil {
t.Fatal(err)
}

err = verifyBlobAttestation(env, test.blobPath, test.predicateType)
if (err != nil) != test.shouldErr {
t.Fatalf("verifyBlobAttestation()= %s, expected shouldErr=%t ", err, test.shouldErr)
}
})
}
}
1 change: 1 addition & 0 deletions doc/cosign.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions doc/cosign_verify-blob-attestation.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 797033c

Please sign in to comment.