From 38840cc871814ce551373629ccc7f36ee315095d Mon Sep 17 00:00:00 2001 From: Jake Sanders Date: Wed, 10 Nov 2021 13:01:41 -0800 Subject: [PATCH] remove `Verify` in favor of explicit `VerifyImage{Signatures, Attestations}` Signed-off-by: Jake Sanders --- cmd/cosign/cli/verify/verify.go | 2 +- cmd/cosign/cli/verify/verify_attestation.go | 2 +- pkg/cosign/kubernetes/webhook/validation.go | 2 +- pkg/cosign/verify.go | 335 +++++++++++++------- pkg/sget/sget.go | 2 +- 5 files changed, 216 insertions(+), 127 deletions(-) diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 52641a5a876..160767465b9 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -120,7 +120,7 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return errors.Wrapf(err, "resolving attachment type %s for image %s", c.Attachment, img) } - verified, bundleVerified, err := cosign.VerifySignatures(ctx, ref, co) + verified, bundleVerified, err := cosign.VerifyImageSignatures(ctx, ref, co) if err != nil { return err } diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 72aee484147..6f27365300a 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -107,7 +107,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return err } - verified, bundleVerified, err := cosign.VerifyAttestations(ctx, ref, co) + verified, bundleVerified, err := cosign.VerifyImageAttestations(ctx, ref, co) if err != nil { return err } diff --git a/pkg/cosign/kubernetes/webhook/validation.go b/pkg/cosign/kubernetes/webhook/validation.go index a966927e7a1..dc0e0f718ec 100644 --- a/pkg/cosign/kubernetes/webhook/validation.go +++ b/pkg/cosign/kubernetes/webhook/validation.go @@ -72,7 +72,7 @@ func valid(ctx context.Context, ref name.Reference, keys []*ecdsa.PublicKey, opt } // For testing -var cosignVerifySignatures = cosign.VerifySignatures +var cosignVerifySignatures = cosign.VerifyImageSignatures func validSignatures(ctx context.Context, ref name.Reference, verifier signature.Verifier, opts ...ociremote.Option) ([]oci.Signature, error) { sigs, _, err := cosignVerifySignatures(ctx, ref, &cosign.CheckOpts{ diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 9bb2d02eecc..5944ba7bfb5 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -84,57 +84,123 @@ func (w *reverseDSSEVerifier) VerifySignature(s io.Reader, m io.Reader, opts ... 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) { - return Verify(ctx, signedImgRef, SignaturesAccessor, co) +func getSignedEntity(signedImgRef name.Reference, regClientOpts []ociremote.Option) (oci.SignedEntity, v1.Hash, error) { + se, err := ociremote.SignedEntity(signedImgRef, regClientOpts...) + if err != nil { + return nil, v1.Hash{}, err + } + // Both of the SignedEntity types implement Digest() + h, err := se.(interface{ Digest() (v1.Hash, error) }).Digest() + if err != nil { + return nil, v1.Hash{}, err + } + return se, h, nil } -// VerifyAttestations does all the main cosign checks in a loop, returning the verified attestations. -// If there were no valid attestations, we return an error. -func VerifyAttestations(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { - return Verify(ctx, signedImgRef, AttestationsAccessor, co) +func verifyOCISignature(ctx context.Context, verifier signature.Verifier, sig oci.Signature) error { + b64sig, err := sig.Base64Signature() + if err != nil { + return err + } + signature, err := base64.StdEncoding.DecodeString(b64sig) + if err != nil { + return err + } + payload, err := sig.Payload() + if err != nil { + return err + } + return verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload), options.WithContext(ctx)) } -// Accessor is used by Verify to extract the signatures to be verified. -type Accessor func(oci.SignedEntity) (oci.Signatures, error) - -var ( - AttestationsAccessor Accessor = func(se oci.SignedEntity) (oci.Signatures, error) { return se.Attestations() } - SignaturesAccessor Accessor = func(se oci.SignedEntity) (oci.Signatures, error) { return se.Signatures() } -) - -// Verify does all the main cosign checks in a loop, returning the verified signatures. -// If there were no valid signatures, we return an error. -func Verify(ctx context.Context, signedImgRef name.Reference, accessor Accessor, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { - // Enforce this up front. - if co.RootCerts == nil && co.SigVerifier == nil { - return nil, false, errors.New("one of verifier or root certs is required") +func validateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) { + verifier, err := signature.LoadECDSAVerifier(cert.PublicKey.(*ecdsa.PublicKey), crypto.SHA256) + if err != nil { + return nil, errors.Wrap(err, "invalid certificate found on signature") } - validationErrs := []string{} - var rekorClient *client.Rekor - if co.RekorURL != "" { - rekorClient, err = rekor.GetRekorClient(co.RekorURL) - if err != nil { - return nil, false, err + // Now verify the cert, then the signature. + if err := TrustedCert(cert, co.RootCerts); err != nil { + return nil, err + } + if co.CertEmail != "" { + emailVerified := false + for _, em := range cert.EmailAddresses { + if co.CertEmail == em { + emailVerified = true + break + } + } + if !emailVerified { + return nil, errors.New("expected email not found in certificate") } } + return verifier, nil +} - se, err := ociremote.SignedEntity(signedImgRef, co.RegistryClientOpts...) +func tlogValidatePublicKey(rekorClient *client.Rekor, pub crypto.PublicKey, sig oci.Signature) error { + pemBytes, err := cryptoutils.MarshalPublicKeyToPEM(pub) if err != nil { - return nil, false, err + return err } - // Both of the SignedEntity types implement Digest() - h, err := se.(interface{ Digest() (v1.Hash, error) }).Digest() + b64sig, err := sig.Base64Signature() if err != nil { - return nil, false, err + return err + } + payload, err := sig.Payload() + if err != nil { + return err + } + _, _, err = FindTlogEntry(rekorClient, b64sig, payload, pemBytes) + return err +} + +func tlogValidateCertificate(rekorClient *client.Rekor, sig oci.Signature) error { + cert, err := sig.Cert() + if err != nil { + return err + } + pemBytes, err := cryptoutils.MarshalCertificateToPEM(cert) + if err != nil { + return err + } + b64sig, err := sig.Base64Signature() + if err != nil { + return err + } + payload, err := sig.Payload() + if err != nil { + return err + } + uuid, _, err := FindTlogEntry(rekorClient, b64sig, payload, pemBytes) + if err != nil { + return err + } + // if we have a cert, we should check expiry + // The IntegratedTime verified in VerifyTlog + e, err := GetTlogEntry(rekorClient, uuid) + if err != nil { + return err + } + return checkExpiry(cert, time.Unix(*e.IntegratedTime, 0)) +} + +// 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 VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { + // Enforce this up front. + if co.RootCerts == nil && co.SigVerifier == nil { + return nil, false, errors.New("one of verifier or root certs is required") } // TODO(mattmoor): We could implement recursive verification if we just wrapped // most of the logic below here in a call to mutate.Map - sigs, err := accessor(se) + se, h, err := getSignedEntity(signedImgRef, co.RegistryClientOpts) + if err != nil { + return nil, false, err + } + sigs, err := se.Signatures() if err != nil { return nil, false, err } @@ -142,83 +208,37 @@ func Verify(ctx context.Context, signedImgRef name.Reference, accessor Accessor, if err != nil { return nil, false, err } + + validationErrs := []string{} + + var rekorClient *client.Rekor + if co.RekorURL != "" { + rekorClient, err = rekor.GetRekorClient(co.RekorURL) + if err != nil { + return nil, false, err + } + } + for _, sig := range sl { if err := func(sig oci.Signature) error { - b64sig, err := sig.Base64Signature() - if err != nil { - return err - } - payload, err := sig.Payload() - if err != nil { - return err - } - cert, err := sig.Cert() - if err != nil { - return err - } - - switch { - // We have a public key to check against. - case co.SigVerifier != nil: - signature, err := base64.StdEncoding.DecodeString(b64sig) + verifier := co.SigVerifier + if verifier == nil { + // If we don't have a public key to check against, we can try a root cert. + cert, err := sig.Cert() 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 { - co.SigVerifier = newReverseDSSEVerifier(co.SigVerifier) - } - if err := co.SigVerifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload), options.WithContext(ctx)); err != nil { - return err - } - // If we don't have a public key to check against, we can try a root cert. - case co.RootCerts != nil: - // There might be signatures with a public key instead of a cert, though if cert == nil { return errors.New("no certificate found on signature") } - 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") - } - // Now verify the cert, then the signature. - if err := TrustedCert(cert, co.RootCerts); err != nil { - return err - } - - signature, err := base64.StdEncoding.DecodeString(b64sig) + verifier, err = validateAndUnpackCert(cert, co) 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 - } - if co.CertEmail != "" { - emailVerified := false - for _, em := range cert.EmailAddresses { - if co.CertEmail == em { - emailVerified = true - break - } - } - if !emailVerified { - return errors.New("expected email not found in certificate") - } - } + if err := verifyOCISignature(ctx, verifier, sig); err != nil { + return err } // We can't check annotations without claims, both require unmarshalling the payload. @@ -235,55 +255,124 @@ func Verify(ctx context.Context, signedImgRef name.Reference, accessor Accessor, bundleVerified = bundleVerified || verified if !verified && co.RekorURL != "" { - // Get the right public key to use (key or cert) - var pemBytes []byte if co.SigVerifier != nil { - var pub crypto.PublicKey - pub, err = co.SigVerifier.PublicKey(co.PKOpts...) + pub, err := co.SigVerifier.PublicKey(co.PKOpts...) if err != nil { return err } - pemBytes, err = cryptoutils.MarshalPublicKeyToPEM(pub) - } else { - pemBytes, err = cryptoutils.MarshalCertificateToPEM(cert) + return tlogValidatePublicKey(rekorClient, pub, sig) + } + + return tlogValidateCertificate(rekorClient, sig) + } + return nil + }(sig); err != nil { + validationErrs = append(validationErrs, err.Error()) + continue + } + + // Phew, we made it. + checkedSignatures = append(checkedSignatures, sig) + } + if len(checkedSignatures) == 0 { + return nil, false, fmt.Errorf("no matching signatures:\n%s", strings.Join(validationErrs, "\n ")) + } + return checkedSignatures, bundleVerified, nil +} + +// VerifyAttestations does all the main cosign checks in a loop, returning the verified attestations. +// If there were no valid attestations, we return an error. +func VerifyImageAttestations(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) { + // Enforce this up front. + if co.RootCerts == nil && co.SigVerifier == nil { + return nil, false, errors.New("one of verifier or root certs is required") + } + + // TODO(mattmoor): We could implement recursive verification if we just wrapped + // most of the logic below here in a call to mutate.Map + + se, h, err := getSignedEntity(signedImgRef, co.RegistryClientOpts) + if err != nil { + return nil, false, err + } + atts, err := se.Attestations() + if err != nil { + return nil, false, err + } + sl, err := atts.Get() + if err != nil { + return nil, false, err + } + + validationErrs := []string{} + + var rekorClient *client.Rekor + if co.RekorURL != "" { + rekorClient, err = rekor.GetRekorClient(co.RekorURL) + if err != nil { + return nil, false, err + } + } + for _, att := range sl { + if err := func(att oci.Signature) error { + verifier := co.SigVerifier + if verifier == nil { + // If we don't have a public key to check against, we can try a root cert. + cert, err := att.Cert() + if err != nil { + return err } + if cert == nil { + return errors.New("no certificate found on attestation") + } + verifier, err = validateAndUnpackCert(cert, co) if err != nil { return err } + } + verifier = newReverseDSSEVerifier(verifier) - // Find the uuid then the entry. - uuid, _, err := FindTlogEntry(rekorClient, b64sig, payload, pemBytes) - if err != nil { + if err := verifyOCISignature(ctx, verifier, att); err != nil { + return err + } + + // We can't check annotations without claims, both require unmarshalling the payload. + if co.ClaimVerifier != nil { + if err := co.ClaimVerifier(att, h, co.Annotations); err != nil { return err } + } - // if we have a cert, we should check expiry - // The IntegratedTime verified in VerifyTlog - if cert != nil { - e, err := GetTlogEntry(rekorClient, uuid) - if err != nil { - return err - } + verified, err := VerifyBundle(att) + if err != nil && co.RekorURL == "" { + return errors.Wrap(err, "unable to verify bundle") + } + bundleVerified = bundleVerified || verified - // Expiry check is only enabled with Tlog support - if err := checkExpiry(cert, time.Unix(*e.IntegratedTime, 0)); err != nil { + if !verified && co.RekorURL != "" { + if co.SigVerifier != nil { + pub, err := co.SigVerifier.PublicKey(co.PKOpts...) + if err != nil { return err } + return tlogValidatePublicKey(rekorClient, pub, att) } + + return tlogValidateCertificate(rekorClient, att) } return nil - }(sig); err != nil { + }(att); err != nil { validationErrs = append(validationErrs, err.Error()) continue } // Phew, we made it. - checkedSignatures = append(checkedSignatures, sig) + checkedAttestations = append(checkedAttestations, att) } - if len(checkedSignatures) == 0 { - return nil, false, fmt.Errorf("no matching signatures:\n%s", strings.Join(validationErrs, "\n ")) + if len(checkedAttestations) == 0 { + return nil, false, fmt.Errorf("no matching attestations:\n%s", strings.Join(validationErrs, "\n ")) } - return checkedSignatures, bundleVerified, nil + return checkedAttestations, bundleVerified, nil } func checkExpiry(cert *x509.Certificate, it time.Time) error { diff --git a/pkg/sget/sget.go b/pkg/sget/sget.go index 33bb04265a9..9b4566143e2 100644 --- a/pkg/sget/sget.go +++ b/pkg/sget/sget.go @@ -85,7 +85,7 @@ func (sg *SecureGet) Do(ctx context.Context) error { if co.SigVerifier != nil || options.EnableExperimental() { co.RootCerts = fulcio.GetRoots() - sp, bundleVerified, err := cosign.VerifySignatures(ctx, ref, co) + sp, bundleVerified, err := cosign.VerifyImageSignatures(ctx, ref, co) if err != nil { return err }