Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: notation implementation #1885

Open
wants to merge 4 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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.

2 changes: 1 addition & 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
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
138 changes: 138 additions & 0 deletions internal/validator/notation/notation_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package notation

import (
"connaisseur/internal/image"
"connaisseur/internal/policy"
"connaisseur/internal/utils"
"connaisseur/internal/validator/auth"
"context"
"fmt"

"github.com/notaryproject/notation-go"
"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
TrustStore truststore.X509TrustStore
}

type NotationValidatorYaml struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Auth auth.Auth `yaml:"auth"`
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)
}

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)
}

if authn := nv.Auth.LookUp(image.Context().Name()); authn.Username != "" &&
authn.Password != "" {
client := orasAuth.DefaultClient
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)

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,
}

digest, _, err := notation.Verify(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)
}

return &trustpolicy.Document{
Version: "1.0",
TrustPolicies: []trustpolicy.TrustPolicy{
{
Name: "default",
RegistryScopes: []string{image.Context().String()},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
},
Comment on lines +129 to +131
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about VerifyTimestamp? Without having looked at it this looks like something we want to have an opinion on

TrustStores: utils.Map(trs, func(tr auth.TrustRoot) string { return fmt.Sprintf("ca:%s", tr.Name) }),
TrustedIdentities: []string{"*"},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without knowing anything, this looks weird 🤔

},
},
}, nil
}
49 changes: 49 additions & 0 deletions internal/validator/notation/trust_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package notation

import (
"connaisseur/internal/validator/auth"
"context"
"crypto/x509"
"encoding/pem"
"fmt"

"github.com/notaryproject/notation-go/verifier/truststore"
)

type InMemoryTrustStore struct {
trustRoots []auth.TrustRoot
certs map[string][]*x509.Certificate
truststore.X509TrustStore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this used, isn't the interface already satisfied?

}

func NewInMemoryTrustStore(trustRoots []auth.TrustRoot) (*InMemoryTrustStore, error) {
certs := make(map[string][]*x509.Certificate)

for _, trustRoot := range trustRoots {
block, _ := pem.Decode([]byte(trustRoot.Cert))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return &InMemoryTrustStore{}, fmt.Errorf("failed to parse certificate for trust root %s: %w", trustRoot.Name, err)
}
certs[trustRoot.Name] = []*x509.Certificate{cert}
}

return &InMemoryTrustStore{
trustRoots: trustRoots,
certs: certs,
}, nil
}

func (imts *InMemoryTrustStore) GetCertificates(
ctx context.Context,
_ truststore.Type,
namedStore string,
) ([]*x509.Certificate, error) {
for name, certs := range imts.certs {
if name == namedStore {
return certs, nil
}
}

return nil, fmt.Errorf("no certificates found for trustRoot %s", namedStore)
}
5 changes: 4 additions & 1 deletion internal/validator/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"connaisseur/internal/policy"
cosign "connaisseur/internal/validator/cosignvalidator"
nv1 "connaisseur/internal/validator/notaryv1"
"connaisseur/internal/validator/notation"
static "connaisseur/internal/validator/staticvalidator"
"context"
"fmt"
Expand All @@ -15,7 +16,7 @@ type Validator struct {
// Name of the validator
Name string `validate:"required,eqcsfield=SpecificValidator.Name"`
// Type of the validator
Type string `validate:"oneof=static notaryv1 cosign,eqcsfield=SpecificValidator.Type"`
Type string `validate:"oneof=static notaryv1 cosign notation,eqcsfield=SpecificValidator.Type"`
// the specific validator (e.g. cosign, static)
SpecificValidator SpecificValidator `validate:"required"`
Validate
Expand Down Expand Up @@ -63,6 +64,8 @@ func (v *Validator) UnmarshalYAML(unmarshal func(interface{}) error) error {
specific = &cosign.CosignValidator{}
case constants.NotaryV1Validator:
specific = &nv1.NotaryV1Validator{}
case constants.NotationValidator:
specific = &notation.NotationValidator{}
default:
return fmt.Errorf("unsupported type \"%s\" for validator", v.Type)
}
Expand Down
5 changes: 5 additions & 0 deletions test/integration/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ case ${1:-} in
# testing cosign validator
test_case "cosign/install.yaml" "cosign/cases.yaml" "make"
;;
"notation")
# testing notation feature
source "${SCRIPT_PATH}"/notation/test.sh
notation_test
;;
"load")
# testing load
source "${SCRIPT_PATH}"/load/test.sh
Expand Down
8 changes: 8 additions & 0 deletions test/integration/notation/cases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# simple test cases for notation
- id: unsigned
txt: Testing unsigned image...
ref: ghcr.io/sse-secure-systems/testimage:notation-unsign
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ref: ghcr.io/sse-secure-systems/testimage:notation-unsign
ref: ghcr.io/sse-secure-systems/testimage:notation-unsigned

