Skip to content

Commit 17d3525

Browse files
authored
feat: add local sign/list/verification for OCI layout directory (#595)
This PR adds local sign/list/verification for OCI image layout directory. For RC.4: 1. It only supports storing the generated signature into the target OCI layout directory. 2. It supports listing signatures within the OCI layout directory. 3. It only supports verifying signatures within the target OCI layout directory. This PR is based on spec PR: #601 (Merged). This PR is dependent on the corresponding notation-go PR: notaryproject/notation-go#288. Please review the notation-go PR first. Resolves #283. Both remote registry and oci-layout scenario are tested. E2E tests are also included. --------- Signed-off-by: Patrick Zheng <[email protected]>
1 parent 65b0a4c commit 17d3525

22 files changed

+478
-150
lines changed

cmd/notation/inspect.go

+6-12
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import (
77
"errors"
88
"fmt"
99
"os"
10-
"strings"
1110
"strconv"
11+
"strings"
1212
"time"
1313

1414
"github.com/notaryproject/notation-core-go/signature"
@@ -98,22 +98,16 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error {
9898

9999
// initialize
100100
reference := opts.reference
101-
sigRepo, err := getSignatureRepository(ctx, &opts.SecureFlagOpts, reference)
101+
sigRepo, err := getRemoteRepository(ctx, &opts.SecureFlagOpts, reference)
102102
if err != nil {
103103
return err
104104
}
105-
106-
manifestDesc, ref, err := getManifestDescriptor(ctx, &opts.SecureFlagOpts, reference, sigRepo)
105+
manifestDesc, resolvedRef, err := resolveReference(ctx, inputTypeRegistry, reference, sigRepo, func(ref string, manifestDesc ocispec.Descriptor) {
106+
fmt.Fprintf(os.Stderr, "Warning: Always inspect the artifact using digest(@sha256:...) rather than a tag(:%s) because resolved digest may not point to the same signed artifact, as tags are mutable.\n", ref)
107+
})
107108
if err != nil {
108109
return err
109110
}
110-
111-
// reference is a digest reference
112-
if err := ref.ValidateReferenceAsDigest(); err != nil {
113-
fmt.Fprintf(os.Stderr, "Warning: Always inspect the artifact using digest(@sha256:...) rather than a tag(:%s) because resolved digest may not point to the same signed artifact, as tags are mutable.\n", ref.Reference)
114-
ref.Reference = manifestDesc.Digest.String()
115-
}
116-
117111
output := inspectOutput{MediaType: manifestDesc.MediaType, Signatures: []signatureOutput{}}
118112
skippedSignatures := false
119113
err = sigRepo.ListSignatures(ctx, manifestDesc, func(signatureManifests []ocispec.Descriptor) error {
@@ -177,7 +171,7 @@ func runInspect(command *cobra.Command, opts *inspectOpts) error {
177171
return err
178172
}
179173

180-
err = printOutput(opts.outputFormat, ref.String(), output)
174+
err = printOutput(opts.outputFormat, resolvedRef, output)
181175
if err != nil {
182176
return err
183177
}

cmd/notation/internal/errors/errors.go

+13
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,16 @@ func (e ErrorReferrersAPINotSupported) Error() string {
1212
}
1313
return "referrers API not supported"
1414
}
15+
16+
// ErrorOCILayoutMissingReference is used when signing local content in oci
17+
// layout folder but missing input tag or digest.
18+
type ErrorOCILayoutMissingReference struct {
19+
Msg string
20+
}
21+
22+
func (e ErrorOCILayoutMissingReference) Error() string {
23+
if e.Msg != "" {
24+
return e.Msg
25+
}
26+
return "reference is missing either digest or tag"
27+
}

cmd/notation/list.go

+19-9
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,25 @@ import (
77

88
notationregistry "github.com/notaryproject/notation-go/registry"
99
"github.com/notaryproject/notation/internal/cmd"
10+
"github.com/notaryproject/notation/internal/experimental"
1011
"github.com/opencontainers/go-digest"
1112
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1213
"github.com/spf13/cobra"
13-
"oras.land/oras-go/v2/registry"
1414
)
1515

1616
type listOpts struct {
1717
cmd.LoggingFlagOpts
1818
SecureFlagOpts
1919
reference string
20+
ociLayout bool
21+
inputType inputType
2022
}
2123

2224
func listCommand(opts *listOpts) *cobra.Command {
2325
if opts == nil {
24-
opts = &listOpts{}
26+
opts = &listOpts{
27+
inputType: inputTypeRegistry, // remote registry by default
28+
}
2529
}
2630
cmd := &cobra.Command{
2731
Use: "list [flags] <reference>",
@@ -35,12 +39,20 @@ func listCommand(opts *listOpts) *cobra.Command {
3539
opts.reference = args[0]
3640
return nil
3741
},
42+
PreRunE: func(cmd *cobra.Command, args []string) error {
43+
if opts.ociLayout {
44+
opts.inputType = inputTypeOCILayout
45+
}
46+
return experimental.CheckFlagsAndWarn(cmd, "oci-layout")
47+
},
3848
RunE: func(cmd *cobra.Command, args []string) error {
3949
return runList(cmd.Context(), opts)
4050
},
4151
}
4252
opts.LoggingFlagOpts.ApplyFlags(cmd.Flags())
4353
opts.SecureFlagOpts.ApplyFlags(cmd.Flags())
54+
cmd.Flags().BoolVar(&opts.ociLayout, "oci-layout", false, "[Experimental] list signatures stored in OCI image layout")
55+
experimental.HideFlags(cmd, "oci-layout")
4456
return cmd
4557
}
4658

@@ -50,23 +62,21 @@ func runList(ctx context.Context, opts *listOpts) error {
5062

5163
// initialize
5264
reference := opts.reference
53-
sigRepo, err := getSignatureRepository(ctx, &opts.SecureFlagOpts, reference)
65+
sigRepo, err := getRepository(ctx, opts.inputType, reference, &opts.SecureFlagOpts)
5466
if err != nil {
5567
return err
5668
}
57-
manifestDesc, ref, err := getManifestDescriptor(ctx, &opts.SecureFlagOpts, reference, sigRepo)
69+
targetDesc, resolvedRef, err := resolveReference(ctx, opts.inputType, reference, sigRepo, nil)
5870
if err != nil {
5971
return err
6072
}
61-
6273
// print all signature manifest digests
63-
return printSignatureManifestDigests(ctx, manifestDesc, sigRepo, ref)
74+
return printSignatureManifestDigests(ctx, targetDesc, sigRepo, resolvedRef)
6475
}
6576

6677
// printSignatureManifestDigests returns the signature manifest digests of
6778
// the subject manifest.
68-
func printSignatureManifestDigests(ctx context.Context, manifestDesc ocispec.Descriptor, sigRepo notationregistry.Repository, ref registry.Reference) error {
69-
ref.Reference = manifestDesc.Digest.String()
79+
func printSignatureManifestDigests(ctx context.Context, targetDesc ocispec.Descriptor, sigRepo notationregistry.Repository, ref string) error {
7080
titlePrinted := false
7181
printTitle := func() {
7282
if !titlePrinted {
@@ -77,7 +87,7 @@ func printSignatureManifestDigests(ctx context.Context, manifestDesc ocispec.Des
7787
}
7888

7989
var prevDigest digest.Digest
80-
err := sigRepo.ListSignatures(ctx, manifestDesc, func(signatureManifests []ocispec.Descriptor) error {
90+
err := sigRepo.ListSignatures(ctx, targetDesc, func(signatureManifests []ocispec.Descriptor) error {
8191
for _, sigManifestDesc := range signatureManifests {
8292
if prevDigest != "" {
8393
// check and print title

cmd/notation/manifest.go

+100-15
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,119 @@ package main
33
import (
44
"context"
55
"errors"
6+
"fmt"
7+
"os"
8+
"strings"
9+
"unicode"
610

711
"github.com/notaryproject/notation-go/log"
812
notationregistry "github.com/notaryproject/notation-go/registry"
13+
notationerrors "github.com/notaryproject/notation/cmd/notation/internal/errors"
14+
"github.com/opencontainers/go-digest"
915
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
1016
"oras.land/oras-go/v2/registry"
1117
)
1218

13-
// getManifestDescriptor returns target artifact manifest descriptor and
14-
// registry.Reference given user input reference.
15-
func getManifestDescriptor(ctx context.Context, opts *SecureFlagOpts, reference string, sigRepo notationregistry.Repository) (ocispec.Descriptor, registry.Reference, error) {
16-
logger := log.GetLogger(ctx)
17-
19+
// resolveReference resolves user input reference based on user input type.
20+
// Returns the resolved manifest descriptor and resolvedRef in digest
21+
func resolveReference(ctx context.Context, inputType inputType, reference string, sigRepo notationregistry.Repository, fn func(string, ocispec.Descriptor)) (ocispec.Descriptor, string, error) {
22+
// sanity check
1823
if reference == "" {
19-
return ocispec.Descriptor{}, registry.Reference{}, errors.New("missing reference")
24+
return ocispec.Descriptor{}, "", errors.New("missing user input reference")
25+
}
26+
var tagOrDigestRef string
27+
var resolvedRef string
28+
switch inputType {
29+
case inputTypeRegistry:
30+
ref, err := registry.ParseReference(reference)
31+
if err != nil {
32+
return ocispec.Descriptor{}, "", fmt.Errorf("failed to resolve user input reference: %w", err)
33+
}
34+
tagOrDigestRef = ref.Reference
35+
resolvedRef = ref.Registry + "/" + ref.Repository
36+
case inputTypeOCILayout:
37+
layoutPath, layoutReference, err := parseOCILayoutReference(reference)
38+
if err != nil {
39+
return ocispec.Descriptor{}, "", fmt.Errorf("failed to resolve user input reference: %w", err)
40+
}
41+
layoutPathInfo, err := os.Stat(layoutPath)
42+
if err != nil {
43+
return ocispec.Descriptor{}, "", fmt.Errorf("failed to resolve user input reference: %w", err)
44+
}
45+
if !layoutPathInfo.IsDir() {
46+
return ocispec.Descriptor{}, "", errors.New("failed to resolve user input reference: input path is not a dir")
47+
}
48+
tagOrDigestRef = layoutReference
49+
resolvedRef = layoutPath
50+
default:
51+
return ocispec.Descriptor{}, "", fmt.Errorf("unsupported user inputType: %d", inputType)
2052
}
21-
ref, err := registry.ParseReference(reference)
53+
54+
manifestDesc, err := getManifestDescriptor(ctx, tagOrDigestRef, sigRepo)
2255
if err != nil {
23-
return ocispec.Descriptor{}, registry.Reference{}, err
56+
return ocispec.Descriptor{}, "", fmt.Errorf("failed to get manifest descriptor: %w", err)
57+
}
58+
resolvedRef = resolvedRef + "@" + manifestDesc.Digest.String()
59+
if _, err := digest.Parse(tagOrDigestRef); err == nil {
60+
// tagOrDigestRef is a digest reference
61+
return manifestDesc, resolvedRef, nil
2462
}
25-
if ref.Reference == "" {
26-
return ocispec.Descriptor{}, registry.Reference{}, errors.New("reference is missing digest or tag")
63+
// tagOrDigestRef is a tag reference
64+
if fn != nil {
65+
fn(tagOrDigestRef, manifestDesc)
2766
}
67+
return manifestDesc, resolvedRef, nil
68+
}
2869

29-
manifestDesc, err := sigRepo.Resolve(ctx, ref.Reference)
30-
if err != nil {
31-
return ocispec.Descriptor{}, registry.Reference{}, err
70+
// resolveArtifactDigestReference creates reference in Verification given user input
71+
// trust policy scope
72+
func resolveArtifactDigestReference(reference, policyScope string) string {
73+
if policyScope != "" {
74+
if _, digest, ok := strings.Cut(reference, "@"); ok {
75+
return policyScope + "@" + digest
76+
}
3277
}
78+
return reference
79+
}
3380

34-
logger.Infof("Reference %s resolved to manifest descriptor: %+v", ref.Reference, manifestDesc)
35-
return manifestDesc, ref, nil
81+
// parseOCILayoutReference parses the raw in format of <path>[:<tag>|@<digest>].
82+
// Returns the path to the OCI layout and the reference (tag or digest).
83+
func parseOCILayoutReference(raw string) (string, string, error) {
84+
var path string
85+
var ref string
86+
if idx := strings.LastIndex(raw, "@"); idx != -1 {
87+
// `digest` found
88+
path, ref = raw[:idx], raw[idx+1:]
89+
} else {
90+
// find `tag`
91+
idx := strings.LastIndex(raw, ":")
92+
if idx == -1 || (idx == 1 && len(raw) > 2 && unicode.IsLetter(rune(raw[0])) && raw[2] == '\\') {
93+
return "", "", notationerrors.ErrorOCILayoutMissingReference{}
94+
} else {
95+
path, ref = raw[:idx], raw[idx+1:]
96+
}
97+
}
98+
if path == "" {
99+
return "", "", fmt.Errorf("found empty file path in %q", raw)
100+
}
101+
if ref == "" {
102+
return "", "", fmt.Errorf("found empty reference in %q", raw)
103+
}
104+
return path, ref, nil
105+
}
106+
107+
// getManifestDescriptor returns target artifact manifest descriptor given
108+
// reference (digest or tag) and Repository.
109+
func getManifestDescriptor(ctx context.Context, reference string, sigRepo notationregistry.Repository) (ocispec.Descriptor, error) {
110+
logger := log.GetLogger(ctx)
111+
112+
if reference == "" {
113+
return ocispec.Descriptor{}, errors.New("reference cannot be empty")
114+
}
115+
manifestDesc, err := sigRepo.Resolve(ctx, reference)
116+
if err != nil {
117+
return ocispec.Descriptor{}, err
118+
}
119+
logger.Infof("Reference %s resolved to manifest descriptor: %+v", reference, manifestDesc)
120+
return manifestDesc, nil
36121
}

cmd/notation/registry.go

+48-4
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,53 @@ import (
2121
"oras.land/oras-go/v2/registry/remote/errcode"
2222
)
2323

24-
const zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
24+
// inputType denotes the user input type
25+
type inputType int
2526

26-
func getSignatureRepository(ctx context.Context, opts *SecureFlagOpts, reference string) (notationregistry.Repository, error) {
27+
const (
28+
inputTypeRegistry inputType = 1 + iota // inputType remote registry
29+
inputTypeOCILayout // inputType oci-layout
30+
)
31+
32+
const (
33+
zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
34+
)
35+
36+
// getRepository returns a notationregistry.Repository given user input type and
37+
// user input reference
38+
func getRepository(ctx context.Context, inputType inputType, reference string, opts *SecureFlagOpts) (notationregistry.Repository, error) {
39+
switch inputType {
40+
case inputTypeRegistry:
41+
return getRemoteRepository(ctx, opts, reference)
42+
case inputTypeOCILayout:
43+
layoutPath, _, err := parseOCILayoutReference(reference)
44+
if err != nil {
45+
return nil, err
46+
}
47+
return notationregistry.NewOCIRepository(layoutPath, notationregistry.RepositoryOptions{})
48+
default:
49+
return nil, errors.New("unsupported input type")
50+
}
51+
}
52+
53+
// getRepositoryForSign returns a notationregistry.Repository given user input
54+
// type and user input reference during Sign process
55+
func getRepositoryForSign(ctx context.Context, inputType inputType, reference string, opts *SecureFlagOpts, ociImageManifest bool) (notationregistry.Repository, error) {
56+
switch inputType {
57+
case inputTypeRegistry:
58+
return getRemoteRepositoryForSign(ctx, opts, reference, ociImageManifest)
59+
case inputTypeOCILayout:
60+
layoutPath, _, err := parseOCILayoutReference(reference)
61+
if err != nil {
62+
return nil, err
63+
}
64+
return notationregistry.NewOCIRepository(layoutPath, notationregistry.RepositoryOptions{OCIImageManifest: ociImageManifest})
65+
default:
66+
return nil, errors.New("unsupported input type")
67+
}
68+
}
69+
70+
func getRemoteRepository(ctx context.Context, opts *SecureFlagOpts, reference string) (notationregistry.Repository, error) {
2771
ref, err := registry.ParseReference(reference)
2872
if err != nil {
2973
return nil, err
@@ -37,13 +81,13 @@ func getSignatureRepository(ctx context.Context, opts *SecureFlagOpts, reference
3781
return notationregistry.NewRepository(remoteRepo), nil
3882
}
3983

40-
// getSignatureRepositoryForSign returns a registry.Repository for Sign.
84+
// getRemoteRepositoryForSign returns a registry.Repository for Sign.
4185
// ociImageManifest denotes the type of manifest used to store signatures during
4286
// Sign process.
4387
// Setting ociImageManifest to true means using OCI image manifest and the
4488
// Referrers tag schema.
4589
// Otherwise, use OCI artifact manifest and requires the Referrers API.
46-
func getSignatureRepositoryForSign(ctx context.Context, opts *SecureFlagOpts, reference string, ociImageManifest bool) (notationregistry.Repository, error) {
90+
func getRemoteRepositoryForSign(ctx context.Context, opts *SecureFlagOpts, reference string, ociImageManifest bool) (notationregistry.Repository, error) {
4791
logger := log.GetLogger(ctx)
4892
ref, err := registry.ParseReference(reference)
4993
if err != nil {

0 commit comments

Comments
 (0)