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

feat: support OCI image manifest #509

Merged
merged 42 commits into from
Feb 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f6a5843
initial update
Two-Hearts Jan 11, 2023
10d65c9
Merge branch 'main' into image
Two-Hearts Jan 11, 2023
ac6e77c
Added OCI image manifest support
Two-Hearts Jan 12, 2023
9f6a893
Merge branch 'notaryproject:main' into image
Two-Hearts Jan 13, 2023
bb3b0e7
update
Two-Hearts Jan 13, 2023
fa601fc
Merge branch 'image' of https://github.com/patrickzheng200/notation i…
Two-Hearts Jan 13, 2023
29a342c
Merge branch 'notaryproject:main' into image
Two-Hearts Jan 16, 2023
c8c3cc0
fixed unit tests
Two-Hearts Jan 16, 2023
525e1f6
go mod tidy
Two-Hearts Jan 16, 2023
1130d22
go mod tidy
Two-Hearts Jan 16, 2023
be4d7bc
updated spec docs
Two-Hearts Jan 16, 2023
647fe0c
update spec
Two-Hearts Jan 16, 2023
0db62d5
updated per code review
Two-Hearts Jan 17, 2023
2152de6
Merge branch 'notaryproject:main' into image
Two-Hearts Jan 18, 2023
04723f2
update
Two-Hearts Jan 19, 2023
c249ef7
unit test
Two-Hearts Jan 19, 2023
812403e
update
Two-Hearts Jan 19, 2023
5e5b520
update spec
Two-Hearts Jan 19, 2023
314a7f4
update
Two-Hearts Jan 30, 2023
7fdb1b9
Merge branch 'notaryproject:main' into image
Two-Hearts Jan 31, 2023
becd1da
Merge branch 'notaryproject:main' into image
Two-Hearts Feb 2, 2023
fe171e1
updated per code review
Two-Hearts Feb 2, 2023
26c3fc1
Merge branch 'image' of https://github.com/patrickzheng200/notation i…
Two-Hearts Feb 2, 2023
48379ac
updated per code review
Two-Hearts Feb 2, 2023
455f1c4
updated per code review
Two-Hearts Feb 2, 2023
166c5f6
update
Two-Hearts Feb 2, 2023
c4b039c
update
Two-Hearts Feb 3, 2023
2f1c102
resolved conflicts
Two-Hearts Feb 6, 2023
50f3827
update
Two-Hearts Feb 6, 2023
2582c6f
Merge branch 'notaryproject:main' into image
Two-Hearts Feb 7, 2023
b81bb0a
updated per community discussion
Two-Hearts Feb 7, 2023
abadd8b
Merge branch 'image' of https://github.com/patrickzheng200/notation i…
Two-Hearts Feb 7, 2023
7a52d77
update
Two-Hearts Feb 7, 2023
216cd58
updated error messages
Two-Hearts Feb 7, 2023
e0888b9
updated dependency
Two-Hearts Feb 8, 2023
d5dbb29
update with unit tests
Two-Hearts Feb 8, 2023
eea7d3c
Merge branch 'notaryproject:main' into image
Two-Hearts Feb 8, 2023
6ac8e41
Merge branch 'image' of https://github.com/patrickzheng200/notation i…
Two-Hearts Feb 8, 2023
c8d2ce2
updated log
Two-Hearts Feb 8, 2023
d2fb356
update
Two-Hearts Feb 8, 2023
65f2fcb
updated per code review
Two-Hearts Feb 8, 2023
ef09e39
resolved conflicts
Two-Hearts 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"
}
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
Binary file removed cmd/notation/notation
Binary file not shown.
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