Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eff0563
Merge branch 'notaryproject:main' into attestations-impl
byronchien Jan 14, 2023
e9cf5e1
Ignore envelope parsing error and explain in comment
Jan 17, 2023
7e1fcd1
use RemoteSignOptions and fix verify error
Jan 19, 2023
356a303
Use embedded struct
Jan 19, 2023
ed93b36
Add output flag for notation verify
Jan 23, 2023
f2004e7
plaintext => text, adds helper for printing objects
Jan 23, 2023
f2abaed
Beautify json, add behavior for warnings
Jan 24, 2023
f1e85af
Add result to output instead of warnings
Jan 24, 2023
cb6d2ed
initialize output object only when output is json
Jan 25, 2023
8e3d62e
Merge from main
Feb 6, 2023
17f4b82
some nits
Feb 6, 2023
b8367b8
Revert the merge but update the error messages for cleaner diff
Feb 6, 2023
00d8c1a
Merge branch 'main' into metadata-output
byronchien Feb 7, 2023
aa74179
Add e2e test for metadata output
Feb 8, 2023
5447ef8
Add SkippedByTrustPolicy result
Feb 8, 2023
8d52989
fix: add error handling for LoadConfigOnce() (#520)
JeyJeyGao Feb 8, 2023
0ff2455
Update go.mod for notation-go
Feb 8, 2023
51951af
feat: add support for signed user metadata in notation sign and verif…
byronchien Feb 8, 2023
be0a68a
Merge branch 'main' into metadata-output
byronchien Feb 8, 2023
b22ae06
Fix bad merge conflict resolution
Feb 8, 2023
053f0f8
Dont access value of default pointer if it is nil (#541)
priteshbandi Feb 8, 2023
d1c6526
Expect failure for E2E metadata test
Feb 8, 2023
4f573af
feat: support OCI image manifest (#509)
Feb 9, 2023
682d674
Assorted updates
Feb 9, 2023
c4a432f
Merge branch 'main' into metadata-output
byronchien Feb 9, 2023
79b3217
Assorted nits
Feb 9, 2023
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
14 changes: 14 additions & 0 deletions cmd/notation/internal/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package errors

// ErrorReferrersAPINotSupported is used when the target registry does not
// support the Referrers API
type ErrorReferrersAPINotSupported struct {
Msg string
}

func (e ErrorReferrersAPINotSupported) Error() string {
if e.Msg != "" {
return e.Msg
}
return "referrers API not supported"
}
6 changes: 4 additions & 2 deletions cmd/notation/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func addKey(ctx context.Context, opts *keyAddOpts) error {
// set log level
ctx = opts.LoggingFlagOpts.SetLoggerLevel(ctx)

pluginConfig, err := cmd.ParseFlagPluginConfig(opts.pluginConfig)
pluginConfig, err := cmd.ParseFlagMap(opts.pluginConfig, cmd.PflagPluginConfig.Name)
if err != nil {
return err
}
Expand Down Expand Up @@ -228,7 +228,9 @@ func deleteKeys(ctx context.Context, opts *keyDeleteOpts) error {
var deletedNames []string
var prevDefault string
exec := func(s *config.SigningKeys) error {
prevDefault = *s.Default
if s.Default != nil {
prevDefault = *s.Default
}
var err error
deletedNames, err = s.Remove(opts.names...)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/notation/key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestKeyAddCommand_BasicArgs(t *testing.T) {
if err := cmd.ParseFlags([]string{
"--plugin", expected.plugin,
"--id", expected.id,
"-c", "pluginconfig",
"--plugin-config", "pluginconfig",
expected.name}); err != nil {
t.Fatalf("Parse Flag failed: %v", err)
}
Expand Down
104 changes: 96 additions & 8 deletions cmd/notation/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,51 +8,102 @@ import (

"github.com/notaryproject/notation-go/log"
notationregistry "github.com/notaryproject/notation-go/registry"
notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors"
"github.com/notaryproject/notation/internal/trace"
"github.com/notaryproject/notation/internal/version"
loginauth "github.com/notaryproject/notation/pkg/auth"
"github.com/notaryproject/notation/pkg/configutil"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/errcode"
)

const zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"

func getSignatureRepository(ctx context.Context, opts *SecureFlagOpts, reference string) (notationregistry.Repository, error) {
ref, err := registry.ParseReference(reference)
if err != nil {
return nil, err
}

// generate notation repository
return getRepositoryClient(ctx, opts, ref)
remoteRepo, err := getRepositoryClient(ctx, opts, ref)
if err != nil {
return nil, err
}
return notationregistry.NewRepository(remoteRepo), nil
}

func getRegistryClient(ctx context.Context, opts *SecureFlagOpts, serverAddress string) (*remote.Registry, error) {
reg, err := remote.NewRegistry(serverAddress)
// getSignatureRepositoryForSign returns a registry.Repository for Sign.
// ociImageManifest denotes the type of manifest used to store signatures during
// Sign process.
// Setting ociImageManifest to true means using OCI image manifest and the
// Referrers tag schema.
// Otherwise, use OCI artifact manifest and requires the Referrers API.
func getSignatureRepositoryForSign(ctx context.Context, opts *SecureFlagOpts, reference string, ociImageManifest bool) (notationregistry.Repository, error) {
logger := log.GetLogger(ctx)
ref, err := registry.ParseReference(reference)
if err != nil {
return nil, err
}

reg.Client, reg.PlainHTTP, err = getAuthClient(ctx, opts, reg.Reference)
// generate notation repository
remoteRepo, err := getRepositoryClient(ctx, opts, ref)
if err != nil {
return nil, err
}
return reg, nil

// Notation enforces the following two paths during Sign process:
// 1. OCI artifact manifest uses the Referrers API
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#listing-referrers
// 2. OCI image manifest uses the Referrers Tag Schema
// Reference: https://github.com/opencontainers/distribution-spec/blob/v1.1.0-rc1/spec.md#referrers-tag-schema
if !ociImageManifest {
logger.Info("Use OCI artifact manifest and Referrers API to store signature")
// ping Referrers API
if err := pingReferrersAPI(ctx, remoteRepo); err != nil {
return nil, err
}
logger.Info("Successfully pinged Referrers API on target registry")
} else {
logger.Info("Use OCI image manifest and Referrers Tag Schema to store signature")
if err := remoteRepo.SetReferrersCapability(false); err != nil {
return nil, err
}
}
repositoryOpts := notationregistry.RepositoryOptions{
OCIImageManifest: ociImageManifest,
}
return notationregistry.NewRepositoryWithOptions(remoteRepo, repositoryOpts), nil
}

func getRepositoryClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Reference) (notationregistry.Repository, error) {
func getRepositoryClient(ctx context.Context, opts *SecureFlagOpts, ref registry.Reference) (*remote.Repository, error) {
authClient, plainHTTP, err := getAuthClient(ctx, opts, ref)
if err != nil {
return nil, err
}

remoteRepo := &remote.Repository{
return &remote.Repository{
Client: authClient,
Reference: ref,
PlainHTTP: plainHTTP,
}, nil
}

func getRegistryClient(ctx context.Context, opts *SecureFlagOpts, serverAddress string) (*remote.Registry, error) {
reg, err := remote.NewRegistry(serverAddress)
if err != nil {
return nil, err
}
return notationregistry.NewRepository(remoteRepo), nil

reg.Client, reg.PlainHTTP, err = getAuthClient(ctx, opts, reg.Reference)
if err != nil {
return nil, err
}
return reg, nil
}

func setHttpDebugLog(ctx context.Context, authClient *auth.Client) {
Expand Down Expand Up @@ -127,3 +178,40 @@ func getSavedCreds(ctx context.Context, serverAddress string) (auth.Credential,

return nativeStore.Get(serverAddress)
}

func pingReferrersAPI(ctx context.Context, remoteRepo *remote.Repository) error {
logger := log.GetLogger(ctx)
if err := remoteRepo.SetReferrersCapability(true); err != nil {
return err
}
var checkReferrerDesc ocispec.Descriptor
checkReferrerDesc.Digest = zeroDigest
// core process
err := remoteRepo.Referrers(ctx, checkReferrerDesc, "", func(referrers []ocispec.Descriptor) error {
return nil
})
if err != nil {
var errResp *errcode.ErrorResponse
if !errors.As(err, &errResp) || errResp.StatusCode != http.StatusNotFound {
return err
}
if isErrorCode(errResp, errcode.ErrorCodeNameUnknown) {
// The repository is not found in the target registry.
// This is triggered when putting signatures to an empty repository.
// For notation, this path should never be triggered.
return err
}
// A 404 returned by Referrers API indicates that Referrers API is
// not supported.
logger.Infof("failed to ping Referrers API with error: %v", err)
errMsg := "Target registry does not support the Referrers API. Try the flag `--signature-manifest image` to store signatures using OCI image manifest for backwards compatibility"
return notationerrors.ErrorReferrersAPINotSupported{Msg: errMsg}
}
return nil
}

// isErrorCode returns true if err is an Error and its Code equals to code.
func isErrorCode(err error, code string) bool {
var ec errcode.Error
return errors.As(err, &ec) && ec.Code == code
}
135 changes: 135 additions & 0 deletions cmd/notation/registry_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package main

import (
"context"
"errors"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"

notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors"
"oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/errcode"
)

func TestRegistry_pingReferrersAPI_Success(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{ "test": "TEST" }`))
return
}
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
repo, err := remote.NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
ctx := context.Background()
err = pingReferrersAPI(ctx, repo)
if err != nil {
t.Errorf("pingReferrersAPI() expected nil error, but got error: %v", err)
}
}

func TestRegistry_pingReferrersAPI_ReferrersAPINotSupported(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{ "errorresponse": { "method": "GET", "statuscode": 404 } }`))
return
}
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
repo, err := remote.NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
err = pingReferrersAPI(ctx, repo)
var errorReferrersAPINotSupported notationerrors.ErrorReferrersAPINotSupported
if err == nil || !errors.As(err, &errorReferrersAPINotSupported) {
t.Errorf("pingReferrersAPI() expected ErrorReferrersAPINotSupported, but got: %v", err)
}
}

func TestRegistry_pingReferrersAPI_Failed(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest {
w.WriteHeader(http.StatusOK)
return
}
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
repo, err := remote.NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
err = pingReferrersAPI(ctx, repo)
if err == nil {
t.Errorf("pingReferrersAPI expected to get error but got nil")
}
}

func TestRegistry_pingReferrersAPI_RepositoryNotFound(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet && r.URL.Path == "/v2/test/referrers/"+zeroDigest {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte(`{ "errors": [ { "code": "NAME_UNKNOWN", "message": "repository name not known to registry" } ] }`))
return
}
t.Errorf("unexpected access: %s %q", r.Method, r.URL)
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
uri, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("invalid test http server: %v", err)
}
ctx := context.Background()
expectedErr := errcode.Error{
Code: errcode.ErrorCodeNameUnknown,
Message: "repository name not known to registry",
}

repo, err := remote.NewRepository(uri.Host + "/test")
if err != nil {
t.Fatalf("NewRepository() error = %v", err)
}
repo.PlainHTTP = true
err = pingReferrersAPI(ctx, repo)
if err == nil {
t.Fatalf("pingReferrersAPI() expected error but got nil")
}
var ec errcode.Error
if !errors.As(err, &ec) {
t.Errorf("pingReferrersAPI() expected errcode.Error")
}
if !reflect.DeepEqual(ec, expectedErr) {
t.Errorf("pingReferrersAPI() expected error: %v, but got: %v", expectedErr, err)
}
}
Loading