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: add support for signed user metadata #242

Merged
merged 12 commits into from Feb 8, 2023
12 changes: 12 additions & 0 deletions errors.go
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 {
byronchien marked this conversation as resolved.
Show resolved Hide resolved
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
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
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
@@ -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
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
byronchien marked this conversation as resolved.
Show resolved Hide resolved
}

// 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
byronchien marked this conversation as resolved.
Show resolved Hide resolved
}

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
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
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