Skip to content

Commit

Permalink
Merge branch 'main' into per
Browse files Browse the repository at this point in the history
  • Loading branch information
priteshbandi authored Feb 8, 2023
2 parents e1d18fa + 6ef3544 commit 15c4dad
Show file tree
Hide file tree
Showing 9 changed files with 235 additions and 23 deletions.
12 changes: 12 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,15 @@ func (e ErrorVerificationFailed) Error() string {
}
return "signature verification failed"
}

// ErrorUserMetadataVerificationFailed is used when the signature does not contain the user specified metadata
type ErrorUserMetadataVerificationFailed struct {
Msg string
}

func (e ErrorUserMetadataVerificationFailed) Error() string {
if e.Msg != "" {
return e.Msg
}
return "unable to find specified metadata in the signature"
}
8 changes: 5 additions & 3 deletions example_remoteSign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ func Example_remoteSign() {
exampleRepo := registry.NewRepository(remoteRepo)

// exampleSignOptions is an example of notation.SignOptions.
exampleSignOptions := notation.SignOptions{
ArtifactReference: exampleArtifactReference,
SignatureMediaType: exampleSignatureMediaType,
exampleSignOptions := notation.RemoteSignOptions{
SignOptions: notation.SignOptions{
ArtifactReference: exampleArtifactReference,
SignatureMediaType: exampleSignatureMediaType,
},
}

// remote sign core process
Expand Down
11 changes: 10 additions & 1 deletion internal/mock/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ var MockSaExpiredSigEnv []byte
//go:embed testdata/sa_plugin_sig_env.json
var MockSaPluginSigEnv []byte // extended attributes are "SomeKey":"SomeValue", "io.cncf.notary.verificationPlugin":"plugin-name"

//go:embed testdata/sig_env_with_metadata.json
var MockSigEnvWithMetadata []byte

//go:embed testdata/ca_incompatible_pluginver_sig_env_1.0.9.json
var MockCaIncompatiblePluginVerSigEnv_1_0_9 []byte

Expand Down Expand Up @@ -67,7 +70,7 @@ var (
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: SampleDigest,
Size: 528,
Annotations: nil,
Annotations: Annotations,
}
SigManfiestDescriptor = ocispec.Descriptor{
MediaType: "application/vnd.cncf.oras.artifact.manifest.v1+json",
Expand All @@ -92,6 +95,12 @@ var (
Critical: true,
Value: "SomeValue",
}
MetadataSigEnvDescriptor = ocispec.Descriptor{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: digest.Digest("sha256:5a07385af4e6b6af81b0ebfd435aedccdfa3507f0609c658209e1aba57159b2b"),
Size: 942,
Annotations: map[string]string{"io.wabbit-networks.buildId": "123", "io.wabbit-networks.buildTime": "1672944615"},
}
)

type Repository struct {
Expand Down
11 changes: 11 additions & 0 deletions internal/mock/testdata/sig_env_with_metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJhbm5vdGF0aW9ucyI6eyJpby53YWJiaXQtbmV0d29ya3MuYnVpbGRJZCI6IjEyMyIsImlvLndhYmJpdC1uZXR3b3Jrcy5idWlsZFRpbWUiOiIxNjcyOTQ0NjE1In0sImRpZ2VzdCI6InNoYTI1Njo1YTA3Mzg1YWY0ZTZiNmFmODFiMGViZmQ0MzVhZWRjY2RmYTM1MDdmMDYwOWM2NTgyMDllMWFiYTU3MTU5YjJiIiwibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsInNpemUiOjk0Mn19",
"protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDIzLTAxLTExVDEwOjAyOjU0LTA4OjAwIn0",
"header": {
"x5c": [
"MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExMTAwNTIxMloXDTIzMDExMjAwNTIxMlowWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANH4GCn0bO8LurJvnDh9F6E5iU8MydVw5bypnPlRpP3Mt9AmdWgBYhTegHT9DecA7smkLP3FAZG33Z9c1oxeZaeMnkWmiPGtuGQtXRHoj3+ioe4zH8LKYtCDW2uNs0xaDI1CldDXf4xZGa1mYqXVT1SeYXLwHf2dAL9q6FY98lYLax139PIwJwgEiod1hyIJyQZ2Zf9+IHe+v+Aja0wNLp/w4tO9Q5FR6VNhtmeGL/zPLD8chcj4iBzArsPyos2jBDUwogsEPTYoa6Rtn6IrUyrg4aJ8S3W0qGX7qGPeSY3wbsI63Q7XYQkRrD+cb1yvwt1+YhqN8nnvM/ujVtT+JfsCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQBs475D3dkDhjTksg+ff0zhu2MaO0UR0kVuW+7tLFkgGptfos7Z6WN4xsjpMOL44xYx3DIKHkPybTFFEr75TGsfXUFRjYRoXCYW6L72p53kzR27Im14xiELGQoIw9n0/7ajIh1j4qKg+jP7dNSGg5234QllmZZMiRWl1/X2UlE1TEgJP26vuLKsw0bPsmRPaxoKcAAQxSWuOG5gdpZVw2p08rEwsaleK2Hbh7rIQwyL7JOGrUMYyEXuF/gE72Az4NYBVlLYPE5up/Cuq4bhjpRZ4qmVTQfiDoyhn5gSCJh+1wVewbqS/DECRpKETHTCYtrfrnxsROOkB8jtaSp7vTLk"
],
"io.cncf.notary.signingAgent":"Notation/1.0.0"
},
"signature":"Fqe_cSgUlbYXKYz5K-O_iZobcmwUdQVaT_mPsI-fnp2ibsFbWOfokYS-DJboJJJEJyzDH41WWAi9Xxr_yieub3Eq9vD4TIz5iVm7oJxI-x92mqe3MhgeybIQDyivtChmb2ufwmr1bFCtj4girLeYc_kUVj_BZDIUYo8rlx8nyr6ucFsxK-YyNYez9ySeInWCGz-Lce4ySuXCopgiGB-lVAeDzpxBwQHVYacKfvhvoXJgmsw372dBYUVVOHbfK5PX04r2ArpysNpvlPT7iY3t6pUVsRniDNFQ1nh2t7ZttuG9qQMTrpeegAIVDJ4i-PZnLS_8LQmF07Z6rpU8e1E6_Q"
}
90 changes: 82 additions & 8 deletions notation.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"

"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-go/internal/envelope"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
Expand All @@ -22,6 +24,7 @@ import (
const annotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256"

var errDoneVerification = errors.New("done verification")
var reservedAnnotationPrefixes = [...]string{"io.cncf.notary"}

// SignOptions contains parameters for Signer.Sign.
type SignOptions struct {
Expand All @@ -44,6 +47,14 @@ type SignOptions struct {
SigningAgent string
}

// RemoteSignOptions contains parameters for notation.Sign.
type RemoteSignOptions struct {
SignOptions

// UserMetadata contains key-value pairs that are added to the signature payload
UserMetadata map[string]string
}

// Signer is a generic interface for signing an artifact.
// The interface allows signing with local or remote keys,
// and packing in various signature formats.
Expand All @@ -62,18 +73,18 @@ type signerAnnotation interface {
// Sign signs the artifact in the remote registry and push the signature to the
// remote.
// The descriptor of the sign content is returned upon sucessful signing.
func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts SignOptions) (ocispec.Descriptor, error) {
func Sign(ctx context.Context, signer Signer, repo registry.Repository, remoteOpts RemoteSignOptions) (ocispec.Descriptor, error) {
// Input validation for expiry duration
if opts.ExpiryDuration < 0 {
if remoteOpts.ExpiryDuration < 0 {
return ocispec.Descriptor{}, fmt.Errorf("expiry duration cannot be a negative value")
}

if opts.ExpiryDuration%time.Second != 0 {
if remoteOpts.ExpiryDuration%time.Second != 0 {
return ocispec.Descriptor{}, fmt.Errorf("expiry duration supports minimum granularity of seconds")
}

logger := log.GetLogger(ctx)
artifactRef := opts.ArtifactReference
artifactRef := remoteOpts.ArtifactReference
ref, err := orasRegistry.ParseReference(artifactRef)
if err != nil {
return ocispec.Descriptor{}, err
Expand All @@ -91,7 +102,12 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts Sig
logger.Infof("Resolved artifact tag `%s` to digest `%s` before signing", ref.Reference, targetDesc.Digest.String())
}

sig, signerInfo, err := signer.Sign(ctx, targetDesc, opts)
targetDesc, err = addUserMetadataToDescriptor(ctx, targetDesc, remoteOpts.UserMetadata)
if err != nil {
return ocispec.Descriptor{}, err
}

sig, signerInfo, err := signer.Sign(ctx, targetDesc, remoteOpts.SignOptions)
if err != nil {
return ocispec.Descriptor{}, err
}
Expand All @@ -107,8 +123,8 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts Sig
return ocispec.Descriptor{}, err
}
logger.Debugf("Generated annotations: %+v", annotations)
logger.Debugf("Pushing signature of artifact descriptor: %+v, signature media type: %v", targetDesc, opts.SignatureMediaType)
_, _, err = repo.PushSignature(ctx, opts.SignatureMediaType, sig, targetDesc, annotations)
logger.Debugf("Pushing signature of artifact descriptor: %+v, signature media type: %v", targetDesc, remoteOpts.SignatureMediaType)
_, _, err = repo.PushSignature(ctx, remoteOpts.SignatureMediaType, sig, targetDesc, annotations)
if err != nil {
logger.Error("Failed to push the signature")
return ocispec.Descriptor{}, ErrorPushSignatureFailed{Msg: err.Error()}
Expand All @@ -117,6 +133,32 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts Sig
return targetDesc, nil
}

func addUserMetadataToDescriptor(ctx context.Context, desc ocispec.Descriptor, userMetadata map[string]string) (ocispec.Descriptor, error) {
logger := log.GetLogger(ctx)

if desc.Annotations == nil && len(userMetadata) > 0 {
desc.Annotations = map[string]string{}
}

for k, v := range userMetadata {
logger.Debugf("Adding metadata %v=%v to annotations", k, v)

for _, reservedPrefix := range reservedAnnotationPrefixes {
if strings.HasPrefix(k, reservedPrefix) {
return desc, fmt.Errorf("error adding user metadata: metadata key %v has reserved prefix %v", k, reservedPrefix)
}
}

if _, ok := desc.Annotations[k]; ok {
return desc, fmt.Errorf("error adding user metadata: metadata key %v is already present in the target artifact", k)
}

desc.Annotations[k] = v
}

return desc, nil
}

// ValidationResult encapsulates the verification result (passed or failed)
// for a verification type, including the desired verification action as
// specified in the trust policy
Expand Down Expand Up @@ -155,6 +197,24 @@ type VerificationOutcome struct {
Error error
}

func (outcome *VerificationOutcome) UserMetadata() (map[string]string, error) {
if outcome.EnvelopeContent == nil {
return nil, errors.New("unable to find envelope content for verification outcome")
}

var payload envelope.Payload
err := json.Unmarshal(outcome.EnvelopeContent.Payload.Content, &payload)
if err != nil {
return nil, errors.New("failed to unmarshal the payload content in the signature blob to envelope.Payload")
}

if payload.TargetArtifact.Annotations == nil {
return map[string]string{}, nil
}

return payload.TargetArtifact.Annotations, nil
}

// VerifyOptions contains parameters for Verifier.Verify.
type VerifyOptions struct {
// ArtifactReference is the reference of the artifact that is been
Expand All @@ -168,6 +228,9 @@ type VerifyOptions struct {

// PluginConfig is a map of plugin configs.
PluginConfig map[string]string

// UserMetadata contains key-value pairs that must be present in the signature
UserMetadata map[string]string
}

// Verifier is a generic interface for verifying an artifact.
Expand All @@ -193,6 +256,9 @@ type RemoteVerifyOptions struct {
// will be processed for verification. If set to less than or equals
// to zero, an error will be returned.
MaxSignatureAttempts int

// UserMetadata contains key-value pairs that must be present in the signature
UserMetadata map[string]string
}

type skipVerifier interface {
Expand All @@ -212,6 +278,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, re
opts := VerifyOptions{
ArtifactReference: remoteOpts.ArtifactReference,
PluginConfig: remoteOpts.PluginConfig,
UserMetadata: remoteOpts.UserMetadata,
}

if skipChecker, ok := verifier.(skipVerifier); ok {
Expand Down Expand Up @@ -255,6 +322,8 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, re
errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("total number of signatures associated with an artifact should be less than: %d", remoteOpts.MaxSignatureAttempts)}
numOfSignatureProcessed := 0

var verificationFailedErr error = ErrorVerificationFailed{}

// get signature manifests
logger.Debug("Fetching signature manifests using referrers API")
err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error {
Expand Down Expand Up @@ -282,6 +351,11 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, re
logger.Error("Got nil outcome. Expecting non-nil outcome on verification failure")
return err
}

if _, ok := outcome.Error.(ErrorUserMetadataVerificationFailed); ok {
verificationFailedErr = outcome.Error
}

continue
}
// at this point, the signature is verified successfully. Add
Expand Down Expand Up @@ -315,7 +389,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, re
// Verification Failed
if len(verificationOutcomes) == 0 {
logger.Debugf("Signature verification failed for all the signatures associated with artifact %v", artifactDescriptor.Digest)
return ocispec.Descriptor{}, verificationOutcomes, ErrorVerificationFailed{}
return ocispec.Descriptor{}, verificationOutcomes, verificationFailedErr
}

// Verification Succeeded
Expand Down
57 changes: 46 additions & 11 deletions notation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

var expectedMetadata = map[string]string{"foo": "bar", "bar": "foo"}

func TestSignSuccess(t *testing.T) {
repo := mock.NewRepository()
testCases := []struct {
Expand All @@ -26,10 +28,10 @@ func TestSignSuccess(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(b *testing.T) {
opts := SignOptions{
ExpiryDuration: tc.dur,
ArtifactReference: mock.SampleArtifactUri,
}
opts := RemoteSignOptions{}
opts.ExpiryDuration = tc.dur
opts.ArtifactReference = mock.SampleArtifactUri

_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
if err != nil {
b.Fatalf("Sign failed with error: %v", err)
Expand All @@ -38,15 +40,15 @@ func TestSignSuccess(t *testing.T) {
}
}

func TestSignWithAnnotationsSuccess(t *testing.T) {
func TestSignSuccessWithUserMetadata(t *testing.T) {
repo := mock.NewRepository()
opts := RemoteSignOptions{}
opts.ArtifactReference = mock.SampleArtifactUri
opts.UserMetadata = expectedMetadata

opts := SignOptions{
ArtifactReference: mock.SampleArtifactUri,
}
_, err := Sign(context.Background(), &dummyPluginSigner{}, repo, opts)
_, err := Sign(context.Background(), &verifyMetadataSigner{}, repo, opts)
if err != nil {
t.Fatalf("Sign failed with error: %v", err)
t.Fatalf("error: %v", err)
}
}

Expand All @@ -61,7 +63,29 @@ func TestSignWithInvalidExpiry(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(b *testing.T) {
_, err := Sign(context.Background(), &dummySigner{}, repo, SignOptions{ExpiryDuration: tc.dur})
opts := RemoteSignOptions{}
opts.ExpiryDuration = tc.dur

_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
if err == nil {
b.Fatalf("Expected error but not found")
}
})
}
}

func TestSignWithInvalidUserMetadata(t *testing.T) {
repo := mock.NewRepository()
testCases := []struct {
name string
metadata map[string]string
}{
{"reservedAnnotationKey", map[string]string{reservedAnnotationPrefixes[0] + ".foo": "bar"}},
{"keyConflict", map[string]string{"key": "value2"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(b *testing.T) {
_, err := Sign(context.Background(), &dummySigner{}, repo, RemoteSignOptions{UserMetadata: tc.metadata})
if err == nil {
b.Fatalf("Expected error but not found")
}
Expand Down Expand Up @@ -235,6 +259,17 @@ func (s *dummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts Si
return []byte("ABC"), &signature.SignerInfo{}, nil
}

type verifyMetadataSigner struct{}

func (s *verifyMetadataSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignOptions) ([]byte, *signature.SignerInfo, error) {
for k, v := range expectedMetadata {
if desc.Annotations[k] != v {
return nil, nil, errors.New("expected metadata not present in descriptor")
}
}
return []byte("ABC"), &signature.SignerInfo{}, nil
}

type dummyPluginSigner struct{}

func (s *dummyPluginSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignOptions) ([]byte, *signature.SignerInfo, error) {
Expand Down
2 changes: 2 additions & 0 deletions signer/signer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,12 @@ func generateSigningContent(tsa *timestamptest.TSA) (ocispec.Descriptor, notatio
},
}
sOpts := notation.SignOptions{ExpiryDuration: 24 * time.Hour}

if tsa != nil {
tsaRoots := x509.NewCertPool()
tsaRoots.AddCert(tsa.Certificate())
}

return desc, sOpts
}

Expand Down
Loading

0 comments on commit 15c4dad

Please sign in to comment.