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
11 changes: 11 additions & 0 deletions errors.go
Expand Up @@ -47,3 +47,14 @@ func (e ErrorVerificationFailed) Error() string {
}
return "signature verification failed"
}

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"
}
7 changes: 3 additions & 4 deletions example_remoteSign_test.go
Expand Up @@ -46,10 +46,9 @@ func Example_remoteSign() {
exampleRepo := registry.NewRepository(remoteRepo)

// exampleSignOptions is an example of notation.SignOptions.
exampleSignOptions := notation.SignOptions{
ArtifactReference: exampleArtifactReference,
SignatureMediaType: exampleSignatureMediaType,
}
exampleSignOptions := notation.RemoteSignOptions{}
exampleSignOptions.ArtifactReference = exampleArtifactReference
exampleSignOptions.SignatureMediaType = exampleSignatureMediaType
byronchien marked this conversation as resolved.
Show resolved Hide resolved

// remote sign core process
// upon successful signing, descriptor of the sign content is returned and
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

var (
SampleArtifactUri = "registry.acme-rockets.io/software/net-monitor@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e"
SampleDigest = digest.Digest("sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e")
Expand All @@ -43,7 +46,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 @@ -62,6 +65,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"
}
89 changes: 84 additions & 5 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 @@ -56,18 +67,18 @@ type Signer 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 @@ -85,6 +96,20 @@ 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())
}

targetDesc, err = addUserMetadataToDescriptor(ctx, targetDesc, remoteOpts.UserMetadata)
if err != nil {
return ocispec.Descriptor{}, err
}

// opts to be passed in signer.Sign()
opts := SignOptions{
ArtifactReference: remoteOpts.ArtifactReference,
SignatureMediaType: remoteOpts.SignatureMediaType,
ExpiryDuration: remoteOpts.ExpiryDuration,
PluginConfig: remoteOpts.PluginConfig,
SigningAgent: remoteOpts.SigningAgent,
}
byronchien marked this conversation as resolved.
Show resolved Hide resolved

sig, signerInfo, err := signer.Sign(ctx, targetDesc, opts)
byronchien marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return ocispec.Descriptor{}, err
Expand All @@ -105,6 +130,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 @@ -156,6 +207,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 @@ -181,6 +235,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 @@ -200,6 +257,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 @@ -243,6 +301,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 @@ -270,6 +330,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 @@ -303,7 +368,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 All @@ -325,3 +390,17 @@ func generateAnnotations(signerInfo *signature.SignerInfo) (map[string]string, e
annotationX509ChainThumbprint: string(val),
}, nil
}

func (outcome *VerificationOutcome) GetUserMetadata() (map[string]string, error) {
byronchien marked this conversation as resolved.
Show resolved Hide resolved
var payload envelope.Payload
err := json.Unmarshal(outcome.EnvelopeContent.Payload.Content, &payload)
byronchien marked this conversation as resolved.
Show resolved Hide resolved
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
byronchien marked this conversation as resolved.
Show resolved Hide resolved
}
byronchien marked this conversation as resolved.
Show resolved Hide resolved
57 changes: 52 additions & 5 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,6 +40,18 @@ func TestSignSuccess(t *testing.T) {
}
}

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

_, err := Sign(context.Background(), &verifyMetadataSigner{}, repo, opts)
if err != nil {
t.Fatalf("error: %v", err)
}
}

func TestSignWithInvalidExpiry(t *testing.T) {
repo := mock.NewRepository()
testCases := []struct {
Expand All @@ -49,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 @@ -223,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 dummyVerifier struct {
TrustPolicyDoc *trustpolicy.Document
PluginManager plugin.Manager
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