diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index e10459f877e..fae0190089a 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -21,7 +21,6 @@ import ( "encoding/json" "flag" "fmt" - "io" "os" "path/filepath" @@ -37,7 +36,6 @@ import ( "github.com/sigstore/cosign/pkg/cosign/pivkey" sigs "github.com/sigstore/cosign/pkg/signature" "github.com/sigstore/sigstore/pkg/signature" - "github.com/sigstore/sigstore/pkg/signature/dsse" ) // VerifyAttestationCommand verifies a signature on a supplied container image @@ -55,17 +53,6 @@ type VerifyAttestationCommand struct { Policies []string } -// DSSE messages contain the signature and payload in one object, but our interface expects a signature and payload -// This means we need to use one field and ignore the other. The DSSE verifier upstream uses the signature field and ignores -// The message field, but we want the reverse here. -type reverseDSSEVerifier struct { - signature.Verifier -} - -func (w *reverseDSSEVerifier) VerifySignature(s io.Reader, m io.Reader, opts ...signature.VerifyOption) error { - return w.Verifier.VerifySignature(m, nil, opts...) -} - // Exec runs the verification command func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (err error) { if len(images) == 0 { @@ -80,7 +67,6 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e if err != nil { return errors.Wrap(err, "constructing client options") } - co := &cosign.CheckOpts{ RegistryClientOpts: ociremoteOpts, } @@ -111,9 +97,11 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return errors.Wrap(err, "initializing piv token verifier") } } - - co.SigVerifier = &reverseDSSEVerifier{ - Verifier: dsse.WrapVerifier(pubKey), + if pubKey != nil { + // TODO(vaikas): Should this be private and cosign just figures out + // how to wrap things. This would mean we need to pass more context, so + // just making it like this for now. + co.SigVerifier = cosign.NewReverseDSSEVerifier(pubKey) } for _, imageRef := range images { diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 2aa18ddb0e9..45ce27262d4 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -24,6 +24,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "io" "strings" "time" @@ -38,6 +39,7 @@ import ( "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/sigstore/pkg/signature/dsse" "github.com/sigstore/sigstore/pkg/signature/options" ) @@ -65,6 +67,23 @@ type CheckOpts struct { CertEmail string } +// DSSE messages (Attestations) contain the signature and payload in one object, but our interface expects a signature and payload +// This means we need to use one field and ignore the other. The DSSE verifier upstream uses the signature field and ignores +// The message field, but we want the reverse here. +type reverseDSSEVerifier struct { + signature.Verifier +} + +func NewReverseDSSEVerifier(v signature.Verifier) signature.Verifier { + return &reverseDSSEVerifier{ + Verifier: dsse.WrapVerifier(v), + } +} + +func (w *reverseDSSEVerifier) VerifySignature(s io.Reader, m io.Reader, opts ...signature.VerifyOption) error { + return w.Verifier.VerifySignature(m, nil, opts...) +} + // VerifySignatures does all the main cosign checks in a loop, returning the verified signatures. // If there were no valid signatures, we return an error. func VerifySignatures(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { @@ -154,7 +173,8 @@ func Verify(ctx context.Context, signedImgRef name.Reference, accessor Accessor, if cert == nil { return errors.New("no certificate found on signature") } - pub, err := signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) + var pub signature.Verifier + pub, err = signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) if err != nil { return errors.Wrap(err, "invalid certificate found on signature") } @@ -167,6 +187,14 @@ func Verify(ctx context.Context, signedImgRef name.Reference, accessor Accessor, if err != nil { return err } + + // The fact that there's no signature (or empty rather), implies + // that this is an Attestation that we're verifying. So, we need + // to construct a Verifier that grabs the signature from the + // payload instead of the Signatures annotations. + if len(signature) == 0 { + pub = NewReverseDSSEVerifier(pub) + } if err := pub.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload), options.WithContext(ctx)); err != nil { return err } diff --git a/pkg/oci/interface.go b/pkg/oci/interface.go index 46a9a4e914b..bf7a8973caa 100644 --- a/pkg/oci/interface.go +++ b/pkg/oci/interface.go @@ -22,6 +22,8 @@ type SignedEntity interface { // Attestations returns the set of attestations currently associated with this // entity, or the empty equivalent if none are found. + // Attestations are just like a Signature, but they do not contain + // Base64Signature because it's baked into the payload. Attestations() (Signatures, error) // Attachment returns a named entity associated with this entity, or error if not found. diff --git a/pkg/oci/static/signature.go b/pkg/oci/static/signature.go index 1503f82eafb..c16e0341dd1 100644 --- a/pkg/oci/static/signature.go +++ b/pkg/oci/static/signature.go @@ -49,6 +49,9 @@ func NewSignature(payload []byte, b64sig string, opts ...Option) (oci.Signature, } // NewAttestation constructs a new oci.Signature from the provided options. +// Since Attestation is treated just like a Signature but the actual signature +// is baked into the payload, the Signature does not actually have +// the Base64Signature. func NewAttestation(payload []byte, opts ...Option) (oci.Signature, error) { return NewSignature(payload, "", opts...) }