diff --git a/pkg/cli/admin/release/extract.go b/pkg/cli/admin/release/extract.go index 903d83ee17..4d6aef5316 100644 --- a/pkg/cli/admin/release/extract.go +++ b/pkg/cli/admin/release/extract.go @@ -9,8 +9,6 @@ import ( "io/ioutil" "os" "path" - "path/filepath" - "strings" "sync" "time" @@ -290,23 +288,6 @@ func (o *ExtractOptions) Run() error { manifestErrs = append(manifestErrs, errors.Wrapf(err, "error parsing %s", hdr.Name)) return true, nil } - for i := range ms { - ms[i].OriginalFilename = filepath.Base(hdr.Name) - src := fmt.Sprintf("the config map %s/%s", ms[i].Obj.GetNamespace(), ms[i].Obj.GetName()) - data, _, err := unstructured.NestedStringMap(ms[i].Obj.Object, "data") - if err != nil { - manifestErrs = append(manifestErrs, errors.Wrapf(err, "%s is not valid", src)) - continue - } - for k, v := range data { - switch { - case strings.HasPrefix(k, "verifier-public-key-"): - klog.V(2).Infof("Found in %s:\n%s %s", hdr.Name, k, v) - case strings.HasPrefix(k, "store-"): - klog.V(2).Infof("Found in %s:\n%s\n%s", hdr.Name, k, v) - } - } - } o.Manifests = append(o.Manifests, ms...) } } @@ -320,19 +301,10 @@ func (o *ExtractOptions) Run() error { return fmt.Errorf("image did not contain %s", o.File) } - // Only output manifest errors if manifests were being extracted and we didn't find the expected signature - // manifests. We don't care about errors in other manifests and they will only confuse/alarm the user. + // Only output manifest errors if manifests were being extracted. // Do not return an error so current operation, e.g. mirroring, continues. - if len(manifestErrs) > 0 { - if o.ExtractManifests && len(o.Manifests) == 0 { - fmt.Fprintf(o.ErrOut, "Errors: %s\n", errorList(manifestErrs)) - } - } - - // Output an error if manifests were being extracted and we didn't find the expected signature - // manifests. Do not return an error so current operation, e.g. mirroring, continues. - if o.ExtractManifests && len(o.Manifests) == 0 { - fmt.Fprintf(o.ErrOut, "No manifests found\n") + if o.ExtractManifests && len(manifestErrs) > 0 { + fmt.Fprintf(o.ErrOut, "Errors: %s\n", errorList(manifestErrs)) } return nil diff --git a/pkg/cli/admin/release/mirror.go b/pkg/cli/admin/release/mirror.go index b043458686..5706dca35b 100644 --- a/pkg/cli/admin/release/mirror.go +++ b/pkg/cli/admin/release/mirror.go @@ -39,11 +39,14 @@ import ( "github.com/openshift/library-go/pkg/image/dockerv1client" imagereference "github.com/openshift/library-go/pkg/image/reference" "github.com/openshift/library-go/pkg/manifest" + "github.com/openshift/library-go/pkg/verify" + "github.com/openshift/library-go/pkg/verify/store/configmap" + "github.com/openshift/library-go/pkg/verify/store/sigstore" + "github.com/openshift/library-go/pkg/verify/util" "github.com/openshift/oc/pkg/cli/image/extract" "github.com/openshift/oc/pkg/cli/image/imagesource" imagemanifest "github.com/openshift/oc/pkg/cli/image/manifest" "github.com/openshift/oc/pkg/cli/image/mirror" - "github.com/openshift/oc/pkg/helpers/release" ) // configFilesBaseDir is created under '--to-dir', when specified, to contain release image @@ -236,7 +239,7 @@ func (o *MirrorOptions) Complete(cmd *cobra.Command, f kcmdutil.Factory, args [] if err != nil { return nil, err } - client := coreClient.ConfigMaps(release.NamespaceLabelConfigMap) + client := coreClient.ConfigMaps(configmap.NamespaceLabelConfigMap) return client, nil } o.PrintImageContentInstructions = true @@ -321,7 +324,7 @@ func (o *MirrorOptions) handleSignatures(context context.Context, signaturesByDi } } for digest, signatures := range signaturesByDigest { - cmData, err := release.GetSignaturesAsConfigmap(digest, signatures) + cmData, err := verify.GetSignaturesAsConfigmap(digest, signatures) if err != nil { return fmt.Errorf("converting signatures to a configmap: %v", err) } @@ -360,7 +363,7 @@ func (o *MirrorOptions) handleSignatures(context context.Context, signaturesByDi if o.DryRun { fmt.Fprintf(o.Out, "info: Write configmap signature file %s\n", fullName) } else { - cmDataBytes, err := yaml.Marshal(cmData) + cmDataBytes, err := util.ConfigMapAsBytes(cmData) if err != nil { return fmt.Errorf("marshaling configmap YAML: %v", err) } @@ -497,11 +500,11 @@ func (o *MirrorOptions) Run() error { sourceFn := func(ref imagesource.TypedImageReference) imagesource.TypedImageReference { return ref } - // Wraps operator's HTTPClient method to allow image verifier to create http client with up-to-date config - clientBuilder := &verifyClientBuilder{builder: o.HTTPClient} + + httpClientConstructor := sigstore.NewCachedHTTPClientConstructor(o.HTTPClient, nil) // Attempt to load a verifier as defined by the release being mirrored - imageVerifier, err := release.LoadConfigMapVerifierDataFromUpdate(manifests, clientBuilder, nil) + imageVerifier, err := verify.NewFromManifests(manifests, httpClientConstructor.HTTPClient) if err != nil { return fmt.Errorf("Unable to load configmap verifier: %v", err) } @@ -509,7 +512,6 @@ func (o *MirrorOptions) Run() error { klog.V(4).Infof("Verifying release authenticity: %v", imageVerifier) } else { fmt.Fprintf(o.ErrOut, "warning: No release authenticity verification is configured, all releases are considered unverified\n") - imageVerifier = release.Reject } // verify the provided payload ctx, cancelFn := context.WithCancel(context.Background()) diff --git a/pkg/helpers/release/configmap_test.go b/pkg/helpers/release/configmap_test.go deleted file mode 100644 index 3fe99d871b..0000000000 --- a/pkg/helpers/release/configmap_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package release - -import ( - "io/ioutil" - "path/filepath" - "testing" - - "golang.org/x/crypto/openpgp" -) - -type VerifierAccessor interface { - Verifiers() map[string]openpgp.EntityList -} - -func Test_loadReleaseVerifierFromConfigMap(t *testing.T) { - redhatData, err := ioutil.ReadFile(filepath.Join("testdata", "keyrings", "redhat.txt")) - if err != nil { - t.Fatal(err) - } - - tests := []struct { - name string - data map[string]string - want bool - wantErr bool - wantVerifiers int - }{ - { - name: "requires data", - data: nil, - wantErr: true, - }, - { - name: "requires stores", - data: map[string]string{ - "verifier-public-key-redhat": string(redhatData), - }, - wantErr: true, - }, - { - name: "requires verifiers", - data: map[string]string{ - "store-local": "file://../testdata/signatures", - }, - wantErr: true, - }, - { - name: "loads valid configuration", - data: map[string]string{ - "verifier-public-key-redhat": string(redhatData), - "store-local": "file://../testdata/signatures", - }, - want: true, - wantVerifiers: 1, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := NewFromConfigMapData("from_test", tt.data, DefaultClient) - if (err != nil) != tt.wantErr { - t.Fatalf("loadReleaseVerifierFromPayload() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != nil != tt.want { - t.Fatal(got) - } - if err != nil { - return - } - if got == nil { - return - } - if len(got.Verifiers()) != tt.wantVerifiers { - t.Fatalf("unexpected release verifier: %#v", got) - } - }) - } -} diff --git a/pkg/helpers/release/testdata/keyrings/combined.txt b/pkg/helpers/release/testdata/keyrings/combined.txt deleted file mode 100644 index cfe4a4734e..0000000000 --- a/pkg/helpers/release/testdata/keyrings/combined.txt +++ /dev/null @@ -1,56 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v2.0.22 (GNU/Linux) -Comment: Use "gpg --dearmor" for unpacking - -mQINBErgSTsBEACh2A4b0O9t+vzC9VrVtL1AKvUWi9OPCjkvR7Xd8DtJxeeMZ5eF -0HtzIG58qDRybwUe89FZprB1ffuUKzdE+HcL3FbNWSSOXVjZIersdXyH3NvnLLLF -0DNRB2ix3bXG9Rh/RXpFsNxDp2CEMdUvbYCzE79K1EnUTVh1L0Of023FtPSZXX0c -u7Pb5DI5lX5YeoXO6RoodrIGYJsVBQWnrWw4xNTconUfNPk0EGZtEnzvH2zyPoJh -XGF+Ncu9XwbalnYde10OCvSWAZ5zTCpoLMTvQjWpbCdWXJzCm6G+/hx9upke546H -5IjtYm4dTIVTnc3wvDiODgBKRzOl9rEOCIgOuGtDxRxcQkjrC+xvg5Vkqn7vBUyW -9pHedOU+PoF3DGOM+dqv+eNKBvh9YF9ugFAQBkcG7viZgvGEMGGUpzNgN7XnS1gj -/DPo9mZESOYnKceve2tIC87p2hqjrxOHuI7fkZYeNIcAoa83rBltFXaBDYhWAKS1 -PcXS1/7JzP0ky7d0L6Xbu/If5kqWQpKwUInXtySRkuraVfuK3Bpa+X1XecWi24JY -HVtlNX025xx1ewVzGNCTlWn1skQN2OOoQTV4C8/qFpTW6DTWYurd4+fE0OJFJZQF -buhfXYwmRlVOgN5i77NTIJZJQfYFj38c/Iv5vZBPokO6mffrOTv3MHWVgQARAQAB -tDNSZWQgSGF0LCBJbmMuIChyZWxlYXNlIGtleSAyKSA8c2VjdXJpdHlAcmVkaGF0 -LmNvbT6JAjYEEwECACAFAkrgSTsCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAK -CRAZni+R/UMdUWzpD/9s5SFR/ZF3yjY5VLUFLMXIKUztNN3oc45fyLdTI3+UClKC -2tEruzYjqNHhqAEXa2sN1fMrsuKec61Ll2NfvJjkLKDvgVIh7kM7aslNYVOP6BTf -C/JJ7/ufz3UZmyViH/WDl+AYdgk3JqCIO5w5ryrC9IyBzYv2m0HqYbWfphY3uHw5 -un3ndLJcu8+BGP5F+ONQEGl+DRH58Il9Jp3HwbRa7dvkPgEhfFR+1hI+Btta2C7E -0/2NKzCxZw7Lx3PBRcU92YKyaEihfy/aQKZCAuyfKiMvsmzs+4poIX7I9NQCJpyE -IGfINoZ7VxqHwRn/d5mw2MZTJjbzSf+Um9YJyA0iEEyD6qjriWQRbuxpQXmlAJbh -8okZ4gbVFv1F8MzK+4R8VvWJ0XxgtikSo72fHjwha7MAjqFnOq6eo6fEC/75g3NL -Ght5VdpGuHk0vbdENHMC8wS99e5qXGNDued3hlTavDMlEAHl34q2H9nakTGRF5Ki -JUfNh3DVRGhg8cMIti21njiRh7gyFI2OccATY7bBSr79JhuNwelHuxLrCFpY7V25 -OFktl15jZJaMxuQBqYdBgSay2G0U6D1+7VsWufpzd/Abx1/c3oi9ZaJvW22kAggq -dzdA27UUYjWvx42w9menJwh/0jeQcTecIUd0d0rFcw/c1pvgMMl/Q73yzKgKY5kB -DQRcvUOrAQgA9TL3MF/X9VbvzP3YfkiWG7gD+Lq7WWe2KGTpc6OpcP8Qxfc9BHn/ -AVwLTu2DErX28Z+Uam95D5wNtAkV62luD6gOZgd+7mwxk4cW/HGrQk3lqXf+aJq2 -4yzKygqYNDg304DWWI/YEQ8g0yj45VtsY1/Qpo/5Zphj2AxuKnazaXonJjI6WF8m -A1cRU0RTHYn8U4x0EU+UfT3avFgxS63d2WVqOHzeUW/gclofDLrB4/hch8QOCXw/ -xulR8p9fU+8U/4OdyXz6Gyi3WqFynUmqKwmClrshmhsi0rQJ9TF4HIbMHAWXFPdh -HoHKGWPCt3GIUW8O60FFJMd6dMr4ktQ6zwARAQABtAxvcGVuc2hpZnQtY2mJAVQE -EwEIAD4WIQTQR2GxFiA7DAhZthYot24FuSOIjgUCXL1DqwIbAwUJA8JnAAULCQgH -AgYVCgkICwIEFgIDAQIeAQIXgAAKCRAot24FuSOIjqsFB/sE0V12ZAej3ZLENrRf -8d7092AKdRb5vmgbdC9/p1MiOFuMFpgr0PZmKpFzA0sfK4EsDLcMCXu8SQZANXyv -AqD3gqh3P6JqC1EuvwY3G7F8kv57OneWb9HylR7pmdt1dqlScD6ZjXaZHXwYcBxS -ptByz2gsijN/Hzj4a1MBFvDnHlXR3wZ5JAMmFwPfvahhd4BwtzFzC5Gh+qQ2GDX8 -dj+PqmJzJ0zEjjryrCVmO7fE579UKLWoP7lvMlpSAUp74NQUO7tWSbNRCksIcEGy -K2I/nkvnvHXTe6khyU6DMx7LU40mEE4QNYglVOkvih2ixXsp39Xej6pMnh/xg25P -hPobuQENBFy9Q6sBCADYVORXM8KrhAHI4QPpH/p4tFJfUNmqvqwC99XYPrBjGWsH -A7uWqHMKJV3gSJFZdt+RhXyUnWEcyG5OZUqlSvlreWI+MjiDvkBAJOSOdXczguYt -wD06jjNFD0NevLm3KE+S2P2liyap1QI4GP0p9r1wMLGL5LiWTKXjj6DYKHAFsMBs -V5DxMv/zgN68MsujxEdlO8S1i+Ujh/KMY57JxwPfJxeIrjkKm8D08H8lje8a+xwG -OiomsB5g9E98sLMEdWxGdQmJ/CsaTTLh3+7W2jDzjb2sFRKjNcXPfuLQdyJnTFAf -XiIsCLKauvJnRON3slHjPX9n6DUeuyo+he4bwcA7ABEBAAGJATYEGAEIACAWIQTQ -R2GxFiA7DAhZthYot24FuSOIjgUCXL1DqwIbDAAKCRAot24FuSOIjiwgCADTXQcB -RSaU2hGYTrwxLHzphwxRPsRtnwavkjudwODP+MXyegVZ6UbwID7xLvxA/CzCAW7m -jFKV4wMFCyDpzRAbGHpvptyCnK2QCIX1wIyPBKs6a43IlIlRMdPl0eniG7BZtoJu -tbx3274ikskIN4aShvP4NrBYEPQjYuQxYISGHrKfuzcAgvlDlRgbvdDuEiKviDLN -p9zk0dhiBM9C4BLwv90e6ZATYyzU3HBMQTajkoSct158J7b2H5cVcBAVbMhGyi7y -1NsbZSBPyHRrLCkEfFBRbIBZhol97dU3GoRZy5a+hLfrweCdNl6/rr2fNb/2atTh -8+4iI63dvDLJWLtu -=8lmj ------END PGP PUBLIC KEY BLOCK----- diff --git a/pkg/helpers/release/testdata/keyrings/redhat.txt b/pkg/helpers/release/testdata/keyrings/redhat.txt deleted file mode 100644 index 0009a3e88b..0000000000 --- a/pkg/helpers/release/testdata/keyrings/redhat.txt +++ /dev/null @@ -1,34 +0,0 @@ -pub 4096R/FD431D51 2009-10-22 - Key fingerprint = 567E 347A D004 4ADE 55BA 8A5F 199E 2F91 FD43 1D51 -uid Red Hat, Inc. (release key 2) - ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.5 (GNU/Linux) - -mQINBErgSTsBEACh2A4b0O9t+vzC9VrVtL1AKvUWi9OPCjkvR7Xd8DtJxeeMZ5eF -0HtzIG58qDRybwUe89FZprB1ffuUKzdE+HcL3FbNWSSOXVjZIersdXyH3NvnLLLF -0DNRB2ix3bXG9Rh/RXpFsNxDp2CEMdUvbYCzE79K1EnUTVh1L0Of023FtPSZXX0c -u7Pb5DI5lX5YeoXO6RoodrIGYJsVBQWnrWw4xNTconUfNPk0EGZtEnzvH2zyPoJh -XGF+Ncu9XwbalnYde10OCvSWAZ5zTCpoLMTvQjWpbCdWXJzCm6G+/hx9upke546H -5IjtYm4dTIVTnc3wvDiODgBKRzOl9rEOCIgOuGtDxRxcQkjrC+xvg5Vkqn7vBUyW -9pHedOU+PoF3DGOM+dqv+eNKBvh9YF9ugFAQBkcG7viZgvGEMGGUpzNgN7XnS1gj -/DPo9mZESOYnKceve2tIC87p2hqjrxOHuI7fkZYeNIcAoa83rBltFXaBDYhWAKS1 -PcXS1/7JzP0ky7d0L6Xbu/If5kqWQpKwUInXtySRkuraVfuK3Bpa+X1XecWi24JY -HVtlNX025xx1ewVzGNCTlWn1skQN2OOoQTV4C8/qFpTW6DTWYurd4+fE0OJFJZQF -buhfXYwmRlVOgN5i77NTIJZJQfYFj38c/Iv5vZBPokO6mffrOTv3MHWVgQARAQAB -tDNSZWQgSGF0LCBJbmMuIChyZWxlYXNlIGtleSAyKSA8c2VjdXJpdHlAcmVkaGF0 -LmNvbT6JAjYEEwECACAFAkrgSTsCGwMGCwkIBwMCBBUCCAMEFgIDAQIeAQIXgAAK -CRAZni+R/UMdUWzpD/9s5SFR/ZF3yjY5VLUFLMXIKUztNN3oc45fyLdTI3+UClKC -2tEruzYjqNHhqAEXa2sN1fMrsuKec61Ll2NfvJjkLKDvgVIh7kM7aslNYVOP6BTf -C/JJ7/ufz3UZmyViH/WDl+AYdgk3JqCIO5w5ryrC9IyBzYv2m0HqYbWfphY3uHw5 -un3ndLJcu8+BGP5F+ONQEGl+DRH58Il9Jp3HwbRa7dvkPgEhfFR+1hI+Btta2C7E -0/2NKzCxZw7Lx3PBRcU92YKyaEihfy/aQKZCAuyfKiMvsmzs+4poIX7I9NQCJpyE -IGfINoZ7VxqHwRn/d5mw2MZTJjbzSf+Um9YJyA0iEEyD6qjriWQRbuxpQXmlAJbh -8okZ4gbVFv1F8MzK+4R8VvWJ0XxgtikSo72fHjwha7MAjqFnOq6eo6fEC/75g3NL -Ght5VdpGuHk0vbdENHMC8wS99e5qXGNDued3hlTavDMlEAHl34q2H9nakTGRF5Ki -JUfNh3DVRGhg8cMIti21njiRh7gyFI2OccATY7bBSr79JhuNwelHuxLrCFpY7V25 -OFktl15jZJaMxuQBqYdBgSay2G0U6D1+7VsWufpzd/Abx1/c3oi9ZaJvW22kAggq -dzdA27UUYjWvx42w9menJwh/0jeQcTecIUd0d0rFcw/c1pvgMMl/Q73yzKgKYw== -=zbHE ------END PGP PUBLIC KEY BLOCK----- - diff --git a/pkg/helpers/release/testdata/keyrings/simple.txt b/pkg/helpers/release/testdata/keyrings/simple.txt deleted file mode 100644 index 72775a7b57..0000000000 --- a/pkg/helpers/release/testdata/keyrings/simple.txt +++ /dev/null @@ -1,30 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -mQENBFy9Q6sBCAD1MvcwX9f1Vu/M/dh+SJYbuAP4urtZZ7YoZOlzo6lw/xDF9z0E -ef8BXAtO7YMStfbxn5Rqb3kPnA20CRXraW4PqA5mB37ubDGThxb8catCTeWpd/5o -mrbjLMrKCpg0ODfTgNZYj9gRDyDTKPjlW2xjX9Cmj/lmmGPYDG4qdrNpeicmMjpY -XyYDVxFTRFMdifxTjHQRT5R9Pdq8WDFLrd3ZZWo4fN5Rb+ByWh8MusHj+FyHxA4J -fD/G6VHyn19T7xT/g53JfPobKLdaoXKdSaorCYKWuyGaGyLStAn1MXgchswcBZcU -92EegcoZY8K3cYhRbw7rQUUkx3p0yviS1DrPABEBAAG0DG9wZW5zaGlmdC1jaYkB -VAQTAQgAPhYhBNBHYbEWIDsMCFm2Fii3bgW5I4iOBQJcvUOrAhsDBQkDwmcABQsJ -CAcCBhUKCQgLAgQWAgMBAh4BAheAAAoJECi3bgW5I4iOqwUH+wTRXXZkB6PdksQ2 -tF/x3vT3YAp1Fvm+aBt0L3+nUyI4W4wWmCvQ9mYqkXMDSx8rgSwMtwwJe7xJBkA1 -fK8CoPeCqHc/omoLUS6/BjcbsXyS/ns6d5Zv0fKVHumZ23V2qVJwPpmNdpkdfBhw -HFKm0HLPaCyKM38fOPhrUwEW8OceVdHfBnkkAyYXA9+9qGF3gHC3MXMLkaH6pDYY -Nfx2P4+qYnMnTMSOOvKsJWY7t8Tnv1Qotag/uW8yWlIBSnvg1BQ7u1ZJs1EKSwhw -QbIrYj+eS+e8ddN7qSHJToMzHstTjSYQThA1iCVU6S+KHaLFeynf1d6PqkyeH/GD -bk+E+hu5AQ0EXL1DqwEIANhU5FczwquEAcjhA+kf+ni0Ul9Q2aq+rAL31dg+sGMZ -awcDu5aocwolXeBIkVl235GFfJSdYRzIbk5lSqVK+Wt5Yj4yOIO+QEAk5I51dzOC -5i3APTqOM0UPQ168ubcoT5LY/aWLJqnVAjgY/Sn2vXAwsYvkuJZMpeOPoNgocAWw -wGxXkPEy//OA3rwyy6PER2U7xLWL5SOH8oxjnsnHA98nF4iuOQqbwPTwfyWN7xr7 -HAY6KiawHmD0T3ywswR1bEZ1CYn8KxpNMuHf7tbaMPONvawVEqM1xc9+4tB3ImdM -UB9eIiwIspq68mdE43eyUeM9f2foNR67Kj6F7hvBwDsAEQEAAYkBNgQYAQgAIBYh -BNBHYbEWIDsMCFm2Fii3bgW5I4iOBQJcvUOrAhsMAAoJECi3bgW5I4iOLCAIANNd -BwFFJpTaEZhOvDEsfOmHDFE+xG2fBq+SO53A4M/4xfJ6BVnpRvAgPvEu/ED8LMIB -buaMUpXjAwULIOnNEBsYem+m3IKcrZAIhfXAjI8EqzprjciUiVEx0+XR6eIbsFm2 -gm61vHfbviKSyQg3hpKG8/g2sFgQ9CNi5DFghIYesp+7NwCC+UOVGBu90O4SIq+I -Ms2n3OTR2GIEz0LgEvC/3R7pkBNjLNTccExBNqOShJy3XnwntvYflxVwEBVsyEbK -LvLU2xtlIE/IdGssKQR8UFFsgFmGiX3t1TcahFnLlr6Et+vB4J02Xr+uvZ81v/Zq -1OHz7iIjrd28MslYu24= -=xMCa ------END PGP PUBLIC KEY BLOCK----- diff --git a/pkg/helpers/release/testdata/signatures-2/.gitkeep b/pkg/helpers/release/testdata/signatures-2/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/pkg/helpers/release/testdata/signatures/sha256=e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7/signature-1 b/pkg/helpers/release/testdata/signatures/sha256=e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7/signature-1 deleted file mode 100644 index b13c5c4a92..0000000000 Binary files a/pkg/helpers/release/testdata/signatures/sha256=e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7/signature-1 and /dev/null differ diff --git a/pkg/helpers/release/testdata/signatures/sha256=edd9824f0404f1a139688017e7001370e2f3fbc088b94da84506653b473fe140/signature-1 b/pkg/helpers/release/testdata/signatures/sha256=edd9824f0404f1a139688017e7001370e2f3fbc088b94da84506653b473fe140/signature-1 deleted file mode 100644 index c6dcbfc34e..0000000000 Binary files a/pkg/helpers/release/testdata/signatures/sha256=edd9824f0404f1a139688017e7001370e2f3fbc088b94da84506653b473fe140/signature-1 and /dev/null differ diff --git a/pkg/helpers/release/verify_test.go b/pkg/helpers/release/verify_test.go deleted file mode 100644 index bd428dbac3..0000000000 --- a/pkg/helpers/release/verify_test.go +++ /dev/null @@ -1,354 +0,0 @@ -package release - -import ( - "bytes" - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "path/filepath" - "reflect" - "testing" - - "golang.org/x/crypto/openpgp" -) - -type fakeSigStore struct { - digest string - signatures [][]byte - err error -} - -func (s *fakeSigStore) DigestSignatures(ctx context.Context, digest string) ([][]byte, error) { - if len(s.digest) > 0 && s.digest != digest { - panic("unexpected digest") - } - return s.signatures, s.err -} - -func (s *fakeSigStore) String() string { - return "test sig store" -} - -func Test_ReleaseVerifier_Verify(t *testing.T) { - data, err := ioutil.ReadFile(filepath.Join("testdata", "keyrings", "redhat.txt")) - if err != nil { - t.Fatal(err) - } - redhatPublic, err := openpgp.ReadArmoredKeyRing(bytes.NewBuffer(data)) - if err != nil { - t.Fatal(err) - } - data, err = ioutil.ReadFile(filepath.Join("testdata", "keyrings", "simple.txt")) - if err != nil { - t.Fatal(err) - } - simple, err := openpgp.ReadArmoredKeyRing(bytes.NewBuffer(data)) - if err != nil { - t.Fatal(err) - } - data, err = ioutil.ReadFile(filepath.Join("testdata", "keyrings", "combined.txt")) - if err != nil { - t.Fatal(err) - } - combined, err := openpgp.ReadArmoredKeyRing(bytes.NewBuffer(data)) - if err != nil { - t.Fatal(err) - } - - serveSignatures := http.FileServer(http.Dir(filepath.Join("testdata", "signatures"))) - sigServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - serveSignatures.ServeHTTP(w, req) - })) - defer sigServer.Close() - sigServerURL, _ := url.Parse(sigServer.URL) - - serveEmpty := http.FileServer(http.Dir(filepath.Join("testdata", "signatures-2"))) - emptyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - serveEmpty.ServeHTTP(w, req) - })) - defer emptyServer.Close() - emptyServerURL, _ := url.Parse(emptyServer.URL) - - validSignatureData, err := ioutil.ReadFile(filepath.Join("testdata", "signatures", "sha256=e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7", "signature-1")) - - tests := []struct { - name string - verifiers map[string]openpgp.EntityList - locations []*url.URL - stores []SignatureStore - releaseDigest string - wantErr bool - }{ - {releaseDigest: "", wantErr: true}, - {releaseDigest: "!", wantErr: true}, - - { - name: "valid signature for sha over file", - releaseDigest: "sha256:e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7", - locations: []*url.URL{ - {Scheme: "file", Path: "testdata/signatures"}, - }, - stores: []SignatureStore{ - &fakeSigStore{err: fmt.Errorf("logged only")}, - }, - verifiers: map[string]openpgp.EntityList{"redhat": redhatPublic}, - }, - { - name: "valid signature for sha over http", - releaseDigest: "sha256:e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7", - locations: []*url.URL{ - sigServerURL, - }, - verifiers: map[string]openpgp.EntityList{"redhat": redhatPublic}, - }, - { - name: "valid signature for sha from store", - releaseDigest: "sha256:e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7", - stores: []SignatureStore{ - &fakeSigStore{}, - &fakeSigStore{err: fmt.Errorf("logged only")}, - &fakeSigStore{digest: "sha256:e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7", signatures: [][]byte{validSignatureData}}, - }, - verifiers: map[string]openpgp.EntityList{"redhat": redhatPublic}, - }, - { - name: "valid signature for sha over http with custom gpg key", - releaseDigest: "sha256:edd9824f0404f1a139688017e7001370e2f3fbc088b94da84506653b473fe140", - locations: []*url.URL{ - sigServerURL, - }, - verifiers: map[string]openpgp.EntityList{"simple": simple}, - }, - { - name: "valid signature for sha over http with multi-key keyring", - releaseDigest: "sha256:edd9824f0404f1a139688017e7001370e2f3fbc088b94da84506653b473fe140", - locations: []*url.URL{ - sigServerURL, - }, - verifiers: map[string]openpgp.EntityList{"combined": combined}, - }, - - { - name: "store rejects if no store found", - releaseDigest: "sha256:e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7", - stores: []SignatureStore{ - &fakeSigStore{}, - &fakeSigStore{}, - }, - verifiers: map[string]openpgp.EntityList{"redhat": redhatPublic}, - wantErr: true, - }, - { - name: "file location rejects if digest is not found", - releaseDigest: "sha256:0000000000000000000000000000000000000000000000000000000000000000", - locations: []*url.URL{ - {Scheme: "file", Path: "testdata/signatures"}, - }, - verifiers: map[string]openpgp.EntityList{"redhat": redhatPublic}, - wantErr: true, - }, - { - name: "http location rejects if digest is not found", - releaseDigest: "sha256:0000000000000000000000000000000000000000000000000000000000000000", - locations: []*url.URL{ - sigServerURL, - }, - verifiers: map[string]openpgp.EntityList{"redhat": redhatPublic}, - wantErr: true, - }, - - { - name: "sha contains invalid characters", - releaseDigest: "!sha256:e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7", - locations: []*url.URL{ - {Scheme: "file", Path: "testdata/signatures"}, - }, - verifiers: map[string]openpgp.EntityList{"redhat": redhatPublic}, - wantErr: true, - }, - { - name: "sha contains too many separators", - releaseDigest: "sha256:e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7:", - locations: []*url.URL{ - {Scheme: "file", Path: "testdata/signatures"}, - }, - verifiers: map[string]openpgp.EntityList{"redhat": redhatPublic}, - wantErr: true, - }, - - { - name: "could not find signature in file location", - releaseDigest: "sha256:e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7", - locations: []*url.URL{ - {Scheme: "file", Path: "testdata/signatures-2"}, - }, - verifiers: map[string]openpgp.EntityList{"redhat": redhatPublic}, - wantErr: true, - }, - { - name: "could not find signature in http location", - releaseDigest: "sha256:e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7", - locations: []*url.URL{ - emptyServerURL, - }, - verifiers: map[string]openpgp.EntityList{"redhat": redhatPublic}, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if err != nil { - t.Fatal(err) - } - v := &ReleaseVerifier{ - verifiers: tt.verifiers, - stores: tt.stores, - locations: tt.locations, - clientBuilder: DefaultClient, - } - if err := v.Verify(context.Background(), tt.releaseDigest); (err != nil) != tt.wantErr { - t.Errorf("ReleaseVerifier.Verify() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} - -func Test_ReleaseVerifier_String(t *testing.T) { - data, err := ioutil.ReadFile(filepath.Join("testdata", "keyrings", "redhat.txt")) - if err != nil { - t.Fatal(err) - } - redhatPublic, err := openpgp.ReadArmoredKeyRing(bytes.NewBuffer(data)) - if err != nil { - t.Fatal(err) - } - - tests := []struct { - name string - verifiers map[string]openpgp.EntityList - locations []*url.URL - stores []SignatureStore - want string - }{ - { - want: `All release image digests must have GPG signatures from - `, - }, - { - locations: []*url.URL{ - {Scheme: "http", Host: "localhost", Path: "test"}, - {Scheme: "file", Path: "/absolute/url"}, - }, - want: `All release image digests must have GPG signatures from - will check for signatures in containers/image format at http://localhost/test, file:///absolute/url`, - }, - { - verifiers: map[string]openpgp.EntityList{ - "redhat": redhatPublic, - }, - locations: []*url.URL{{Scheme: "http", Host: "localhost", Path: "test"}}, - want: `All release image digests must have GPG signatures from redhat (567E347AD0044ADE55BA8A5F199E2F91FD431D51: Red Hat, Inc. (release key 2) ) - will check for signatures in containers/image format at http://localhost/test`, - }, - { - verifiers: map[string]openpgp.EntityList{ - "redhat": redhatPublic, - }, - stores: []SignatureStore{&fakeSigStore{}, &fakeSigStore{}}, - want: `All release image digests must have GPG signatures from redhat (567E347AD0044ADE55BA8A5F199E2F91FD431D51: Red Hat, Inc. (release key 2) ) - will check for signatures in containers/image format from test sig store, test sig store`, - }, - { - verifiers: map[string]openpgp.EntityList{ - "redhat": redhatPublic, - }, - locations: []*url.URL{ - {Scheme: "http", Host: "localhost", Path: "test"}, - {Scheme: "file", Path: "/absolute/url"}, - }, - stores: []SignatureStore{&fakeSigStore{}, &fakeSigStore{}}, - want: `All release image digests must have GPG signatures from redhat (567E347AD0044ADE55BA8A5F199E2F91FD431D51: Red Hat, Inc. (release key 2) ) - will check for signatures in containers/image format at http://localhost/test, file:///absolute/url and from test sig store, test sig store`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - v := &ReleaseVerifier{ - verifiers: tt.verifiers, - locations: tt.locations, - stores: tt.stores, - } - if got := v.String(); got != tt.want { - t.Errorf("ReleaseVerifier.String() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_ReleaseVerifier_Signatures(t *testing.T) { - data, err := ioutil.ReadFile(filepath.Join("testdata", "keyrings", "redhat.txt")) - if err != nil { - t.Fatal(err) - } - redhatPublic, err := openpgp.ReadArmoredKeyRing(bytes.NewBuffer(data)) - if err != nil { - t.Fatal(err) - } - - const signedDigest = "sha256:e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7" - - // verify we don't cache a negative result - verifier := NewReleaseVerifier( - map[string]openpgp.EntityList{"redhat": redhatPublic}, - []*url.URL{{Scheme: "file", Path: "testdata/signatures-wrong"}}, - DefaultClient, - ) - if err := verifier.Verify(context.Background(), signedDigest); err == nil || err.Error() != "unable to locate a valid signature for one or more sources" { - t.Fatal(err) - } - if sigs := verifier.Signatures(); len(sigs) != 0 { - t.Fatalf("%#v", sigs) - } - - // verify we cache a valid request - verifier = NewReleaseVerifier( - map[string]openpgp.EntityList{"redhat": redhatPublic}, - []*url.URL{{Scheme: "file", Path: "testdata/signatures"}}, - DefaultClient, - ) - if err := verifier.Verify(context.Background(), signedDigest); err != nil { - t.Fatal(err) - } - if sigs := verifier.Signatures(); len(sigs) != 1 { - t.Fatalf("%#v", sigs) - } - - // verify we hit the cache instead of verifying, even after changing the locations directory - verifier.locations = []*url.URL{{Scheme: "file", Path: "testdata/signatures-wrong"}} - if err := verifier.Verify(context.Background(), signedDigest); err != nil { - t.Fatal(err) - } - if sigs := verifier.Signatures(); len(sigs) != 1 { - t.Fatalf("%#v", sigs) - } - - // verify we maintain a maximum number of cache entries a valid request - expectedSignature, err := ioutil.ReadFile(filepath.Join("testdata", "signatures", "sha256=e3f12513a4b22a2d7c0e7c9207f52128113758d9d68c7d06b11a0ac7672966f7", "signature-1")) - if err != nil { - t.Fatal(err) - } - - verifier = NewReleaseVerifier( - map[string]openpgp.EntityList{"redhat": redhatPublic}, - []*url.URL{{Scheme: "file", Path: "testdata/signatures"}}, - DefaultClient, - ) - for i := 0; i < maxSignatureCacheSize*2; i++ { - verifier.signatureCache[fmt.Sprintf("test-%d", i)] = [][]byte{[]byte("blah")} - } - - if err := verifier.Verify(context.Background(), signedDigest); err != nil { - t.Fatal(err) - } - if sigs := verifier.Signatures(); len(sigs) != maxSignatureCacheSize || !reflect.DeepEqual(sigs[signedDigest], [][]byte{expectedSignature}) { - t.Fatalf("%d %#v", len(sigs), sigs) - } -} diff --git a/vendor/github.com/openshift/library-go/pkg/verify/OWNERS b/vendor/github.com/openshift/library-go/pkg/verify/OWNERS new file mode 100644 index 0000000000..578fa80a81 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/OWNERS @@ -0,0 +1,5 @@ +# See the OWNERS docs: https://git.k8s.io/community/contributors/guide/owners.md +# This file just uses aliases defined in OWNERS_ALIASES. + +approvers: + - update-approvers diff --git a/vendor/github.com/openshift/library-go/pkg/verify/configmap.go b/vendor/github.com/openshift/library-go/pkg/verify/configmap.go new file mode 100644 index 0000000000..050163a8e4 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/configmap.go @@ -0,0 +1,174 @@ +package verify + +import ( + "bytes" + "fmt" + "net/url" + "strings" + + "github.com/pkg/errors" + "golang.org/x/crypto/openpgp" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/klog/v2" + + "github.com/openshift/library-go/pkg/manifest" + "github.com/openshift/library-go/pkg/verify/store" + "github.com/openshift/library-go/pkg/verify/store/configmap" + "github.com/openshift/library-go/pkg/verify/store/parallel" + "github.com/openshift/library-go/pkg/verify/store/sigstore" + "github.com/openshift/library-go/pkg/verify/util" +) + +const ( + // ReleaseAnnotationConfigMapVerifier is an annotation set on a config map in the + // release payload to indicate that this config map controls signing for the payload. + // Only the first config map within the payload should be used, regardless of whether + // it has data. See NewFromConfigMapData for more. + ReleaseAnnotationConfigMapVerifier = "release.openshift.io/verification-config-map" + + // verifierPublicKeyPrefix is the unique portion of the key used within a config map + // identifying data field containing one or more GPG public keys in ASCII form that + // must have signed the release image by digest. + verifierPublicKeyPrefix = "verifier-public-key-" + + // storePrefix is the unique portion of the key used within a config map identifying + // data field containing a URL (scheme http://, or https://) location that contains + // signatures. + storePrefix = "store-" +) + +// GetSignaturesAsConfigmap returns the given signatures in a config map. Uses +// util.DigestToKeyPrefix to replace colon with dash when saving digest to config map. +func GetSignaturesAsConfigmap(digest string, signatures [][]byte) (*corev1.ConfigMap, error) { + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: corev1.SchemeGroupVersion.String(), + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: configmap.NamespaceLabelConfigMap, + Labels: map[string]string{ + configmap.ReleaseLabelConfigMap: "", + }, + }, + BinaryData: make(map[string][]byte), + } + prefix, err := util.DigestToKeyPrefix(digest, "-") + if err != nil { + return nil, err + } + cm.Name = prefix + for i, v := range signatures { + cm.BinaryData[fmt.Sprintf("%s-%d", prefix, i+1)] = v + } + return cm, nil +} + +// NewFromManifests fetches the first config map in the manifest list with the correct annotation. +// It returns an error if the data is not valid, or no verifier if a config map wth the required +// annotation is not found. See the verify package for more details on the algorithm for verification. +// If the annotation is set, a verifier or error is always returned. +func NewFromManifests(manifests []manifest.Manifest, clientBuilder sigstore.HTTPClient) (*ReleaseVerifier, error) { + for _, manifest := range manifests { + configMap, err := util.ReadConfigMap(manifest.Raw) + + // configMap will be nil if this is not a config map + if err != nil || configMap == nil { + continue + } + if _, ok := configMap.Annotations[ReleaseAnnotationConfigMapVerifier]; !ok { + continue + } + src := fmt.Sprintf("the config map %s/%s", configMap.Namespace, configMap.Name) + data, _, err := unstructured.NestedStringMap(manifest.Obj.Object, "data") + if err != nil { + return nil, errors.Wrapf(err, "%s is not valid: %v", src, err) + } + verifier, err := newFromConfigMapData(src, data, clientBuilder) + if err != nil { + return nil, err + } + return verifier, nil + } + return nil, nil +} + +// newFromConfigMapData expects to receive the data field of the first config map in the release +// image payload with the annotation "release.openshift.io/verification-config-map". Only the +// first payload item in lexographic order will be considered - all others are ignored. The +// verifier returned by this method +// +// The presence of one or more config maps instructs the CVO to verify updates before they are +// downloaded. +// +// The keys within the config map in the data field define how verification is performed: +// +// verifier-public-key-*: One or more GPG public keys in ASCII form that must have signed the +// release image by digest. +// +// store-*: A URL (scheme file://, http://, or https://) location that contains signatures. These +// signatures are in the atomic container signature format. The URL will have the digest +// of the image appended to it as "/=/signature-" as described +// in the container image signing format. The docker-image-manifest section of the +// signature must match the release image digest. Signatures are searched starting at +// NUMBER 1 and incrementing if the signature exists but is not valid. The signature is a +// GPG signed and encrypted JSON message. The file store is provided for testing only at +// the current time, although future versions of the CVO might allow host mounting of +// signatures. +// +// See https://github.com/containers/image/blob/ab49b0a48428c623a8f03b41b9083d48966b34a9/docs/signature-protocols.md +// for a description of the signature store +// +// The returned verifier will require that any new release image will only be considered verified +// if each provided public key has signed the release image digest. The signature may be in any +// store and the lookup order is internally defined. +func newFromConfigMapData(src string, data map[string]string, clientBuilder sigstore.HTTPClient) (*ReleaseVerifier, error) { + verifiers := make(map[string]openpgp.EntityList) + var stores []store.Store + for k, v := range data { + switch { + case strings.HasPrefix(k, verifierPublicKeyPrefix): + keyring, err := loadArmoredOrUnarmoredGPGKeyRing([]byte(v)) + if err != nil { + return nil, errors.Wrapf(err, "%s has an invalid key %q that must be a GPG public key: %v", src, k, err) + } + verifiers[k] = keyring + case strings.HasPrefix(k, storePrefix): + v = strings.TrimSpace(v) + u, err := url.Parse(v) + if err != nil || (u.Scheme != "http" && u.Scheme != "https" && u.Scheme != "file") { + return nil, fmt.Errorf("%s has an invalid key %q: must be a valid URL with scheme file://, http://, or https://", src, k) + } + if u.Scheme == "file" { + stores = append(stores, &fileStore{ + directory: u.Path, + }) + } else { + stores = append(stores, &sigstore.Store{ + URI: u, + HTTPClient: clientBuilder, + }) + } + default: + klog.Warningf("An unexpected key was found in %s and will be ignored (expected store-* or verifier-public-key-*): %s", src, k) + } + } + if len(stores) == 0 { + return nil, fmt.Errorf("%s did not provide any signature stores to read from and cannot be used", src) + } + if len(verifiers) == 0 { + return nil, fmt.Errorf("%s did not provide any GPG public keys to verify signatures from and cannot be used", src) + } + + return NewReleaseVerifier(verifiers, ¶llel.Store{Stores: stores}), nil +} + +func loadArmoredOrUnarmoredGPGKeyRing(data []byte) (openpgp.EntityList, error) { + keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(data)) + if err == nil { + return keyring, nil + } + return openpgp.ReadKeyRing(bytes.NewReader(data)) +} diff --git a/vendor/github.com/openshift/library-go/pkg/verify/persist.go b/vendor/github.com/openshift/library-go/pkg/verify/persist.go new file mode 100644 index 0000000000..45cdf06eda --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/persist.go @@ -0,0 +1,53 @@ +package verify + +import ( + "context" + "time" + + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/klog/v2" +) + +// SignatureSource provides a set of signatures by digest to save. +type SignatureSource interface { + // Signatures returns a list of valid signatures for release digests. + Signatures() map[string][][]byte +} + +// PersistentSignatureStore is a store that can save signatures for +// later recovery. +type PersistentSignatureStore interface { + // Store saves the provided signatures or return an error. If context + // reaches its deadline the store should be cancelled. + Store(ctx context.Context, signatures map[string][][]byte) error +} + +// StorePersister saves signatures into store periodically. +type StorePersister struct { + store PersistentSignatureStore + signatures SignatureSource +} + +// NewSignatureStorePersister creates an instance that can save signatures into the destination +// store. +func NewSignatureStorePersister(dst PersistentSignatureStore, src SignatureSource) *StorePersister { + return &StorePersister{ + store: dst, + signatures: src, + } +} + +// Run flushes signatures to the provided store every interval or until the context is finished. +// After context is done, it runs one more time to attempt to flush the current state. It does not +// return until that last store completes. +func (p *StorePersister) Run(ctx context.Context, interval time.Duration) { + wait.Until(func() { + if err := p.store.Store(ctx, p.signatures.Signatures()); err != nil { + klog.Warningf("Unable to save signatures: %v", err) + } + }, interval, ctx.Done()) + + if err := p.store.Store(context.Background(), p.signatures.Signatures()); err != nil { + klog.Warningf("Unable to save signatures during final flush: %v", err) + } +} diff --git a/vendor/github.com/openshift/library-go/pkg/verify/store/configmap/configmap.go b/vendor/github.com/openshift/library-go/pkg/verify/store/configmap/configmap.go new file mode 100644 index 0000000000..f1c374f461 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/store/configmap/configmap.go @@ -0,0 +1,178 @@ +package configmap + +import ( + "context" + "fmt" + "sort" + "strings" + "sync" + "time" + + "golang.org/x/time/rate" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/util/retry" + "k8s.io/klog/v2" + + "github.com/openshift/library-go/pkg/verify/store" + "github.com/openshift/library-go/pkg/verify/util" +) + +const ( + // NamespaceLabelConfigMap is the Namespace label applied to a configmap + // containing signatures. + NamespaceLabelConfigMap = "openshift-config-managed" + + // ReleaseLabelConfigMap is a label applied to a configmap inside the + // openshift-config-managed namespace that indicates it contains signatures + // for release image digests. Any binaryData key that starts with the digest + // is added to the list of signatures checked. + ReleaseLabelConfigMap = "release.openshift.io/verification-signatures" +) + +// Store abstracts retrieving signatures from config maps on a cluster. +type Store struct { + client corev1client.ConfigMapsGetter + ns string + + limiter *rate.Limiter + lock sync.Mutex + last []corev1.ConfigMap +} + +// NewStore returns a store that can retrieve or persist signatures on a +// cluster. If limiter is not specified it defaults to one call every 30 seconds. +func NewStore(client corev1client.ConfigMapsGetter, limiter *rate.Limiter) *Store { + if limiter == nil { + limiter = rate.NewLimiter(rate.Every(30*time.Second), 1) + } + return &Store{ + client: client, + ns: NamespaceLabelConfigMap, + limiter: limiter, + } +} + +// String displays information about this source for human review. +func (s *Store) String() string { + return fmt.Sprintf("config maps in %s with label %q", s.ns, ReleaseLabelConfigMap) +} + +// rememberMostRecentConfigMaps stores a set of config maps containing +// signatures. +func (s *Store) rememberMostRecentConfigMaps(last []corev1.ConfigMap) { + names := make([]string, 0, len(last)) + for _, cm := range last { + names = append(names, cm.ObjectMeta.Name) + } + sort.Strings(names) + s.lock.Lock() + defer s.lock.Unlock() + klog.V(4).Infof("remember most recent signature config maps: %s", strings.Join(names, " ")) + s.last = last +} + +// mostRecentConfigMaps returns the last cached version of config maps +// containing signatures. +func (s *Store) mostRecentConfigMaps() []corev1.ConfigMap { + s.lock.Lock() + defer s.lock.Unlock() + klog.V(4).Info("use cached most recent signature config maps") + return s.last +} + +// Signatures returns a list of signatures that match the request +// digest out of config maps labelled with ReleaseLabelConfigMap in the +// NamespaceLabelConfigMap namespace. +func (s *Store) Signatures(ctx context.Context, name string, digest string, fn store.Callback) error { + // avoid repeatedly reloading config maps + items := s.mostRecentConfigMaps() + r := s.limiter.Reserve() + if items == nil || r.OK() { + configMaps, err := s.client.ConfigMaps(s.ns).List(ctx, metav1.ListOptions{ + LabelSelector: ReleaseLabelConfigMap, + }) + if err != nil { + s.rememberMostRecentConfigMaps([]corev1.ConfigMap{}) + return err + } + items = configMaps.Items + s.rememberMostRecentConfigMaps(configMaps.Items) + } + + prefix, err := util.DigestToKeyPrefix(digest, "-") + if err != nil { + return err + } + + for _, cm := range items { + klog.V(4).Infof("searching for %s in signature config map %s", prefix, cm.ObjectMeta.Name) + for k, v := range cm.BinaryData { + if strings.HasPrefix(k, prefix) { + klog.V(4).Infof("key %s from signature config map %s matches %s", k, cm.ObjectMeta.Name, digest) + done, err := fn(ctx, v, nil) + if err != nil || done { + return err + } + if err := ctx.Err(); err != nil { + return err + } + } + } + } + return nil +} + +// Store attempts to persist the provided signatures into a form Signatures will +// retrieve. +func (s *Store) Store(ctx context.Context, signaturesByDigest map[string][][]byte) error { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: s.ns, + Name: "signatures-managed", + Labels: map[string]string{ + ReleaseLabelConfigMap: "", + }, + }, + BinaryData: make(map[string][]byte), + } + count := 0 + for digest, signatures := range signaturesByDigest { + prefix, err := util.DigestToKeyPrefix(digest, "-") + if err != nil { + return err + } + for i := 0; i < len(signatures); i++ { + cm.BinaryData[fmt.Sprintf("%s-%d", prefix, i)] = signatures[i] + count += 1 + } + } + return retry.OnError( + retry.DefaultRetry, + func(err error) bool { return errors.IsConflict(err) || errors.IsAlreadyExists(err) }, + func() error { + existing, err := s.client.ConfigMaps(s.ns).Get(ctx, cm.Name, metav1.GetOptions{}) + if errors.IsNotFound(err) { + _, err := s.client.ConfigMaps(s.ns).Create(ctx, cm, metav1.CreateOptions{}) + if err != nil { + klog.V(4).Infof("create signature cache config map %s in namespace %s with %d signatures", cm.ObjectMeta.Name, s.ns, count) + } + return err + } + if err != nil { + return err + } + existing.Labels = cm.Labels + existing.BinaryData = cm.BinaryData + existing.Data = cm.Data + _, err = s.client.ConfigMaps(s.ns).Update(ctx, existing, metav1.UpdateOptions{}) + if err != nil { + klog.V(4).Infof("update signature cache config map %s in namespace %s with %d signatures", cm.ObjectMeta.Name, s.ns, count) + } + return err + }, + ) +} diff --git a/vendor/github.com/openshift/library-go/pkg/verify/store/parallel/parallel.go b/vendor/github.com/openshift/library-go/pkg/verify/store/parallel/parallel.go new file mode 100644 index 0000000000..4d3161ad90 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/store/parallel/parallel.go @@ -0,0 +1,92 @@ +// Package parallel combines several signature stores in a single store. +// Signatures are searched in each substore simultaneously until a +// match is found. +package parallel + +import ( + "context" + "fmt" + "strings" + + "github.com/openshift/library-go/pkg/verify/store" +) + +type signatureResponse struct { + signature []byte + errIn error +} + +// Store provides access to signatures stored in sub-stores. +type Store struct { + Stores []store.Store +} + +// Signatures fetches signatures for the provided digest. +func (s *Store) Signatures(ctx context.Context, name string, digest string, fn store.Callback) error { + nestedCtx, cancel := context.WithCancel(ctx) + defer cancel() + responses := make(chan signatureResponse, len(s.Stores)) + errorChannelCount := 0 + errorChannel := make(chan error, 1) + + for i := range s.Stores { + errorChannelCount++ + go func(ctx context.Context, wrappedStore store.Store, name string, digest string, responses chan signatureResponse, errorChannel chan error) { + errorChannel <- wrappedStore.Signatures(ctx, name, digest, func(ctx context.Context, signature []byte, errIn error) (done bool, err error) { + select { + case <-ctx.Done(): + return true, nil + case responses <- signatureResponse{signature: signature, errIn: errIn}: + } + return false, nil + }) + }(nestedCtx, s.Stores[i], name, digest, responses, errorChannel) + } + + allDone := false + var loopError error + for errorChannelCount > 0 { + if allDone { + err := <-errorChannel + errorChannelCount-- + if loopError == nil && err != nil && err != context.Canceled && err != context.DeadlineExceeded { + loopError = err + } + } else { + select { + case response := <-responses: + done, err := fn(ctx, response.signature, response.errIn) + if done || err != nil { + allDone = true + loopError = err + cancel() + } + case err := <-errorChannel: + errorChannelCount-- + if loopError == nil && err != nil && err != context.Canceled && err != context.DeadlineExceeded { + loopError = err + } + } + } + } + close(responses) + close(errorChannel) + if loopError != nil { + return loopError + } + return ctx.Err() // because we discard context errors from the wrapped stores +} + +// String returns a description of where this store finds +// signatures. +func (s *Store) String() string { + wrapped := "no stores" + if len(s.Stores) > 0 { + names := make([]string, 0, len(s.Stores)) + for _, store := range s.Stores { + names = append(names, store.String()) + } + wrapped = strings.Join(names, ", ") + } + return fmt.Sprintf("parallel signature store wrapping %s", wrapped) +} diff --git a/vendor/github.com/openshift/library-go/pkg/verify/store/sigstore/client.go b/vendor/github.com/openshift/library-go/pkg/verify/store/sigstore/client.go new file mode 100644 index 0000000000..c9904e553b --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/store/sigstore/client.go @@ -0,0 +1,51 @@ +package sigstore + +import ( + "net/http" + "sync" + "time" + + "golang.org/x/time/rate" +) + +// HTTPClient returns a client suitable for retrieving signatures. It is not +// required to be unique per call, but may be called concurrently. +type HTTPClient func() (*http.Client, error) + +// DefaultClient creates an http.Client with no configuration. +func DefaultClient() (*http.Client, error) { + return &http.Client{}, nil +} + +// CachedHTTPClientConstructor wraps an HTTPClient implementation so +// that it is not called more frequently than the configured limiter. +type CachedHTTPClientConstructor struct { + wrapped HTTPClient + limiter *rate.Limiter + + lock sync.Mutex + lastClient *http.Client + lastError error +} + +// NewCachedHTTPClientConstructor creates a new cached constructor. +// If limiter is not specified it defaults to one call every 30 seconds. +func NewCachedHTTPClientConstructor(wrapped HTTPClient, limiter *rate.Limiter) *CachedHTTPClientConstructor { + if limiter == nil { + limiter = rate.NewLimiter(rate.Every(30*time.Second), 1) + } + return &CachedHTTPClientConstructor{ + wrapped: wrapped, + limiter: limiter, + } +} + +func (c *CachedHTTPClientConstructor) HTTPClient() (*http.Client, error) { + c.lock.Lock() + defer c.lock.Unlock() + r := c.limiter.Reserve() + if r.OK() { + c.lastClient, c.lastError = c.wrapped() + } + return c.lastClient, c.lastError +} diff --git a/vendor/github.com/openshift/library-go/pkg/verify/store/sigstore/sigstore.go b/vendor/github.com/openshift/library-go/pkg/verify/store/sigstore/sigstore.go new file mode 100644 index 0000000000..d61bb07f4e --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/store/sigstore/sigstore.go @@ -0,0 +1,151 @@ +// Package sigstore retrieves signatures using the sig-store protocol +// described in [1]. +// +// A URL (scheme http:// or https://) location that contains +// signatures. These signatures are in the atomic container signature +// format. The URL will have the digest of the image appended to it as +// "/=/signature-" as described in the +// container image signing format. Signatures are searched starting at +// NUMBER 1 and incrementing if the signature exists but is not valid. +// +// [1]: https://github.com/containers/image/blob/ab49b0a48428c623a8f03b41b9083d48966b34a9/docs/signature-protocols.md +package sigstore + +import ( + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "path" + "strconv" + + "k8s.io/klog/v2" + + "github.com/openshift/library-go/pkg/verify/store" + "github.com/openshift/library-go/pkg/verify/util" +) + +// maxSignatureSearch prevents unbounded recursion on malicious signature stores (if +// an attacker was able to take ownership of the store to perform DoS on clusters). +const maxSignatureSearch = 10 + +var errNotFound = errors.New("no more signatures to check") + +// Store provides access to signatures stored in memory. +type Store struct { + // URI is the base from which signature URIs are constructed. + URI *url.URL + + // HTTPClient is called once for each Signatures call to ensure + // requests are made with the currently-recommended parameters. + HTTPClient HTTPClient +} + +// Signatures fetches signatures for the provided digest. +func (s *Store) Signatures(ctx context.Context, name string, digest string, fn store.Callback) error { + equalDigest, err := util.DigestToKeyPrefix(digest, "=") + if err != nil { + return err + } + switch s.URI.Scheme { + case "http", "https": + client, err := s.HTTPClient() + if err != nil { + _, err = fn(ctx, nil, err) + return err + } + + copied := *s.URI + copied.Path = path.Join(copied.Path, equalDigest) + if err := checkHTTPSignatures(ctx, client, copied, maxSignatureSearch, fn); err != nil { + return err + } + default: + return fmt.Errorf("the store %s scheme is unrecognized", s.URI) + } + + return nil +} + +// checkHTTPSignatures reads signatures as "signature-1", "signature-2", etc. as children of the provided URL +// over HTTP or HTTPS. No more than maxSignaturesToCheck will be read. If the provided context is cancelled +// search will be terminated. +func checkHTTPSignatures(ctx context.Context, client *http.Client, u url.URL, maxSignaturesToCheck int, fn store.Callback) error { + base := path.Join(u.Path, "signature-") + sigURL := u + for i := 1; i < maxSignatureSearch; i++ { + if err := ctx.Err(); err != nil { + return err + } + + sigURL.Path = base + strconv.Itoa(i) + + req, err := http.NewRequest("GET", sigURL.String(), nil) + if err != nil { + _, err = fn(ctx, nil, fmt.Errorf("could not build request to check signature: %v", err)) + return err // even if the callback ate the error, no sense in checking later indexes which will fail the same way + } + req = req.WithContext(ctx) + // load the body, being careful not to allow unbounded reads + resp, err := client.Do(req) + if err != nil { + klog.V(4).Infof("unable to load signature: %v", err) + done, err := fn(ctx, nil, err) + if done || err != nil { + return err + } + continue + } + data, err := func() ([]byte, error) { + body := resp.Body + r := io.LimitReader(body, 50*1024) + + defer func() { + // read the remaining body to avoid breaking the connection + io.Copy(ioutil.Discard, r) + body.Close() + }() + + if resp.StatusCode == http.StatusNotFound { + return nil, errNotFound + } + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + if i == 1 { + klog.V(4).Infof("Could not find signature at store location %v", sigURL) + } + return nil, fmt.Errorf("unable to retrieve signature from %v: %d", sigURL, resp.StatusCode) + } + + return ioutil.ReadAll(resp.Body) + }() + if err == errNotFound { + break + } + if err != nil { + klog.V(4).Info(err) + done, err := fn(ctx, nil, err) + if done || err != nil { + return err + } + continue + } + if len(data) == 0 { + continue + } + + done, err := fn(ctx, data, nil) + if done || err != nil { + return err + } + } + return nil +} + +// String returns a description of where this store finds +// signatures. +func (s *Store) String() string { + return fmt.Sprintf("containers/image signature store under %s", s.URI) +} diff --git a/vendor/github.com/openshift/library-go/pkg/verify/store/store.go b/vendor/github.com/openshift/library-go/pkg/verify/store/store.go new file mode 100644 index 0000000000..00a19d2be4 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/store/store.go @@ -0,0 +1,27 @@ +// Package store defines generic interfaces for signature stores. +package store + +import ( + "context" +) + +// Callback returns true if an acceptable signature has been found, or +// an error if the loop should be aborted. If there was a problem +// retrieving the signature, the incoming error will describe the +// problem and the function can decide how to handle that error. +type Callback func(ctx context.Context, signature []byte, errIn error) (done bool, err error) + +// Store provides access to signatures by digest. +type Store interface { + + // Signatures fetches signatures for the provided digest, feeding + // them into the provided callback until an acceptable signature is + // found or an error occurs. Not finding any acceptable signatures + // is not an error; it is up to the caller to handle that case. + Signatures(ctx context.Context, name string, digest string, fn Callback) error + + // String returns a description of where this store finds + // signatures. The string is a short clause intended for display in + // a description of the verifier. + String() string +} diff --git a/vendor/github.com/openshift/library-go/pkg/verify/util/encode.go b/vendor/github.com/openshift/library-go/pkg/verify/util/encode.go new file mode 100644 index 0000000000..16900bd08c --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/util/encode.go @@ -0,0 +1,40 @@ +package util + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" +) + +var ( + coreScheme = runtime.NewScheme() + coreCodecs = serializer.NewCodecFactory(coreScheme) + coreEncoder runtime.Encoder +) + +func init() { + if err := corev1.AddToScheme(coreScheme); err != nil { + panic(err) + } + coreEncoderCodecFactory := serializer.NewCodecFactory(coreScheme) + coreEncoder = coreEncoderCodecFactory.LegacyCodec(corev1.SchemeGroupVersion) +} + +// ReadConfigMap reads config map object from bytes. nil is returned if +// the object cannot be decoded as a config map. +func ReadConfigMap(objBytes []byte) (*corev1.ConfigMap, error) { + requiredObj, err := runtime.Decode(coreCodecs.UniversalDecoder(corev1.SchemeGroupVersion), objBytes) + if err != nil { + return nil, err + } + cm, ok := requiredObj.(*corev1.ConfigMap) + if ok { + return cm, nil + } + return nil, nil +} + +// ConfigMapAsBytes returns given config map as bytes. +func ConfigMapAsBytes(cm *corev1.ConfigMap) ([]byte, error) { + return runtime.Encode(coreEncoder, cm) +} diff --git a/vendor/github.com/openshift/library-go/pkg/verify/util/util.go b/vendor/github.com/openshift/library-go/pkg/verify/util/util.go new file mode 100644 index 0000000000..62d2c410ff --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/util/util.go @@ -0,0 +1,18 @@ +package util + +import ( + "fmt" + "strings" +) + +// DigestToKeyPrefix changes digest to use the provided newDivider in place of ':', +// {algo}{newDivider}{hash} instead of {algo}:{hash}, because colons are not allowed +// in various places such as ConfigMap keys. +func DigestToKeyPrefix(digest string, newDivider string) (string, error) { + parts := strings.SplitN(digest, ":", 3) + if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { + return "", fmt.Errorf("the provided digest must be of the form ALGO:HASH") + } + algo, hash := parts[0], parts[1] + return fmt.Sprintf("%s%s%s", algo, newDivider, hash), nil +} diff --git a/vendor/github.com/openshift/library-go/pkg/verify/verify.go b/vendor/github.com/openshift/library-go/pkg/verify/verify.go new file mode 100644 index 0000000000..055a07ba34 --- /dev/null +++ b/vendor/github.com/openshift/library-go/pkg/verify/verify.go @@ -0,0 +1,372 @@ +package verify + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + "sync" + "time" + + "golang.org/x/crypto/openpgp" + "k8s.io/klog/v2" + + "github.com/openshift/library-go/pkg/verify/store" + "github.com/openshift/library-go/pkg/verify/util" +) + +// Interface performs verification of the provided content. The default implementation +// in this package uses the container signature format defined at https://github.com/containers/image +// to authenticate that a given release image digest has been signed by a trusted party. +type Interface interface { + // Verify should return nil if the provided release digest has suffient signatures to be considered + // valid. It should return an error in all other cases. + Verify(ctx context.Context, releaseDigest string) error +} + +type rejectVerifier struct{} + +func (rejectVerifier) Verify(ctx context.Context, releaseDigest string) error { + return fmt.Errorf("verification is not possible") +} + +// Reject fails always fails verification. +var Reject Interface = rejectVerifier{} + +// maxSignatureSearch prevents unbounded recursion on malicious signature stores (if +// an attacker was able to take ownership of the store to perform DoS on clusters). +const maxSignatureSearch = 10 + +// validReleaseDigest is a verification rule to filter clearly invalid digests. +var validReleaseDigest = regexp.MustCompile(`^[a-zA-Z0-9:]+$`) + +// ReleaseVerifier implements a signature intersection operation on a provided release +// digest - all verifiers must have at least one valid signature attesting the release +// digest. If any failure occurs the caller should assume the content is unverified. +type ReleaseVerifier struct { + verifiers map[string]openpgp.EntityList + + // Store is the store from which release signatures are retrieved. + Store store.Store + + lock sync.Mutex + signatureCache map[string][][]byte +} + +// NewReleaseVerifier creates a release verifier for the provided inputs. +func NewReleaseVerifier(verifiers map[string]openpgp.EntityList, store store.Store) *ReleaseVerifier { + return &ReleaseVerifier{ + verifiers: verifiers, + Store: store, + + signatureCache: make(map[string][][]byte), + } +} + +// Verifiers returns a copy of the verifiers in this payload. +func (v *ReleaseVerifier) Verifiers() map[string]openpgp.EntityList { + out := make(map[string]openpgp.EntityList, len(v.verifiers)) + for k, v := range v.verifiers { + out[k] = v + } + return out +} + +// String summarizes the verifier for human consumption +func (v *ReleaseVerifier) String() string { + var keys []string + for name := range v.verifiers { + keys = append(keys, name) + } + sort.Strings(keys) + + var builder strings.Builder + builder.Grow(256) + fmt.Fprintf(&builder, "All release image digests must have GPG signatures from") + if len(keys) == 0 { + fmt.Fprint(&builder, " ") + } + for _, name := range keys { + verifier := v.verifiers[name] + fmt.Fprintf(&builder, " %s (", name) + for i, entity := range verifier { + if i != 0 { + fmt.Fprint(&builder, ", ") + } + if entity.PrimaryKey != nil { + fmt.Fprintf(&builder, strings.ToUpper(fmt.Sprintf("%x", entity.PrimaryKey.Fingerprint))) + fmt.Fprint(&builder, ": ") + } + count := 0 + for identityName := range entity.Identities { + if count != 0 { + fmt.Fprint(&builder, ", ") + } + fmt.Fprintf(&builder, "%s", identityName) + count++ + } + } + fmt.Fprint(&builder, ")") + } + fmt.Fprintf(&builder, " - will check for signatures in containers/image format at") + if v.Store == nil { + fmt.Fprint(&builder, " ") + } else { + fmt.Fprintf(&builder, " %s", v.Store) + } + return builder.String() +} + +// Verify ensures that at least one valid signature exists for an image with digest +// matching release digest in any of the provided locations for all verifiers, or returns +// an error. +func (v *ReleaseVerifier) Verify(ctx context.Context, releaseDigest string) error { + if len(v.verifiers) == 0 || v.Store == nil { + return fmt.Errorf("the release verifier is incorrectly configured, unable to verify digests") + } + if len(releaseDigest) == 0 { + return fmt.Errorf("release images that are not accessed via digest cannot be verified") + } + if !validReleaseDigest.MatchString(releaseDigest) { + return fmt.Errorf("the provided release image digest contains prohibited characters") + } + + if v.hasVerified(releaseDigest) { + return nil + } + + remaining := make(map[string]openpgp.EntityList, len(v.verifiers)) + for k, v := range v.verifiers { + remaining[k] = v + } + + var signedWith [][]byte + err := v.Store.Signatures(ctx, "", releaseDigest, func(ctx context.Context, signature []byte, errIn error) (done bool, err error) { + if errIn != nil { + klog.V(4).Infof("error retrieving signature for %s: %v", releaseDigest, errIn) + return false, nil + } + for k, keyring := range remaining { + content, _, err := verifySignatureWithKeyring(bytes.NewReader(signature), keyring) + if err != nil { + klog.V(4).Infof("keyring %q could not verify signature for %s: %v", k, releaseDigest, err) + continue + } + if err := verifyAtomicContainerSignature(content, releaseDigest); err != nil { + klog.V(4).Infof("signature for %s is not valid: %v", releaseDigest, err) + continue + } + delete(remaining, k) + signedWith = append(signedWith, signature) + } + return len(remaining) == 0, nil + }) + if err != nil { + klog.V(4).Infof("Failed to retrieve signatures for %s (should never happen)", releaseDigest) + return err + } + + if len(remaining) > 0 { + if klog.V(4).Enabled() { + for k := range remaining { + klog.Infof("Unable to verify %s against keyring %s", releaseDigest, k) + } + } + return fmt.Errorf("unable to locate a valid signature for one or more sources") + } + + v.cacheVerification(releaseDigest, signedWith) + + return nil +} + +// Signatures returns a copy of any cached signatures that have been validated +// so far. It may return no signatures. +func (v *ReleaseVerifier) Signatures() map[string][][]byte { + copied := make(map[string][][]byte) + v.lock.Lock() + defer v.lock.Unlock() + for k, v := range v.signatureCache { + copied[k] = v + } + return copied +} + +// hasVerified returns true if the digest has already been verified. +func (v *ReleaseVerifier) hasVerified(releaseDigest string) bool { + v.lock.Lock() + defer v.lock.Unlock() + _, ok := v.signatureCache[releaseDigest] + return ok +} + +const maxSignatureCacheSize = 64 + +// cacheVerification caches the result of signature check for a digest for later retrieval. +func (v *ReleaseVerifier) cacheVerification(releaseDigest string, signedWith [][]byte) { + v.lock.Lock() + defer v.lock.Unlock() + + if len(signedWith) == 0 || len(releaseDigest) == 0 || v.signatureCache == nil { + return + } + // remove the new entry + delete(v.signatureCache, releaseDigest) + // ensure the cache doesn't grow beyond our cap + for k := range v.signatureCache { + if len(v.signatureCache) < maxSignatureCacheSize { + break + } + delete(v.signatureCache, k) + } + v.signatureCache[releaseDigest] = signedWith +} + +type fileStore struct { + directory string +} + +// Signatures reads signatures as "signature-1", "signature-2", etc. out of a digest-based subdirectory. +func (s *fileStore) Signatures(ctx context.Context, name string, digest string, fn store.Callback) error { + digestPathSegment, err := util.DigestToKeyPrefix(digest, "=") + if err != nil { + return err + } + + base := filepath.Join(s.directory, digestPathSegment, "signature-") + for i := 1; i < maxSignatureSearch; i++ { + if err := ctx.Err(); err != nil { + return err + } + + path := base + strconv.Itoa(i) + data, err := ioutil.ReadFile(path) + if os.IsNotExist(err) { + break + } + if err != nil { + klog.V(4).Infof("unable to load signature: %v", err) + done, err := fn(ctx, nil, err) + if done || err != nil { + return err + } + continue + } + done, err := fn(ctx, data, nil) + if done || err != nil { + return err + } + } + return nil +} + +func (s *fileStore) String() string { + return fmt.Sprintf("file://%s", s.directory) +} + +// verifySignatureWithKeyring performs a containers/image verification of the provided signature +// message, checking for the integrity and authenticity of the provided message in r. It will return +// the identity of the signer if successful along with the message contents. +func verifySignatureWithKeyring(r io.Reader, keyring openpgp.EntityList) ([]byte, string, error) { + md, err := openpgp.ReadMessage(r, keyring, nil, nil) + if err != nil { + return nil, "", fmt.Errorf("could not read the message: %v", err) + } + if !md.IsSigned { + return nil, "", fmt.Errorf("not signed") + } + content, err := ioutil.ReadAll(md.UnverifiedBody) + if err != nil { + return nil, "", err + } + if md.SignatureError != nil { + return nil, "", fmt.Errorf("signature error: %v", md.SignatureError) + } + if md.SignedBy == nil { + return nil, "", fmt.Errorf("invalid signature") + } + if md.Signature != nil { + if md.Signature.SigLifetimeSecs != nil { + expiry := md.Signature.CreationTime.Add(time.Duration(*md.Signature.SigLifetimeSecs) * time.Second) + if time.Now().After(expiry) { + return nil, "", fmt.Errorf("signature expired on %s", expiry) + } + } + } else if md.SignatureV3 == nil { + return nil, "", fmt.Errorf("unexpected openpgp.MessageDetails: neither Signature nor SignatureV3 is set") + } + + // follow conventions in containers/image + return content, strings.ToUpper(fmt.Sprintf("%x", md.SignedBy.PublicKey.Fingerprint)), nil +} + +// An atomic container signature has the following schema: +// +// { +// "critical": { +// "type": "atomic container signature", +// "image": { +// "docker-manifest-digest": "sha256:817a12c32a39bbe394944ba49de563e085f1d3c5266eb8e9723256bc4448680e" +// }, +// "identity": { +// "docker-reference": "docker.io/library/busybox:latest" +// } +// }, +// "optional": { +// "creator": "some software package v1.0.1-35", +// "timestamp": 1483228800, +// } +// } +type signature struct { + Critical criticalSignature `json:"critical"` + Optional optionalSignature `json:"optional"` +} + +type criticalSignature struct { + Type string `json:"type"` + Image criticalImage `json:"image"` + Identity criticalIdentity `json:"identity"` +} + +type criticalImage struct { + DockerManifestDigest string `json:"docker-manifest-digest"` +} + +type criticalIdentity struct { + DockerReference string `json:"docker-reference"` +} + +type optionalSignature struct { + Creator string `json:"creator"` + Timestamp int64 `json:"timestamp"` +} + +// verifyAtomicContainerSignature verifiers that the provided data authenticates the +// specified release digest. If error is returned the provided data does NOT authenticate +// the release digest and the signature must be ignored. +func verifyAtomicContainerSignature(data []byte, releaseDigest string) error { + d := json.NewDecoder(bytes.NewReader(data)) + d.DisallowUnknownFields() + var sig signature + if err := d.Decode(&sig); err != nil { + return fmt.Errorf("the signature is not valid JSON: %v", err) + } + if sig.Critical.Type != "atomic container signature" { + return fmt.Errorf("signature is not the correct type") + } + if len(sig.Critical.Identity.DockerReference) == 0 { + return fmt.Errorf("signature must have an identity") + } + if sig.Critical.Image.DockerManifestDigest != releaseDigest { + return fmt.Errorf("signature digest does not match") + } + return nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index dbe6753d3f..be3d054dd4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -510,6 +510,12 @@ github.com/openshift/library-go/pkg/serviceability github.com/openshift/library-go/pkg/template/generator github.com/openshift/library-go/pkg/template/templateprocessing github.com/openshift/library-go/pkg/unidling/unidlingclient +github.com/openshift/library-go/pkg/verify +github.com/openshift/library-go/pkg/verify/store +github.com/openshift/library-go/pkg/verify/store/configmap +github.com/openshift/library-go/pkg/verify/store/parallel +github.com/openshift/library-go/pkg/verify/store/sigstore +github.com/openshift/library-go/pkg/verify/util # github.com/operator-framework/api v0.1.1 github.com/operator-framework/api/pkg/lib/version github.com/operator-framework/api/pkg/operators