Skip to content

Commit

Permalink
feat: notation support
Browse files Browse the repository at this point in the history
Implements the notation signature verification for container images.

fixes #312
  • Loading branch information
phbelitz committed Feb 7, 2025
1 parent 2749a8c commit 9aa9ebc
Show file tree
Hide file tree
Showing 34 changed files with 1,077 additions and 3 deletions.
1 change: 1 addition & 0 deletions .github/workflows/107_integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ jobs:
"regular",
"notaryv1",
"cosign",
"notation",
"namespaced",
"deployment",
"pre-config",
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ connaisseur.yaml
*.key
*.pem
*.csr
!test/testdata/**/*.pem
9 changes: 9 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/gobwas/glob v0.2.3
github.com/google/go-containerregistry v0.20.3
github.com/iancoleman/strcase v0.3.0
github.com/notaryproject/notation-go v1.3.0
github.com/opencontainers/go-digest v1.0.0
github.com/prometheus/client_golang v1.20.5
github.com/prometheus/client_model v0.6.1
Expand All @@ -34,6 +35,7 @@ require (
k8s.io/api v0.32.1
k8s.io/apimachinery v0.32.1
k8s.io/client-go v0.32.1
oras.land/oras-go/v2 v2.5.0
)

require (
Expand All @@ -60,6 +62,7 @@ require (
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.0.0 // indirect
Expand Down Expand Up @@ -124,9 +127,11 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-chi/chi v4.1.2+incompatible // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
github.com/go-ldap/ldap/v3 v3.4.10 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
Expand Down Expand Up @@ -186,6 +191,9 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mozillazg/docker-credential-acr-helper v0.4.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/notaryproject/notation-core-go v1.2.0 // indirect
github.com/notaryproject/notation-plugin-framework-go v1.0.0 // indirect
github.com/notaryproject/tspclient-go v1.0.0 // indirect
github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/oleiade/reflections v1.1.0 // indirect
Expand Down Expand Up @@ -223,6 +231,7 @@ require (
github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/transparency-dev/merkle v0.0.2 // indirect
github.com/vbatts/tar-split v0.11.6 // indirect
github.com/veraison/go-cose v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/go-gitlab v0.109.0 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
Expand Down
76 changes: 76 additions & 0 deletions go.sum

Large diffs are not rendered by default.

27 changes: 26 additions & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ func TestValidateErrors(t *testing.T) {
},
},
},
"Type must be one of [static notaryv1 cosign]",
"Type must be one of [static notaryv1 cosign notation]",
},
{ // 5: validator type matches its Type field
Config{
Expand Down Expand Up @@ -508,6 +508,31 @@ func TestValidateErrors(t *testing.T) {
},
"Issuer must not be set if IssuerRegex is",
},
{ // 13: Verification level
Config{
Validators: []validator.Validator{
{
Name: "valName",
Type: "cosign",
SpecificValidator: static.StaticValidator{
Name: "valName",
Type: "static",
Approve: false,
},
},
},
Rules: []policy.Rule{
{
Pattern: "somePattern",
Validator: "valName",
With: policy.RuleOptions{
VerificationLevel: "invalid",
},
},
},
},
"VerificationLevel must be one of [strict permissive audit]",
},
}
for idx, tc := range testCases {
err := tc.cfg.validate()
Expand Down
2 changes: 1 addition & 1 deletion internal/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const (
StaticValidator = "static"
CosignValidator = "cosign"
NotaryV1Validator = "notaryv1"
NotaryV2Validator = "notaryv2"
NotationValidator = "notation"
)

const (
Expand Down
5 changes: 5 additions & 0 deletions internal/policy/rule.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@ type RuleOptions struct {
// or to insecurely skip its mutation and only validate
// that an image exists that would pass validation
ValidationMode string `yaml:"mode" validate:"omitempty,oneof=mutate insecureValidateOnly"`
// verification level used in notation validator
// see https://github.com/notaryproject/specifications/blob/v1.0.0/specs/trust-store-trust-policy.md#signature-verification-details for more information
// leaving out skip intentionally as this is already
// covered by static validators
VerificationLevel string `yaml:"verificationLevel" validate:"omitempty,oneof=strict permissive audit"`
}
173 changes: 173 additions & 0 deletions internal/validator/notation/notation_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package notation

import (
"connaisseur/internal/image"
"connaisseur/internal/policy"
"connaisseur/internal/utils"
"connaisseur/internal/validator/auth"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"

"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/verifier"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation-go/verifier/truststore"
"github.com/sirupsen/logrus"
"oras.land/oras-go/v2/registry/remote"
orasAuth "oras.land/oras-go/v2/registry/remote/auth"
)

type NotationValidator struct {
Name string `validate:"required"`
Type string `validate:"eq=notation"`
Auth auth.Auth
RootCA *x509.CertPool
TrustStore truststore.X509TrustStore
}

type NotationValidatorYaml struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Auth auth.Auth `yaml:"auth"`
Cert string `yaml:"cert"`
TrustRoots []auth.TrustRoot `yaml:"trustRoots"`
}

func (nv *NotationValidator) UnmarshalYAML(unmarshal func(interface{}) error) error {
var valData NotationValidatorYaml
if err := unmarshal(&valData); err != nil {
return err
}

if len(valData.TrustRoots) < 1 {
return fmt.Errorf("no trust roots provided for validator %s", valData.Name)
}

imts, err := NewInMemoryTrustStore(valData.TrustRoots)
if err != nil {
return fmt.Errorf("failed to create trust store: %s", err)
}

if valData.Cert != "" {
rootCA := x509.NewCertPool()
if !rootCA.AppendCertsFromPEM([]byte(valData.Cert)) {
return fmt.Errorf("failed to parse certificate")
}
nv.RootCA = rootCA
}

nv.Name = valData.Name
nv.Type = valData.Type
nv.Auth = valData.Auth
nv.TrustStore = imts

return nil
}

func (nv *NotationValidator) ValidateImage(
ctx context.Context,
image *image.Image,
args policy.RuleOptions,
) (string, error) {
trustPolicy, err := nv.setUpTrustPolicy(image, args)
if err != nil {
return "", fmt.Errorf("failed to set up trust policy: %s", err)
}

verifier, err := verifier.New(trustPolicy, nv.TrustStore, nil)
if err != nil {
return "", fmt.Errorf("failed to create verifier: %s", err)
}

remoteRepo, err := remote.NewRepository(image.Context().String())
if err != nil {
return "", fmt.Errorf("failed to create remote repository: %s", err)
}

client := orasAuth.DefaultClient
if nv.RootCA != nil {
client.Client = &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: nv.RootCA,
},
},
}
}

