From 13607ae0961f5eb2bf901d7cefd3f00bc6fba9f0 Mon Sep 17 00:00:00 2001 From: AdamKorcz Date: Fri, 7 Apr 2023 12:42:40 +0100 Subject: [PATCH] Verifier: Add fuzzer from cncf-fuzzing Signed-off-by: AdamKorcz --- go.mod | 3 + go.sum | 18 ++ verifier/fuzz_test.go | 493 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 514 insertions(+) create mode 100644 verifier/fuzz_test.go diff --git a/go.mod b/go.mod index 7fafa14c..523d4c3b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/notaryproject/notation-go go 1.20 require ( + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 github.com/go-ldap/ldap/v3 v3.4.5 github.com/notaryproject/notation-core-go v1.0.0 github.com/opencontainers/go-digest v1.0.0 @@ -21,3 +22,5 @@ require ( github.com/x448/float16 v0.8.4 // indirect golang.org/x/sync v0.3.0 // indirect ) + +replace github.com/AdaLogics/go-fuzz-headers => github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d diff --git a/go.sum b/go.sum index c8db3179..385d280b 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= @@ -11,8 +13,14 @@ github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-ldap/ldap/v3 v3.4.5 h1:ekEKmaDrpvR2yf5Nc/DClsGG9lAmdDixe44mLzlW5r8= github.com/go-ldap/ldap/v3 v3.4.5/go.mod h1:bMGIq3AGbytbaMwf8wdv5Phdxz0FWHTIYMSzyrYgnQs= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/notaryproject/notation-core-go v1.0.0 h1:FgOAihtFW4XU9JYyTzItg1xW3OaN4eCasw5Bp00Ydu4= github.com/notaryproject/notation-core-go v1.0.0/go.mod h1:eoHFJ2e6b31GZO9hckCms5kfXvHLTySvJ1QwRLB9ZCk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -45,6 +53,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -66,14 +75,23 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.28.1 h1:i+0O8k2NPBCPYaMB+uCkseEbawEt/eFaiRqUx8aB108= +k8s.io/apimachinery v0.28.1 h1:EJD40og3GizBSV3mkIoXQBsws32okPOy+MkRyzh6nPY= +k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= +k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= oras.land/oras-go/v2 v2.3.0 h1:lqX1aXdN+DAmDTKjiDyvq85cIaI4RkIKp/PghWlAGIU= oras.land/oras-go/v2 v2.3.0/go.mod h1:GeAwLuC4G/JpNwkd+bSZ6SkDMGaaYglt6YK2WvZP7uQ= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= diff --git a/verifier/fuzz_test.go b/verifier/fuzz_test.go new file mode 100644 index 00000000..bd1c2999 --- /dev/null +++ b/verifier/fuzz_test.go @@ -0,0 +1,493 @@ +// Copyright The Notary Project 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 verifier + +import ( + "context" + "fmt" + "os" + "strings" + "sync" + "testing" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + + notation "github.com/notaryproject/notation-go" + "github.com/notaryproject/notation-go/dir" + "github.com/notaryproject/notation-go/internal/file" + "github.com/notaryproject/notation-go/internal/mock" + "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/verifier/trustpolicy" + "github.com/notaryproject/notation-go/verifier/truststore" + + fuzz "github.com/AdaLogics/go-fuzz-headers" + orasRegistry "oras.land/oras-go/v2/registry" +) + +var ( + verificationLevels = map[int]string{ + 0: "strict", + 1: "permissive", + 2: "audit", + } + defaultVerificationLevel = "audit" + separators = []string{".", "/", "-"} + scopeChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789-_/.:[]@" +) + +func createScope(ff *fuzz.ConsumeFuzzer) (string, error) { + var b strings.Builder + var noOfChars int + noOfChars, err := ff.GetInt() + if err != nil { + return "domain.com/my/repository", nil + } + if noOfChars == 0 { + noOfChars = 10 + } + str, err := ff.GetStringFrom(scopeChars, noOfChars) + if err != nil { + return "domain.com/my/repository", nil + } + b.WriteString(str) + if !strings.Contains(str, ".") { + b.WriteString(".com/") + + noOfChars, err = ff.GetInt() + if noOfChars == 0 { + noOfChars = 10 + } + repoStr1, err := ff.GetStringFrom(scopeChars, noOfChars) + if err != nil { + return "domain.com/my/repository", nil + } + b.WriteString(repoStr1) + b.WriteString("/") + + noOfChars, err = ff.GetInt() + if noOfChars == 0 { + noOfChars = 10 + } + repoStr2, err := ff.GetStringFrom(scopeChars, noOfChars) + if err != nil { + return "domain.com/my/repository", nil + } + b.WriteString(repoStr2) + } + + return b.String(), nil + +} + +func createTrustPolicy(ff *fuzz.ConsumeFuzzer) (trustpolicy.TrustPolicy, error) { + var name string + var noOfChars int + var ( + wg sync.WaitGroup + m sync.Mutex + ) + noOfChars, err := ff.GetInt() + if err != nil || noOfChars == 0 { + noOfChars = 10 + } + name, err = ff.GetStringFrom(scopeChars, noOfChars) + if err != nil { + return trustpolicy.TrustPolicy{}, err + } + registryScopes := make([]string, 0) + // create trust stores according to the v2 specification + noOfRegistryScopes, err := ff.GetInt() + if err != nil { + return trustpolicy.TrustPolicy{}, err + } + noOfRegistryScopes = noOfRegistryScopes % 20 + if noOfRegistryScopes == 0 { + noOfRegistryScopes = 1 + } + for i := 0; i < noOfRegistryScopes%20; i++ { + wg.Add(1) + scopeBytes, err := ff.GetBytes() + if err != nil { + return trustpolicy.TrustPolicy{}, err + } + ff1 := fuzz.NewConsumer(scopeBytes) + go func(ff2 *fuzz.ConsumeFuzzer) { + defer wg.Done() + registryScope, err := createScope(ff2) + if err != nil { + return + } + m.Lock() + registryScopes = append(registryScopes, registryScope) + m.Unlock() + }(ff1) + } + wg.Wait() + + if len(registryScopes) == 0 { + return trustpolicy.TrustPolicy{}, fmt.Errorf("need at least one registryScope") + } + + trustStores := make([]string, 0) + var trustedIdentities []string + trustedIdentities = make([]string, 0) + + veriLevel, err := ff.GetInt() + if err != nil { + return trustpolicy.TrustPolicy{}, err + } + + verificationLevelName := verificationLevels[veriLevel%len(verificationLevels)] + if verificationLevelName != "skip" { + + // create trust stores according to the v2 specification + noOfTrustStores, err := ff.GetInt() + if err != nil { + return trustpolicy.TrustPolicy{}, err + } + noOfTrustStores = noOfTrustStores % 20 + if noOfTrustStores == 0 { + noOfTrustStores = 1 + } + for i := 0; i < noOfTrustStores%20; i++ { + wg.Add(1) + ff2Bytes, err := ff.GetBytes() + if err != nil { + return trustpolicy.TrustPolicy{}, err + } + ff2 := fuzz.NewConsumer(ff2Bytes) + go func(ff *fuzz.ConsumeFuzzer) { + defer wg.Done() + var b strings.Builder + addCA, err := ff.GetBool() + if err != nil { + return + } + + trustStoreLength, err := ff.GetInt() + if err != nil { + return + } + trustStoreName, err := ff.GetStringFrom(scopeChars, trustStoreLength) + if err != nil { + return + } + if !file.IsValidFileName(trustStoreName) { + return + } + + if addCA { + b.WriteString("ca") + } else { + b.WriteString("signingAuthority") + } + b.WriteString(":") + b.WriteString(trustStoreName) + m.Lock() + trustStores = append(trustStores, b.String()) + m.Unlock() + }(ff2) + } + wg.Wait() + + if len(trustStores) == 0 { + return trustpolicy.TrustPolicy{}, fmt.Errorf("Could not create truststores") + } + + // trusted identities + noOfTrustedIdentities, err := ff.GetInt() + if err != nil { + return trustpolicy.TrustPolicy{}, err + } + noOfTrustedIdentities = noOfTrustedIdentities % 20 + if noOfTrustedIdentities == 0 { + noOfTrustedIdentities = 1 + } + for i := 0; i < noOfTrustStores%20; i++ { + var b strings.Builder + addX509Subject, err := ff.GetBool() + if err != nil { + break + } + + var identityPrefix string + if addX509Subject { + identityPrefix = "x509.subject" + } else { + identityPrefix, err = ff.GetString() + if err != nil { + break + } + } + b.WriteString(identityPrefix) + b.WriteString(":") + identityValue, err := ff.GetString() + if err != nil { + if len(trustedIdentities) == 0 { + trustedIdentities = append(trustedIdentities, "x509.subject:C=US,ST=WA,O=MyOrg,CustomRDN=CustomValue") + } + break + } + if identityValue == "" { + continue + } + b.WriteString(identityValue) + trustedIdentities = append(trustedIdentities, b.String()) + } + if len(trustStores) == 0 { + return trustpolicy.TrustPolicy{}, fmt.Errorf("Invalid configurations") + } + } + + var sv *trustpolicy.SignatureVerification + + sv = &trustpolicy.SignatureVerification{} + err = ff.GenerateStruct(sv) + if err != nil || sv.VerificationLevel == trustpolicy.LevelSkip.Name { + sv = &trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelStrict.Name} + } + sv.VerificationLevel = verificationLevelName + return trustpolicy.TrustPolicy{ + Name: name, + RegistryScopes: registryScopes, + SignatureVerification: *sv, + TrustStores: trustStores, + TrustedIdentities: trustedIdentities, + }, nil +} + +func createTrustPolicies(ff *fuzz.ConsumeFuzzer) ([]trustpolicy.TrustPolicy, error) { + var policies []trustpolicy.TrustPolicy + policies = make([]trustpolicy.TrustPolicy, 0) + numberOfPolicies, err := ff.GetInt() + if err != nil { + return policies, err + } + numberOfPolicies = numberOfPolicies % 3 + if numberOfPolicies == 0 { + numberOfPolicies = 1 + } + var ( + m sync.Mutex + wg sync.WaitGroup + ) + for i := 0; i < numberOfPolicies; i++ { + policyBytes, err := ff.GetBytes() + if err != nil { + return policies, err + } + ff2 := fuzz.NewConsumer(policyBytes) + wg.Add(1) + go func(ff2 *fuzz.ConsumeFuzzer) { + defer wg.Done() + policy, err := createTrustPolicy(ff2) + if err == nil { + m.Lock() + policies = append(policies, policy) + m.Unlock() + } + return + }(ff2) + } + wg.Wait() + if len(policies) == 0 { + return policies, fmt.Errorf("Did not create any policies") + } + return policies, nil +} + +func createOptions(ff *fuzz.ConsumeFuzzer) (notation.VerifyOptions, error) { + pluginConfig := make(map[string]string) + err := ff.FuzzMap(&pluginConfig) + if err != nil { + return notation.VerifyOptions{}, err + } + maxSignatureAttempts, err := ff.GetInt() + if err != nil { + return notation.VerifyOptions{}, err + } + return notation.VerifyOptions{ + ArtifactReference: "", + PluginConfig: pluginConfig, + MaxSignatureAttempts: maxSignatureAttempts, + }, nil +} + +var ( + ts truststore.X509TrustStore + policyDocument = dummyPolicyDocument() + vv *verifier + skv *noSkipVerifier + mockRepo = mock.NewRepository() + counter = 0 +) + +type noSkipVerifier struct { + trustPolicyDoc *trustpolicy.Document + trustStore truststore.X509TrustStore + pluginManager plugin.Manager +} + +func (v *noSkipVerifier) Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts notation.VerifierVerifyOptions) (*notation.VerificationOutcome, error) { + return vv.Verify(ctx, desc, signature, opts) +} + +func init() { + os.Mkdir("fuzz-dir", 0750) + dir.UserConfigDir = "fuzz-dir" + ts = truststore.NewX509TrustStore(dir.ConfigFS()) + vv = &verifier{ + trustPolicyDoc: &policyDocument, + trustStore: ts, + pluginManager: mock.PluginManager{}, + } + skv = &noSkipVerifier{ + trustPolicyDoc: &policyDocument, + trustStore: ts, + pluginManager: mock.PluginManager{}, + } +} + +func getArtifactPathFromReference(artifactReference string) (string, error) { + i := strings.LastIndex(artifactReference, "@") + if i < 0 { + return "", fmt.Errorf("Wrong format") + } + artifactPath := artifactReference[:i] + return artifactPath, nil +} + +func FuzzVerify(f *testing.F) { + f.Fuzz(func(t *testing.T, policyDocBytes []byte) { + var ( + wg sync.WaitGroup + m sync.Mutex + artifactError error + policiesError error + optsError error + artifactRef string + policies []trustpolicy.TrustPolicy + opts notation.VerifyOptions + ) + ff := fuzz.NewConsumer(policyDocBytes) + artifactRef, err := ff.GetString() + if err != nil { + return + } + if !strings.Contains(artifactRef, "@") { + added, err := ff.GetString() + if err != nil { + return + } + var sb strings.Builder + sb.WriteString(artifactRef) + sb.WriteString("@") + sb.WriteString(added) + artifactRef = sb.String() + } + + policyBytes, err := ff.GetBytes() + if err != nil { + return + } + + optionsBytes, err := ff.GetBytes() + if err != nil { + return + } + + wg.Add(3) + go func(artifactRef string) { + defer wg.Done() + ref, err := orasRegistry.ParseReference(artifactRef) + if err != nil || ref.Reference == "" { + artifactError = fmt.Errorf("Invalid artifactRef") + return + } + }(artifactRef) + + ff2 := fuzz.NewConsumer(policyBytes) + go func(ff *fuzz.ConsumeFuzzer) { + defer wg.Done() + var err error + m.Lock() + defer m.Unlock() + policies, err = createTrustPolicies(ff) + if err != nil || len(policies) == 0 { + policiesError = fmt.Errorf("Could not create policies") + return + } + }(ff2) + + ff3 := fuzz.NewConsumer(optionsBytes) + go func(ff *fuzz.ConsumeFuzzer) { + defer wg.Done() + var err error + m.Lock() + defer m.Unlock() + opts, err = createOptions(ff) + if err != nil { + optsError = fmt.Errorf("Optserror") + return + } + }(ff3) + + wg.Wait() + if artifactError != nil || policiesError != nil || optsError != nil { + return + } + opts.ArtifactReference = artifactRef + + // MaxSignatureAttempts cannot be 0 or negative + if opts.MaxSignatureAttempts == 0 { + opts.MaxSignatureAttempts = 1 + } + + var newPolicies []trustpolicy.TrustPolicy + newPolicies = make([]trustpolicy.TrustPolicy, 0) + for i, p := range policies { + if i == 0 { + artifactPath, err := getArtifactPathFromReference(opts.ArtifactReference) + if err != nil { + return + } + rs := p.RegistryScopes + rs = append(rs, artifactPath) + newPolicies = append(newPolicies, trustpolicy.TrustPolicy{ + Name: p.Name, + RegistryScopes: p.RegistryScopes, + SignatureVerification: p.SignatureVerification, + TrustStores: p.TrustStores, + TrustedIdentities: p.TrustedIdentities, + }) + continue + } + newPolicies = append(newPolicies, p) + } + policies = newPolicies + + policyDoc := trustpolicy.Document{ + Version: "1.0", + TrustPolicies: policies, + } + err = policyDoc.Validate() + if err != nil { + t.Skip() + } + vv.trustPolicyDoc = &policyDoc + skv.trustPolicyDoc = &policyDoc + + _, _, _ = notation.Verify(context.Background(), skv, mockRepo, opts) + }) +}