Merge pull request #876 from developer-guy/feature/863
[RFC-0003] Implement OCIRepository verification using Cosign
This commit is contained in:
commit
c9a5a56cfb
|
@ -71,6 +71,10 @@ const (
|
|||
// required fields, or the provided credentials do not match.
|
||||
AuthenticationFailedReason string = "AuthenticationFailed"
|
||||
|
||||
// VerificationError signals that the Source's verification
|
||||
// check failed.
|
||||
VerificationError string = "VerificationError"
|
||||
|
||||
// DirCreationFailedReason signals a failure caused by a directory creation
|
||||
// operation.
|
||||
DirCreationFailedReason string = "DirectoryCreationFailed"
|
||||
|
|
|
@ -78,6 +78,12 @@ type OCIRepositorySpec struct {
|
|||
// +optional
|
||||
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
|
||||
|
||||
// Verify contains the secret name containing the trusted public keys
|
||||
// used to verify the signature and specifies which provider to use to check
|
||||
// whether OCI image is authentic.
|
||||
// +optional
|
||||
Verify *OCIRepositoryVerification `json:"verify,omitempty"`
|
||||
|
||||
// ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
|
||||
// the image pull if the service account has attached pull secrets. For more information:
|
||||
// https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account
|
||||
|
@ -156,11 +162,13 @@ type OCILayerSelector struct {
|
|||
type OCIRepositoryVerification struct {
|
||||
// Provider specifies the technology used to sign the OCI Artifact.
|
||||
// +kubebuilder:validation:Enum=cosign
|
||||
// +kubebuilder:default:=cosign
|
||||
Provider string `json:"provider"`
|
||||
|
||||
// SecretRef specifies the Kubernetes Secret containing the
|
||||
// trusted public keys.
|
||||
SecretRef meta.LocalObjectReference `json:"secretRef"`
|
||||
// +optional
|
||||
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
|
||||
}
|
||||
|
||||
// OCIRepositoryStatus defines the observed state of OCIRepository
|
||||
|
|
|
@ -729,6 +729,11 @@ func (in *OCIRepositorySpec) DeepCopyInto(out *OCIRepositorySpec) {
|
|||
*out = new(meta.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
if in.Verify != nil {
|
||||
in, out := &in.Verify, &out.Verify
|
||||
*out = new(OCIRepositoryVerification)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.CertSecretRef != nil {
|
||||
in, out := &in.CertSecretRef, &out.CertSecretRef
|
||||
*out = new(meta.LocalObjectReference)
|
||||
|
@ -788,7 +793,11 @@ func (in *OCIRepositoryStatus) DeepCopy() *OCIRepositoryStatus {
|
|||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *OCIRepositoryVerification) DeepCopyInto(out *OCIRepositoryVerification) {
|
||||
*out = *in
|
||||
out.SecretRef = in.SecretRef
|
||||
if in.SecretRef != nil {
|
||||
in, out := &in.SecretRef, &out.SecretRef
|
||||
*out = new(meta.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRepositoryVerification.
|
||||
|
|
|
@ -148,6 +148,31 @@ spec:
|
|||
on a remote container registry.
|
||||
pattern: ^oci://.*$
|
||||
type: string
|
||||
verify:
|
||||
description: Verify contains the secret name containing the trusted
|
||||
public keys used to verify the signature and specifies which provider
|
||||
to use to check whether OCI image is authentic.
|
||||
properties:
|
||||
provider:
|
||||
default: cosign
|
||||
description: Provider specifies the technology used to sign the
|
||||
OCI Artifact.
|
||||
enum:
|
||||
- cosign
|
||||
type: string
|
||||
secretRef:
|
||||
description: SecretRef specifies the Kubernetes Secret containing
|
||||
the trusted public keys.
|
||||
properties:
|
||||
name:
|
||||
description: Name of the referent.
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
type: object
|
||||
required:
|
||||
- provider
|
||||
type: object
|
||||
required:
|
||||
- interval
|
||||
- url
|
||||
|
|
|
@ -51,6 +51,8 @@ spec:
|
|||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: TUF_ROOT # store the Fulcio root CA file in tmp
|
||||
value: "/tmp/.sigstore"
|
||||
args:
|
||||
- --watch-all-namespaces
|
||||
- --log-level=info
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: podinfo-deploy-signed-with-key
|
||||
spec:
|
||||
interval: 5m
|
||||
url: oci://ghcr.io/stefanprodan/podinfo-deploy
|
||||
ref:
|
||||
semver: "6.2.x"
|
||||
verify:
|
||||
provider: cosign
|
||||
secretRef:
|
||||
name: cosign-key
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: podinfo-deploy-signed-with-keyless
|
||||
spec:
|
||||
interval: 5m
|
||||
url: oci://ghcr.io/stefanprodan/manifests/podinfo
|
||||
ref:
|
||||
semver: "6.2.x"
|
||||
verify:
|
||||
provider: cosign
|
|
@ -29,6 +29,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
soci "github.com/fluxcd/source-controller/internal/oci"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/authn/k8schain"
|
||||
"github.com/google/go-containerregistry/pkg/crane"
|
||||
|
@ -75,6 +76,7 @@ var ociRepositoryReadyCondition = summarize.Conditions{
|
|||
sourcev1.FetchFailedCondition,
|
||||
sourcev1.ArtifactOutdatedCondition,
|
||||
sourcev1.ArtifactInStorageCondition,
|
||||
sourcev1.SourceVerifiedCondition,
|
||||
meta.ReadyCondition,
|
||||
meta.ReconcilingCondition,
|
||||
meta.StalledCondition,
|
||||
|
@ -84,6 +86,7 @@ var ociRepositoryReadyCondition = summarize.Conditions{
|
|||
sourcev1.FetchFailedCondition,
|
||||
sourcev1.ArtifactOutdatedCondition,
|
||||
sourcev1.ArtifactInStorageCondition,
|
||||
sourcev1.SourceVerifiedCondition,
|
||||
meta.StalledCondition,
|
||||
meta.ReconcilingCondition,
|
||||
},
|
||||
|
@ -308,7 +311,7 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
|
|||
}
|
||||
options = append(options, crane.WithAuthFromKeychain(keychain))
|
||||
|
||||
if _, ok := keychain.(util.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok {
|
||||
if _, ok := keychain.(soci.Anonymous); obj.Spec.Provider != sourcev1.GenericOCIProvider && ok {
|
||||
auth, authErr := oidcAuth(ctxTimeout, obj.Spec.URL, obj.Spec.Provider)
|
||||
if authErr != nil && !errors.Is(authErr, oci.ErrUnconfiguredProvider) {
|
||||
e := serror.NewGeneric(
|
||||
|
@ -406,6 +409,33 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
|
|||
}
|
||||
}()
|
||||
|
||||
// Verify artifact if:
|
||||
// - the upstream digest differs from the one in storage (revision drift)
|
||||
// - the OCIRepository spec has changed (generation drift)
|
||||
// - the previous reconciliation resulted in a failed artifact verification (retry with exponential backoff)
|
||||
if obj.Spec.Verify == nil {
|
||||
// Remove old observations if verification was disabled
|
||||
conditions.Delete(obj, sourcev1.SourceVerifiedCondition)
|
||||
} else if !obj.GetArtifact().HasRevision(revision) ||
|
||||
conditions.GetObservedGeneration(obj, sourcev1.SourceVerifiedCondition) != obj.Generation ||
|
||||
conditions.IsFalse(obj, sourcev1.SourceVerifiedCondition) {
|
||||
err := r.verifyOCISourceSignature(ctx, obj, url, keychain)
|
||||
if err != nil {
|
||||
provider := obj.Spec.Verify.Provider
|
||||
if obj.Spec.Verify.SecretRef == nil {
|
||||
provider = fmt.Sprintf("%s keyless", provider)
|
||||
}
|
||||
e := serror.NewGeneric(
|
||||
fmt.Errorf("failed to verify the signature using provider '%s': %w", provider, err),
|
||||
sourcev1.VerificationError,
|
||||
)
|
||||
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, e.Reason, e.Err.Error())
|
||||
return sreconcile.ResultEmpty, e
|
||||
}
|
||||
|
||||
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of digest %s", revision)
|
||||
}
|
||||
|
||||
// Extract the content of the first artifact layer
|
||||
if !obj.GetArtifact().HasRevision(revision) {
|
||||
layers, err := img.Layers()
|
||||
|
@ -484,6 +514,86 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, obj *sour
|
|||
return sreconcile.ResultSuccess, nil
|
||||
}
|
||||
|
||||
// verifyOCISourceSignature verifies the authenticity of the given image reference url. First, it tries using a key
|
||||
// if a secret with a valid public key is provided. If not, it falls back to a keyless approach for verification.
|
||||
func (r *OCIRepositoryReconciler) verifyOCISourceSignature(ctx context.Context, obj *sourcev1.OCIRepository, url string, keychain authn.Keychain) error {
|
||||
ctxTimeout, cancel := context.WithTimeout(ctx, obj.Spec.Timeout.Duration)
|
||||
defer cancel()
|
||||
|
||||
provider := obj.Spec.Verify.Provider
|
||||
switch provider {
|
||||
case "cosign":
|
||||
defaultCosignOciOpts := []soci.Options{
|
||||
soci.WithAuthnKeychain(keychain),
|
||||
}
|
||||
|
||||
ref, err := name.ParseReference(url)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get the public keys from the given secret
|
||||
if secretRef := obj.Spec.Verify.SecretRef; secretRef != nil {
|
||||
certSecretName := types.NamespacedName{
|
||||
Namespace: obj.Namespace,
|
||||
Name: secretRef.Name,
|
||||
}
|
||||
|
||||
var pubSecret corev1.Secret
|
||||
if err := r.Get(ctxTimeout, certSecretName, &pubSecret); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signatureVerified := false
|
||||
for k, data := range pubSecret.Data {
|
||||
// search for public keys in the secret
|
||||
if strings.HasSuffix(k, ".pub") {
|
||||
verifier, err := soci.NewVerifier(ctxTimeout, append(defaultCosignOciOpts, soci.WithPublicKey(data))...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signatures, _, err := verifier.VerifyImageSignatures(ctxTimeout, ref)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if signatures != nil {
|
||||
signatureVerified = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !signatureVerified {
|
||||
return fmt.Errorf("no matching signatures were found for '%s'", url)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// if no secret is provided, try keyless verification
|
||||
ctrl.LoggerFrom(ctx).Info("no secret reference is provided, trying to verify the image using keyless method")
|
||||
verifier, err := soci.NewVerifier(ctxTimeout, defaultCosignOciOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signatures, _, err := verifier.VerifyImageSignatures(ctxTimeout, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(signatures) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("no matching signatures were found for '%s'", url)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseRepositoryURL validates and extracts the repository URL.
|
||||
func (r *OCIRepositoryReconciler) parseRepositoryURL(obj *sourcev1.OCIRepository) (string, error) {
|
||||
if !strings.HasPrefix(obj.Spec.URL, sourcev1.OCIRepositoryPrefix) {
|
||||
|
@ -591,7 +701,7 @@ func (r *OCIRepositoryReconciler) keychain(ctx context.Context, obj *sourcev1.OC
|
|||
|
||||
// if no pullsecrets available return an AnonymousKeychain
|
||||
if len(pullSecretNames) == 0 {
|
||||
return util.Anonymous{}, nil
|
||||
return soci.Anonymous{}, nil
|
||||
}
|
||||
|
||||
// lookup image pull secrets
|
||||
|
@ -651,7 +761,6 @@ func (r *OCIRepositoryReconciler) transport(ctx context.Context, obj *sourcev1.O
|
|||
tlsConfig.RootCAs = syscerts
|
||||
}
|
||||
return transport, nil
|
||||
|
||||
}
|
||||
|
||||
// oidcAuth generates the OIDC credential authenticator based on the specified cloud provider.
|
||||
|
|
|
@ -13,6 +13,7 @@ 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 controllers
|
||||
|
||||
import (
|
||||
|
@ -52,6 +53,9 @@ import (
|
|||
gcrv1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
. "github.com/onsi/gomega"
|
||||
coptions "github.com/sigstore/cosign/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/sign"
|
||||
"github.com/sigstore/cosign/pkg/cosign"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
@ -1005,6 +1009,227 @@ func TestOCIRepository_reconcileSource_remoteReference(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOCIRepository_reconcileSource_verifyOCISourceSignature(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
server, err := setupRegistryServer(ctx, tmpDir, registryOptions{})
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
podinfoVersions, err := pushMultiplePodinfoImages(server.registryHost, "6.1.4", "6.1.5")
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
img4 := podinfoVersions["6.1.4"]
|
||||
img5 := podinfoVersions["6.1.5"]
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reference *sourcev1.OCIRepositoryRef
|
||||
digest string
|
||||
want sreconcile.Result
|
||||
wantErr bool
|
||||
wantErrMsg string
|
||||
shouldSign bool
|
||||
keyless bool
|
||||
beforeFunc func(obj *sourcev1.OCIRepository)
|
||||
assertConditions []metav1.Condition
|
||||
}{
|
||||
{
|
||||
name: "signed image should pass verification",
|
||||
reference: &sourcev1.OCIRepositoryRef{
|
||||
Tag: "6.1.4",
|
||||
},
|
||||
digest: img4.digest.Hex,
|
||||
shouldSign: true,
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of digest <digest>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsigned image should not pass verification",
|
||||
reference: &sourcev1.OCIRepositoryRef{
|
||||
Tag: "6.1.5",
|
||||
},
|
||||
digest: img5.digest.Hex,
|
||||
wantErr: true,
|
||||
wantErrMsg: "failed to verify the signature using provider 'cosign': no matching signatures were found for '<url>'",
|
||||
want: sreconcile.ResultEmpty,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider '<provider>': no matching signatures were found for '<url>'"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unsigned image should not pass keyless verification",
|
||||
reference: &sourcev1.OCIRepositoryRef{
|
||||
Tag: "6.1.5",
|
||||
},
|
||||
digest: img5.digest.Hex,
|
||||
wantErr: true,
|
||||
want: sreconcile.ResultEmpty,
|
||||
keyless: true,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(meta.ReconcilingCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
|
||||
*conditions.TrueCondition(sourcev1.ArtifactOutdatedCondition, "NewRevision", "new digest '<digest>' for '<url>'"),
|
||||
*conditions.FalseCondition(sourcev1.SourceVerifiedCondition, sourcev1.VerificationError, "failed to verify the signature using provider '<provider> keyless': no matching signatures"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "verify failed before, removed from spec, remove condition",
|
||||
reference: &sourcev1.OCIRepositoryRef{Tag: "6.1.4"},
|
||||
digest: img4.digest.Hex,
|
||||
beforeFunc: func(obj *sourcev1.OCIRepository) {
|
||||
conditions.MarkFalse(obj, sourcev1.SourceVerifiedCondition, "VerifyFailed", "fail msg")
|
||||
obj.Spec.Verify = nil
|
||||
obj.Status.Artifact = &sourcev1.Artifact{Revision: img4.digest.Hex}
|
||||
},
|
||||
want: sreconcile.ResultSuccess,
|
||||
},
|
||||
{
|
||||
name: "same artifact, verified before, change in obj gen verify again",
|
||||
reference: &sourcev1.OCIRepositoryRef{Tag: "6.1.4"},
|
||||
digest: img4.digest.Hex,
|
||||
shouldSign: true,
|
||||
beforeFunc: func(obj *sourcev1.OCIRepository) {
|
||||
obj.Status.Artifact = &sourcev1.Artifact{Revision: img4.digest.Hex}
|
||||
// Set Verified with old observed generation and different reason/message.
|
||||
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Verified", "verified")
|
||||
// Set new object generation.
|
||||
obj.SetGeneration(3)
|
||||
},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, meta.SucceededReason, "verified signature of digest <digest>"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no verify for already verified, verified condition remains the same",
|
||||
reference: &sourcev1.OCIRepositoryRef{Tag: "6.1.4"},
|
||||
digest: img4.digest.Hex,
|
||||
shouldSign: true,
|
||||
beforeFunc: func(obj *sourcev1.OCIRepository) {
|
||||
// Artifact present and custom verified condition reason/message.
|
||||
obj.Status.Artifact = &sourcev1.Artifact{Revision: img4.digest.Hex}
|
||||
conditions.MarkTrue(obj, sourcev1.SourceVerifiedCondition, "Verified", "verified")
|
||||
},
|
||||
want: sreconcile.ResultSuccess,
|
||||
assertConditions: []metav1.Condition{
|
||||
*conditions.TrueCondition(sourcev1.SourceVerifiedCondition, "Verified", "verified"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
builder := fakeclient.NewClientBuilder().WithScheme(testEnv.GetScheme())
|
||||
|
||||
r := &OCIRepositoryReconciler{
|
||||
Client: builder.Build(),
|
||||
EventRecorder: record.NewFakeRecorder(32),
|
||||
Storage: testStorage,
|
||||
}
|
||||
|
||||
pf := func(b bool) ([]byte, error) {
|
||||
return []byte("cosign-password"), nil
|
||||
}
|
||||
|
||||
keys, err := cosign.GenerateKeyPair(pf)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = os.WriteFile(path.Join(tmpDir, "cosign.key"), keys.PrivateBytes, 0600)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cosign-key",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
"cosign.pub": keys.PublicBytes,
|
||||
}}
|
||||
|
||||
err = r.Create(ctx, secret)
|
||||
if err != nil {
|
||||
g.Expect(err).NotTo(HaveOccurred())
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
obj := &sourcev1.OCIRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: "verify-oci-source-signature-",
|
||||
},
|
||||
Spec: sourcev1.OCIRepositorySpec{
|
||||
URL: fmt.Sprintf("oci://%s/podinfo", server.registryHost),
|
||||
Verify: &sourcev1.OCIRepositoryVerification{
|
||||
Provider: "cosign",
|
||||
},
|
||||
Interval: metav1.Duration{Duration: interval},
|
||||
Timeout: &metav1.Duration{Duration: timeout},
|
||||
},
|
||||
}
|
||||
|
||||
if !tt.keyless {
|
||||
obj.Spec.Verify.SecretRef = &meta.LocalObjectReference{Name: "cosign-key"}
|
||||
}
|
||||
|
||||
if tt.reference != nil {
|
||||
obj.Spec.Reference = tt.reference
|
||||
}
|
||||
|
||||
keychain, err := r.keychain(ctx, obj)
|
||||
if err != nil {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
opts := r.craneOptions(ctx, true)
|
||||
opts = append(opts, crane.WithAuthFromKeychain(keychain))
|
||||
artifactURL, err := r.getArtifactURL(obj, opts)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
if tt.shouldSign {
|
||||
ko := coptions.KeyOpts{
|
||||
KeyRef: path.Join(tmpDir, "cosign.key"),
|
||||
PassFunc: pf,
|
||||
}
|
||||
|
||||
ro := &coptions.RootOptions{
|
||||
Timeout: timeout,
|
||||
}
|
||||
err = sign.SignCmd(ro, ko, coptions.RegistryOptions{Keychain: keychain},
|
||||
nil, []string{artifactURL}, "",
|
||||
"", true, "",
|
||||
"", "", false,
|
||||
false, "", false)
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
|
||||
assertConditions := tt.assertConditions
|
||||
for k := range assertConditions {
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<digest>", tt.digest)
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<url>", artifactURL)
|
||||
assertConditions[k].Message = strings.ReplaceAll(assertConditions[k].Message, "<provider>", "cosign")
|
||||
}
|
||||
|
||||
if tt.beforeFunc != nil {
|
||||
tt.beforeFunc(obj)
|
||||
}
|
||||
|
||||
artifact := &sourcev1.Artifact{}
|
||||
got, err := r.reconcileSource(ctx, obj, artifact, tmpDir)
|
||||
if tt.wantErr {
|
||||
tt.wantErrMsg = strings.ReplaceAll(tt.wantErrMsg, "<url>", artifactURL)
|
||||
g.Expect(err).ToNot(BeNil())
|
||||
g.Expect(err.Error()).To(ContainSubstring(tt.wantErrMsg))
|
||||
} else {
|
||||
g.Expect(err).ToNot(HaveOccurred())
|
||||
}
|
||||
g.Expect(got).To(Equal(tt.want))
|
||||
g.Expect(obj.Status.Conditions).To(conditions.MatchConditions(tt.assertConditions))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOCIRepository_reconcileArtifact(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
|
|
|
@ -1028,6 +1028,22 @@ The secret must be of type kubernetes.io/dockerconfigjson.</p>
|
|||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>verify</code><br>
|
||||
<em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryVerification">
|
||||
OCIRepositoryVerification
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Verify contains the secret name containing the trusted public keys
|
||||
used to verify the signature and specifies which provider to use to check
|
||||
whether OCI image is authentic.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>serviceAccountName</code><br>
|
||||
<em>
|
||||
string
|
||||
|
@ -2772,6 +2788,22 @@ The secret must be of type kubernetes.io/dockerconfigjson.</p>
|
|||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>verify</code><br>
|
||||
<em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositoryVerification">
|
||||
OCIRepositoryVerification
|
||||
</a>
|
||||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>Verify contains the secret name containing the trusted public keys
|
||||
used to verify the signature and specifies which provider to use to check
|
||||
whether OCI image is authentic.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<code>serviceAccountName</code><br>
|
||||
<em>
|
||||
string
|
||||
|
@ -2967,6 +2999,10 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus
|
|||
</div>
|
||||
<h3 id="source.toolkit.fluxcd.io/v1beta2.OCIRepositoryVerification">OCIRepositoryVerification
|
||||
</h3>
|
||||
<p>
|
||||
(<em>Appears on:</em>
|
||||
<a href="#source.toolkit.fluxcd.io/v1beta2.OCIRepositorySpec">OCIRepositorySpec</a>)
|
||||
</p>
|
||||
<p>OCIRepositoryVerification verifies the authenticity of an OCI Artifact</p>
|
||||
<div class="md-typeset__scrollwrap">
|
||||
<div class="md-typeset__table">
|
||||
|
@ -2999,6 +3035,7 @@ github.com/fluxcd/pkg/apis/meta.LocalObjectReference
|
|||
</em>
|
||||
</td>
|
||||
<td>
|
||||
<em>(Optional)</em>
|
||||
<p>SecretRef specifies the Kubernetes Secret containing the
|
||||
trusted public keys.</p>
|
||||
</td>
|
||||
|
|
|
@ -409,6 +409,81 @@ list](#default-exclusions), and may overrule the [`.sourceignore` file
|
|||
exclusions](#sourceignore-file). See [excluding files](#excluding-files)
|
||||
for more information.
|
||||
|
||||
### Verification
|
||||
|
||||
`.spec.verify` is an optional field to enable the verification of [Cosign](https://github.com/sigstore/cosign)
|
||||
signatures. The field offers two subfields:
|
||||
|
||||
- `.provider`, to specify the verification provider. Only supports `cosign` at present.
|
||||
- `.secretRef.name`, to specify a reference to a Secret in the same namespace as
|
||||
the OCIRepository, containing the Cosign public keys of trusted authors.
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: <repository-name>
|
||||
spec:
|
||||
verify:
|
||||
provider: cosign
|
||||
secretRef:
|
||||
name: cosign-public-keys
|
||||
```
|
||||
|
||||
When the verification succeeds, the controller adds a Condition with the
|
||||
following attributes to the OCIRepository's `.status.conditions`:
|
||||
|
||||
- `type: SourceVerified`
|
||||
- `status: "True"`
|
||||
- `reason: Succeeded`
|
||||
|
||||
#### Public keys verification
|
||||
|
||||
To verify the authenticity of an OCI artifact, create a Kubernetes secret
|
||||
with the Cosign public keys:
|
||||
|
||||
```yaml
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: cosign-public-keys
|
||||
type: Opaque
|
||||
data:
|
||||
key1.pub: <BASE64>
|
||||
key2.pub: <BASE64>
|
||||
```
|
||||
|
||||
Note that the keys must have the `.pub` extension for Flux to make use of them.
|
||||
|
||||
#### Keyless verification
|
||||
|
||||
For publicly available OCI artifacts, which are signed using the
|
||||
[Cosign Keyless](https://github.com/sigstore/cosign/blob/main/KEYLESS.md) procedure,
|
||||
you can enable the verification by omitting the `.verify.secretRef` field.
|
||||
|
||||
Example of verifying artifacts signed by the
|
||||
[Cosign GitHub Action](https://github.com/sigstore/cosign-installer) with GitHub OIDC Token:
|
||||
|
||||
```yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1beta2
|
||||
kind: OCIRepository
|
||||
metadata:
|
||||
name: podinfo
|
||||
spec:
|
||||
interval: 5m
|
||||
url: oci://ghcr.io/stefanprodan/manifests/podinfo
|
||||
verify:
|
||||
provider: cosign
|
||||
```
|
||||
|
||||
The controller verifies the signatures using the Fulcio root CA and the Rekor
|
||||
instance hosted at [rekor.sigstore.dev](https://rekor.sigstore.dev/).
|
||||
|
||||
Note that keyless verification is an **experimental feature**, using
|
||||
custom root CAs or self-hosted Rekor instances are not currently supported.
|
||||
|
||||
### Suspend
|
||||
|
||||
`.spec.suspend` is an optional field to suspend the reconciliation of a
|
||||
|
@ -764,6 +839,14 @@ and is only present on the OCIRepository while the status value is `"True"`.
|
|||
There may be more arbitrary values for the `reason` field to provide accurate
|
||||
reason for a condition.
|
||||
|
||||
In addition to the above Condition types, when the signature
|
||||
[verification](#verification) fails. A condition with
|
||||
the following attributes is added to the GitRepository's `.status.conditions`:
|
||||
|
||||
- `type: SourceVerified`
|
||||
- `status: "False"`
|
||||
- `reason: VerificationError`
|
||||
|
||||
While the OCIRepository has one or more of these Conditions, the controller
|
||||
will continue to attempt to produce an Artifact for the resource with an
|
||||
exponential backoff, until it succeeds and the OCIRepository is marked as
|
||||
|
|
200
go.mod
200
go.mod
|
@ -58,11 +58,14 @@ require (
|
|||
github.com/otiai10/copy v1.7.0
|
||||
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/sigstore/cosign v1.12.1
|
||||
github.com/sigstore/sigstore v1.4.1
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
golang.org/x/crypto v0.0.0-20220824171710-5757bc0c5503
|
||||
golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c
|
||||
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90
|
||||
golang.org/x/net v0.0.0-20220909164309-bea034e7d591
|
||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
||||
google.golang.org/api v0.94.0
|
||||
google.golang.org/api v0.96.0
|
||||
gotest.tools v2.2.0+incompatible
|
||||
helm.sh/helm/v3 v3.9.4
|
||||
k8s.io/api v0.25.0
|
||||
|
@ -78,14 +81,16 @@ require (
|
|||
replace github.com/emicklei/go-restful => github.com/emicklei/go-restful v2.16.0+incompatible
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.102.1 // indirect
|
||||
bitbucket.org/creachadair/shell v0.0.7 // indirect
|
||||
cloud.google.com/go v0.103.0 // indirect
|
||||
cloud.google.com/go/compute v1.7.0 // indirect
|
||||
cloud.google.com/go/iam v0.3.0 // indirect
|
||||
github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/alibabacloudsdkgo/helper v0.2.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.27 // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.28 // indirect
|
||||
github.com/Azure/go-autorest/autorest/adal v0.9.20 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
|
||||
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
|
||||
|
@ -100,33 +105,60 @@ require (
|
|||
github.com/Masterminds/squirrel v1.5.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d // indirect
|
||||
github.com/ThalesIgnite/crypto11 v1.2.5 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.84 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.15.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.15 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.4 // indirect
|
||||
github.com/alibabacloud-go/cr-20160607 v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/cr-20181201 v1.0.10 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi v0.1.18 // indirect
|
||||
github.com/alibabacloud-go/debug v0.0.0-20190504072949-9472017b5c68 // indirect
|
||||
github.com/alibabacloud-go/endpoint-util v1.1.1 // indirect
|
||||
github.com/alibabacloud-go/openapi-util v0.0.11 // indirect
|
||||
github.com/alibabacloud-go/tea v1.1.18 // indirect
|
||||
github.com/alibabacloud-go/tea-utils v1.4.4 // indirect
|
||||
github.com/alibabacloud-go/tea-xml v1.1.2 // indirect
|
||||
github.com/aliyun/credentials-go v1.2.3 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.96 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.16.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.17.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.12.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecr v1.17.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.13.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.9 // indirect
|
||||
github.com/aws/smithy-go v1.12.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.11.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.13.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.16.17 // indirect
|
||||
github.com/aws/smithy-go v1.13.2 // indirect
|
||||
github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20220706184558-ce46abcd012b // indirect
|
||||
github.com/benbjohnson/clock v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bgentry/speakeasy v0.1.0 // indirect
|
||||
github.com/blang/semver v3.5.1+incompatible // indirect
|
||||
github.com/bshuster-repo/logrus-logstash-hook v1.0.2 // indirect
|
||||
github.com/bugsnag/bugsnag-go v2.1.2+incompatible // indirect
|
||||
github.com/bugsnag/panicwrap v1.3.4 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.1.3 // indirect
|
||||
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 // indirect
|
||||
github.com/chrismellard/docker-credential-acr-env v0.0.0-20220327082430-c57b701bfc08 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.5.6 // indirect
|
||||
github.com/cloudflare/circl v1.1.0 // indirect
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
|
||||
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be // indirect
|
||||
github.com/containerd/containerd v1.6.6 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.12.0 // indirect
|
||||
github.com/coreos/go-oidc/v3 v3.4.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cyberphone/json-canonicalization v0.0.0-20210823021906-dc406ceaf94b // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dimchansky/utfbom v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
|
@ -139,47 +171,80 @@ require (
|
|||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
|
||||
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
|
||||
github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||
github.com/fluxcd/pkg/apis/acl v0.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/fullstorydev/grpcurl v1.8.7 // indirect
|
||||
github.com/go-chi/chi v4.1.2+incompatible // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.0.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-logr/zapr v1.2.3 // indirect
|
||||
github.com/go-openapi/analysis v0.21.4 // indirect
|
||||
github.com/go-openapi/errors v0.20.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.0 // indirect
|
||||
github.com/go-openapi/swag v0.21.1 // indirect
|
||||
github.com/go-openapi/loads v0.21.2 // indirect
|
||||
github.com/go-openapi/runtime v0.24.1 // indirect
|
||||
github.com/go-openapi/spec v0.20.7 // indirect
|
||||
github.com/go-openapi/strfmt v0.21.3 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/go-openapi/validate v0.22.0 // indirect
|
||||
github.com/go-piv/piv-go v1.10.0 // indirect
|
||||
github.com/go-playground/locales v0.14.0 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.11.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/uuid v4.2.0+incompatible // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gomodule/redigo v1.8.2 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/certificate-transparency-go v1.1.3 // indirect
|
||||
github.com/google/gnostic v0.6.9 // indirect
|
||||
github.com/google/go-cmp v0.5.8 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/go-containerregistry/pkg/authn/kubernetes v0.0.0-20220719135131-f79ec2192282 // indirect
|
||||
github.com/google/go-github/v45 v45.2.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/google/trillian v1.5.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.4.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
github.com/gorilla/mux v1.8.0 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/gosuri/uitable v0.0.4 // indirect
|
||||
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.2 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.3.4-0.20220709202702-fa494aaa0add // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jedisct1/go-minisign v0.0.0-20211028175153-1c139d1cc84b // indirect
|
||||
github.com/jhump/protoreflect v1.12.0 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/jmoiron/sqlx v1.3.5 // indirect
|
||||
github.com/jonboulle/clockwork v0.3.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect
|
||||
|
@ -189,18 +254,23 @@ require (
|
|||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/letsencrypt/boulder v0.0.0-20220723181115-27de4befb95e // indirect
|
||||
github.com/lib/pq v1.10.6 // indirect
|
||||
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/spdystream v0.2.0 // indirect
|
||||
|
@ -209,9 +279,15 @@ require (
|
|||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/mozillazg/docker-credential-acr-helper v0.3.0 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20220729202839-6ad7100eb087 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
|
@ -219,42 +295,95 @@ require (
|
|||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rs/xid v1.4.0 // indirect
|
||||
github.com/rubenv/sql-migrate v1.1.2 // indirect
|
||||
github.com/russross/blackfriday v1.6.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sassoftware/relic v0.0.0-20210427151427-dfb082b79b74 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect
|
||||
github.com/segmentio/ksuid v1.0.4 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/shopspring/decimal v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/sigstore/fulcio v0.5.3 // indirect
|
||||
github.com/sigstore/rekor v0.12.1-0.20220915152154-4bb6f441c1b2 // indirect
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/cobra v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/viper v1.13.0 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.1.1 // indirect
|
||||
github.com/stretchr/testify v1.8.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect
|
||||
github.com/tent/canonical-json-go v0.0.0-20130607151641-96e4ba3a7613 // indirect
|
||||
github.com/thales-e-security/pool v0.0.2 // indirect
|
||||
github.com/theupdateframework/go-tuf v0.5.1-0.20220920170306-f237d7ca5b42 // indirect
|
||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect
|
||||
github.com/tjfoc/gmsm v1.3.2 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||
github.com/transparency-dev/merkle v0.0.1 // indirect
|
||||
github.com/urfave/cli v1.22.7 // indirect
|
||||
github.com/vbatts/tar-split v0.11.2 // indirect
|
||||
github.com/xanzy/go-gitlab v0.73.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
||||
github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect
|
||||
github.com/yvasiyarov/go-metrics v0.0.0-20150112132944-c25f46c4b940 // indirect
|
||||
github.com/yvasiyarov/gorelic v0.0.7 // indirect
|
||||
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20160601141957-9c099fbc30e9 // indirect
|
||||
github.com/zeebo/errs v1.2.2 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.306.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/etcdctl/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/etcdutl/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/pkg/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/server/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/tests/v3 v3.6.0-alpha.0 // indirect
|
||||
go.etcd.io/etcd/v3 v3.6.0-alpha.0 // indirect
|
||||
go.mongodb.org/mongo-driver v1.10.1 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.28.0 // indirect
|
||||
go.opentelemetry.io/otel v1.7.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.7.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.7.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.7.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.7.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.7.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.16.0 // indirect
|
||||
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20220823124025-807a23277127 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 // indirect
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect
|
||||
golang.org/x/sys v0.0.0-20220907062415-87db552b00fd // indirect
|
||||
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
|
||||
golang.org/x/text v0.3.8-0.20211004125949-5bd84dd9b33b // indirect
|
||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220720214146-176da50484ac // indirect
|
||||
google.golang.org/grpc v1.48.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220805133916-01dd62135a58 // indirect
|
||||
google.golang.org/grpc v1.49.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.6 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
@ -269,5 +398,6 @@ require (
|
|||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.11.4 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect
|
||||
sigs.k8s.io/release-utils v0.7.3 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
)
|
||||
|
|
|
@ -165,3 +165,12 @@ echo "Run HelmChart from OCI registry tests"
|
|||
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/helmchart-from-oci/source.yaml"
|
||||
kubectl -n source-system wait helmrepository/podinfo --for=condition=ready --timeout=1m
|
||||
kubectl -n source-system wait helmchart/podinfo --for=condition=ready --timeout=1m
|
||||
|
||||
echo "Run OCIRepository verify tests"
|
||||
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/ocirepository/signed-with-key.yaml"
|
||||
kubectl -n source-system apply -f "${ROOT_DIR}/config/testdata/ocirepository/signed-with-keyless.yaml"
|
||||
curl -sSLo cosign.pub https://raw.githubusercontent.com/stefanprodan/podinfo/master/.cosign/cosign.pub
|
||||
kubectl -n source-system create secret generic cosign-key --from-file=cosign.pub --dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
kubectl -n source-system wait ocirepository/podinfo-deploy-signed-with-key --for=condition=ready --timeout=1m
|
||||
kubectl -n source-system wait ocirepository/podinfo-deploy-signed-with-keyless --for=condition=ready --timeout=1m
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package util
|
||||
package oci
|
||||
|
||||
import "github.com/google/go-containerregistry/pkg/authn"
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
Copyright 2022 The Flux 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 oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"fmt"
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/fulcio"
|
||||
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
|
||||
ociremote "github.com/sigstore/cosign/pkg/oci/remote"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
coptions "github.com/sigstore/cosign/cmd/cosign/cli/options"
|
||||
"github.com/sigstore/cosign/pkg/cosign"
|
||||
"github.com/sigstore/cosign/pkg/oci"
|
||||
"github.com/sigstore/sigstore/pkg/cryptoutils"
|
||||
"github.com/sigstore/sigstore/pkg/signature"
|
||||
)
|
||||
|
||||
// options is a struct that holds options for verifier.
|
||||
type options struct {
|
||||
PublicKey []byte
|
||||
Keychain authn.Keychain
|
||||
}
|
||||
|
||||
// Options is a function that configures the options applied to a Verifier.
|
||||
type Options func(opts *options)
|
||||
|
||||
// WithPublicKey sets the public key.
|
||||
func WithPublicKey(publicKey []byte) Options {
|
||||
return func(opts *options) {
|
||||
opts.PublicKey = publicKey
|
||||
}
|
||||
}
|
||||
|
||||
func WithAuthnKeychain(keychain authn.Keychain) Options {
|
||||
return func(opts *options) {
|
||||
opts.Keychain = keychain
|
||||
}
|
||||
}
|
||||
|
||||
// Verifier is a struct which is responsible for executing verification logic.
|
||||
type Verifier struct {
|
||||
opts *cosign.CheckOpts
|
||||
}
|
||||
|
||||
// NewVerifier initializes a new Verifier.
|
||||
func NewVerifier(ctx context.Context, opts ...Options) (*Verifier, error) {
|
||||
o := options{}
|
||||
for _, opt := range opts {
|
||||
opt(&o)
|
||||
}
|
||||
|
||||
checkOpts := &cosign.CheckOpts{}
|
||||
|
||||
ro := coptions.RegistryOptions{}
|
||||
co, err := ro.ClientOpts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if o.Keychain != nil {
|
||||
co = append(co, ociremote.WithRemoteOptions(remote.WithAuthFromKeychain(o.Keychain)))
|
||||
}
|
||||
|
||||
checkOpts.RegistryClientOpts = co
|
||||
|
||||
// If a public key is provided, it will use it to verify the signature.
|
||||
// If there is no public key provided, it will try keyless verification.
|
||||
// https://github.com/sigstore/cosign/blob/main/KEYLESS.md.
|
||||
if len(o.PublicKey) > 0 {
|
||||
pubKeyRaw, err := cryptoutils.UnmarshalPEMToPublicKey(o.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
checkOpts.SigVerifier, err = signature.LoadVerifier(pubKeyRaw, crypto.SHA256)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
rcerts, err := fulcio.GetRoots()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get Fulcio root certs: %w", err)
|
||||
}
|
||||
checkOpts.RootCerts = rcerts
|
||||
|
||||
icerts, err := fulcio.GetIntermediates()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get Fulcio intermediate certs: %w", err)
|
||||
}
|
||||
checkOpts.IntermediateCerts = icerts
|
||||
|
||||
rc, err := rekor.NewClient(coptions.DefaultRekorURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create Rekor client: %w", err)
|
||||
}
|
||||
checkOpts.RekorClient = rc
|
||||
}
|
||||
|
||||
return &Verifier{
|
||||
opts: checkOpts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyImageSignatures verify the authenticity of the given ref OCI image.
|
||||
func (v *Verifier) VerifyImageSignatures(ctx context.Context, ref name.Reference) ([]oci.Signature, bool, error) {
|
||||
return cosign.VerifyImageSignatures(ctx, ref, v.opts)
|
||||
}
|
Loading…
Reference in New Issue