if authn := nv.Auth.LookUp(image.Context().Name()); authn.Username != "" &&
authn.Password != "" {
client.Credential = func(nv2_ctx context.Context, s string) (orasAuth.Credential, error) {
return orasAuth.Credential{
Username: authn.Username,
Password: authn.Password,
}, nil
}
}

remoteRepo.Client = client
remoteRegisty := registry.NewRepository(remoteRepo)

// notation needs digests for signature verification
// thus we resolve the digest if it is not set
if image.Digest() == "" {
desc, err := remoteRegisty.Resolve(ctx, image.Name())
if err != nil {
return "", fmt.Errorf("failed to resolve image tag: %s", err)
}
logrus.Debugf("resolved digest: %s", desc.Digest.String())
image.SetDigest(desc.Digest.String())
}

verifyOptions := notation.VerifyOptions{
ArtifactReference: fmt.Sprintf("%s@%s", image.Context().String(), image.Digest()),
MaxSignatureAttempts: 10,
}

notation_ctx := log.WithLogger(ctx, logrus.StandardLogger())
digest, _, err := notation.Verify(notation_ctx, verifier, remoteRegisty, verifyOptions)
if err != nil {
return "", fmt.Errorf("failed to verify image: %s", err)
}

return string(digest.Digest), nil
}

func (nv *NotationValidator) setUpTrustPolicy(
image *image.Image,
args policy.RuleOptions,
) (*trustpolicy.Document, error) {
imts := nv.TrustStore.(*InMemoryTrustStore)
trs, err := auth.GetTrustRoots([]string{args.TrustRoot}, imts.trustRoots, true)
if err != nil {
return nil, fmt.Errorf("failed to get trust roots: %s", err)
}

vl := args.VerificationLevel
if vl == "" {
vl = trustpolicy.LevelStrict.Name
}

return &trustpolicy.Document{
Version: "1.0",
TrustPolicies: []trustpolicy.TrustPolicy{
{
Name: "default",
RegistryScopes: []string{image.Context().String()},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: vl,
},
TrustStores: utils.Map(
trs,
func(tr auth.TrustRoot) string { return fmt.Sprintf("ca:%s", tr.Name) },
),
TrustedIdentities: []string{"*"},
},
},
}, nil
}
Loading

0 comments on commit 9aa9ebc

Please sign in to comment.