Skip to content

Commit

Permalink
wip: notation implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
phbelitz committed Jan 24, 2025
1 parent 2749a8c commit ccdd4f5
Show file tree
Hide file tree
Showing 12 changed files with 328 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
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
139 changes: 139 additions & 0 deletions internal/validator/notation/notation_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
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)
}

nv.Name = valData.Name
nv.Type = valData.Type
nv.Auth = valData.Auth
nv.TrustStore = &InMemoryTrustStore{
trustRoots: valData.TrustRoots,
}
return nil
}

func (nv *NotationValidator) ValidateImage(
ctx context.Context,
image *image.Image,
args policy.RuleOptions,
) (string, error) {

trustPolicy, err := nv.setUpTrustPolicy(image.Context().String(), 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 string,
args policy.RuleOptions,
) (*trustpolicy.Document, error) {
imtr := nv.TrustStore.(*InMemoryTrustStore)
trs, err := auth.GetTrustRoots([]string{args.TrustRoot}, imtr.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},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
},
TrustStores: utils.Map(
trs,
func(tr auth.TrustRoot) string {
return fmt.Sprintf("ca:%s", tr.Name)
},
),
TrustedIdentities: []string{"*"},
},
},
}, nil
}
41 changes: 41 additions & 0 deletions internal/validator/notation/trust_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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
truststore.X509TrustStore
}

func (imts *InMemoryTrustStore) GetCertificates(
ctx context.Context,
_ truststore.Type,
namedStore string,
) ([]*x509.Certificate, error) {
var certs []*x509.Certificate

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

return certs, nil
}
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
expected_msg: error during notation validation
- id: signed
txt: Testing signed image...
ref: ghcr.io/sse-secure-systems/testimage:notation-sign
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"
}

0 comments on commit ccdd4f5

Please sign in to comment.