expected_msg: error during notation validation
- id: signed
txt: Testing signed image...
ref: ghcr.io/sse-secure-systems/testimage:notation-sign
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
ref: ghcr.io/sse-secure-systems/testimage:notation-sign
ref: ghcr.io/sse-secure-systems/testimage:notation-signed

33 changes: 33 additions & 0 deletions test/integration/notation/install.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
application:
validators:
- name: ghcr-notation
type: notation
trustRoots:
- name: default
cert: |
-----BEGIN CERTIFICATE-----
MIIDrjCCApagAwIBAgIUfA/t/J6eINSu566aAozkOjQKey4wDQYJKoZIhvcNAQEL
BQAwbDELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy
bGluMQwwCgYDVQQKDANTU0UxEjAQBgNVBAsMCURlZmVuc2l2ZTEZMBcGA1UEAwwQ
c2VjdXJlc3lzdGVtcy5kZTAgFw0yNTAxMjQxMjQzMTRaGA8yMTI0MTIzMTEyNDMx
NFowbDELMAkGA1UEBhMCREUxDzANBgNVBAgMBkJlcmxpbjEPMA0GA1UEBwwGQmVy
bGluMQwwCgYDVQQKDANTU0UxEjAQBgNVBAsMCURlZmVuc2l2ZTEZMBcGA1UEAwwQ
c2VjdXJlc3lzdGVtcy5kZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
AJSmGE+Knp0Qc2RqIkMys8e7X1F8zVbpVMmRxvkuZIrWb84Xb+XVeQgE1o5JQgPL
7PSgG6EHL/XjY1wKO2bwlC4AxLm3Cvo/r764yUTqGFoeImEsiT/BW8bFKndJYkP8
b8iHUkrRX7ZYkhjby91zwFzeewDb9dZPkqiV7npATOL/T5KSUVQ6uIIozX5GCj+b
B/iqZWB0bP33uqPEu+GUyYZudJlYe/Yv9aw8vioVXdoEHH10DtTosfXlub/Xd8bC
8a7qOBITpWJRrjWRwjaWgnKUlJxymhqU5Iudi57VgtkzD2AgRGLZEN27x67o5p0X
aN2O2cFDCCY/7DMlW965++sCAwEAAaNGMEQwDgYDVR0PAQH/BAQDAgeAMBMGA1Ud
JQQMMAoGCCsGAQUFBwMDMB0GA1UdDgQWBBSiqJeF6lNSF21K51ePceEtsIBPajAN
BgkqhkiG9w0BAQsFAAOCAQEAhaKvo0ofGKIoMNaCqv4qYCBsnXTLWqeRMzrxY3WP
RmGkiLoKivXP2ZL4R2igERW8IbXSDqC9u1to7ahwLiiM9Ikjik8I/x3EYJz3DAkz
eTDqS227EhSOOGo1G6f0ph/GPO4o71s8ek55Q92ZNrAqHwzGwsByGFbHcwABwtAA
1gqAB5luiuokUXhmlqkH46wbQLiVLYnetqIQ8uJiSnUFrWaKQSICnCY1kxptqepv
Vfgbd7LIriH+m57IWD2fil6qRXV7c0J6v+N5N7gf2TgDvRBxYRhHV+fQpxRWm+Ti
fyEquTrEhfXY4yfYWnpJ/EbjsHheqK+F4EacAqBTjhzW+g==
-----END CERTIFICATE-----
policy:
- pattern: "ghcr.io/sse-secure-systems/testimage:*"
validator: ghcr-notation
alerts: []
10 changes: 10 additions & 0 deletions test/integration/notation/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail

notation_test() {
update_with_file "notation/install.yaml"
update '(.application.validators[] | select(.name == "ghcr-notation") | .auth) += {"secretName": env(IMAGEPULLSECRET)}'
install "make"
multi_test "notation/cases.yaml"
uninstall "make"
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: missing EOF newline

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also for other files below

Loading