diff --git a/go.mod b/go.mod index a9720ffc..55e74f08 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,7 @@ go 1.18 require ( github.com/go-ldap/ldap/v3 v3.4.4 - github.com/golang-jwt/jwt/v4 v4.4.2 - github.com/notaryproject/notation-core-go v0.1.0-alpha.3 + github.com/notaryproject/notation-core-go v0.1.0-alpha.3.0.20220921054535-009c09a9628e github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.0.2 github.com/oras-project/artifacts-spec v1.0.0-rc.2 @@ -15,6 +14,7 @@ require ( require ( github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect + github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/opencontainers/distribution-spec/specs-go v0.0.0-20220620172159-4ab4752c3b86 // indirect golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect diff --git a/go.sum b/go.sum index 8a5bd96f..e7e8c882 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,8 @@ github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI= github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs= github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/notaryproject/notation-core-go v0.1.0-alpha.3 h1:gzB+h5TGzuocWiJxuYZgE/FwUIbJyKAHfk2hWSBbCGg= -github.com/notaryproject/notation-core-go v0.1.0-alpha.3/go.mod h1:Wfyh5SrQ718JegKPhTs7y74rXg86tWd5NfOx2uHK1nI= +github.com/notaryproject/notation-core-go v0.1.0-alpha.3.0.20220921054535-009c09a9628e h1:n3wJRhIVbEGg497rtKV3IMaZJv2hFKYHCOtNIOAyLYw= +github.com/notaryproject/notation-core-go v0.1.0-alpha.3.0.20220921054535-009c09a9628e/go.mod h1:mM4M9wPdu0CGgh8f3wOcu0XMiXwEKWQurjBG4nmqQ4g= github.com/opencontainers/distribution-spec/specs-go v0.0.0-20220620172159-4ab4752c3b86 h1:Oumw+lPnO8qNLTY2mrqPJZMoGExLi/0h/DdikoLTXVU= github.com/opencontainers/distribution-spec/specs-go v0.0.0-20220620172159-4ab4752c3b86/go.mod h1:aA4vdXRS8E1TG7pLZOz85InHi3BiPdErh8IpJN6E0x4= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/internal/mock/mocks.go b/internal/mock/mocks.go index 1efb882d..23b6fcdf 100644 --- a/internal/mock/mocks.go +++ b/internal/mock/mocks.go @@ -1,12 +1,10 @@ package mock import ( + "context" _ "embed" - nsigner "github.com/notaryproject/notation-core-go/signer" -) -import ( - "context" + "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/manager" @@ -54,7 +52,7 @@ var ( Size: 100, Annotations: Annotations, } - PluginExtendedCriticalAttribute = nsigner.Attribute{ + PluginExtendedCriticalAttribute = signature.Attribute{ Key: "SomeKey", Critical: true, Value: "SomeValue", diff --git a/notation.go b/notation.go index 6a69bf23..ce954603 100644 --- a/notation.go +++ b/notation.go @@ -9,8 +9,11 @@ import ( "github.com/opencontainers/go-digest" ) -// Media type for Notary payload for OCI artifacts, which contains an artifact descriptor. -const MediaTypePayload = "application/vnd.cncf.notary.payload.v1+json" +// MediaTypePayloadV1 is the supported content type for signature's payload. +const MediaTypePayloadV1 = "application/vnd.cncf.notary.payload.v1+json" + +// SigningAgent is the unprotected header field used by signature. +var SigningAgent = "Notation/1.0.0" // Descriptor describes the artifact that needs to be signed. type Descriptor struct { @@ -65,7 +68,12 @@ type Signer interface { } // VerifyOptions contains parameters for Verifier.Verify. -type VerifyOptions struct{} +type VerifyOptions struct { + // SignatureMediaType is the envelope type of the signature. + // Currently only `application/jose+json`` is supported. + // TODO: in the future, application/cose will also be supported. + SignatureMediaType string +} // Validate does basic validation on VerifyOptions. func (opts VerifyOptions) Validate() error { diff --git a/plugin/algorithm.go b/plugin/algorithm.go new file mode 100644 index 00000000..a6c1f324 --- /dev/null +++ b/plugin/algorithm.go @@ -0,0 +1,156 @@ +package plugin + +import ( + "errors" + + "github.com/notaryproject/notation-core-go/signature" +) + +// one of the following supported key spec names. +// +// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection +const ( + RSA_2048 = "RSA-2048" + RSA_3072 = "RSA-3072" + RSA_4096 = "RSA-4096" + EC_256 = "EC-256" + EC_384 = "EC-384" + EC_521 = "EC-521" +) + +// one of the following supported hash algorithm names. +// +// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection +const ( + SHA_256 = "SHA-256" + SHA_384 = "SHA-384" + SHA_512 = "SHA-512" +) + +// one of the following supported signing algorithm names. +// +// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection +const ( + ECDSA_SHA_256 = "ECDSA-SHA-256" + ECDSA_SHA_384 = "ECDSA-SHA-384" + ECDSA_SHA_512 = "ECDSA-SHA-512" + RSASSA_PSS_SHA_256 = "RSASSA-PSS-SHA-256" + RSASSA_PSS_SHA_384 = "RSASSA-PSS-SHA-384" + RSASSA_PSS_SHA_512 = "RSASSA-PSS-SHA-512" +) + +// KeySpecName returns the name of a keySpec according to the spec. +func KeySpecString(k signature.KeySpec) string { + switch k.Type { + case signature.KeyTypeEC: + switch k.Size { + case 256: + return EC_256 + case 384: + return EC_384 + case 521: + return EC_521 + } + case signature.KeyTypeRSA: + switch k.Size { + case 2048: + return RSA_2048 + case 3072: + return RSA_3072 + case 4096: + return RSA_4096 + } + } + return "" +} + +// KeySpecHashName returns the name of hash function according to the spec. +func KeySpecHashString(k signature.KeySpec) string { + switch k.Type { + case signature.KeyTypeEC: + switch k.Size { + case 256: + return SHA_256 + case 384: + return SHA_384 + case 521: + return SHA_512 + } + case signature.KeyTypeRSA: + switch k.Size { + case 2048: + return SHA_256 + case 3072: + return SHA_384 + case 4096: + return SHA_512 + } + } + return "" +} + +// ParseKeySpecFromName parses keySpec name to a signature.keySpec type. +func ParseKeySpec(raw string) (keySpec signature.KeySpec, err error) { + switch raw { + case RSA_2048: + keySpec.Size = 2048 + keySpec.Type = signature.KeyTypeRSA + case RSA_3072: + keySpec.Size = 3072 + keySpec.Type = signature.KeyTypeRSA + case RSA_4096: + keySpec.Size = 4096 + keySpec.Type = signature.KeyTypeRSA + case EC_256: + keySpec.Size = 256 + keySpec.Type = signature.KeyTypeEC + case EC_384: + keySpec.Size = 384 + keySpec.Type = signature.KeyTypeEC + case EC_521: + keySpec.Size = 521 + keySpec.Type = signature.KeyTypeEC + default: + keySpec = signature.KeySpec{} + err = errors.New("unknown key spec") + } + return +} + +// SigningAlgorithmName returns the signing algorithm name of an algorithm according to the spec. +func SigningAlgorithmString(alg signature.Algorithm) string { + switch alg { + case signature.AlgorithmES256: + return ECDSA_SHA_256 + case signature.AlgorithmES384: + return ECDSA_SHA_384 + case signature.AlgorithmES512: + return ECDSA_SHA_512 + case signature.AlgorithmPS256: + return RSASSA_PSS_SHA_256 + case signature.AlgorithmPS384: + return RSASSA_PSS_SHA_384 + case signature.AlgorithmPS512: + return RSASSA_PSS_SHA_512 + } + return "" +} + +// ParseSigningAlgorithFromName parses the signing algorithm name from a given string. +func ParseSigningAlgorithm(raw string) (signature.Algorithm, error) { + switch raw { + case ECDSA_SHA_256: + return signature.AlgorithmES256, nil + case ECDSA_SHA_384: + return signature.AlgorithmES384, nil + case ECDSA_SHA_512: + return signature.AlgorithmES512, nil + case RSASSA_PSS_SHA_256: + return signature.AlgorithmPS256, nil + case RSASSA_PSS_SHA_384: + return signature.AlgorithmPS384, nil + case RSASSA_PSS_SHA_512: + return signature.AlgorithmPS512, nil + } + return 0, errors.New("unknown signing algorithm") +} diff --git a/plugin/plugin.go b/plugin/plugin.go index 6416372b..f6b40eda 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -3,8 +3,6 @@ package plugin import ( "context" "time" - - "github.com/notaryproject/notation-core-go/signer" ) // Prefix is the prefix required on all plugin binary names. @@ -127,17 +125,17 @@ type DescribeKeyResponse struct { // One of following supported key types: // https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection - KeySpec signer.KeySpec `json:"keySpec"` + KeySpec string `json:"keySpec"` } // GenerateSignatureRequest contains the parameters passed in a generate-signature request. type GenerateSignatureRequest struct { - ContractVersion string `json:"contractVersion"` - KeyID string `json:"keyId"` - KeySpec signer.KeySpec `json:"keySpec"` - Hash signer.HashAlgorithm `json:"hashAlgorithm"` - Payload []byte `json:"payload"` - PluginConfig map[string]string `json:"pluginConfig,omitempty"` + ContractVersion string `json:"contractVersion"` + KeyID string `json:"keyId"` + KeySpec string `json:"keySpec"` + Hash string `json:"hashAlgorithm"` + Payload []byte `json:"payload"` + PluginConfig map[string]string `json:"pluginConfig,omitempty"` } func (GenerateSignatureRequest) Command() Command { @@ -146,9 +144,9 @@ func (GenerateSignatureRequest) Command() Command { // GenerateSignatureResponse is the response of a generate-signature request. type GenerateSignatureResponse struct { - KeyID string `json:"keyId"` - Signature []byte `json:"signature"` - SigningAlgorithm signer.SignatureAlgorithm `json:"signingAlgorithm"` + KeyID string `json:"keyId"` + Signature []byte `json:"signature"` + SigningAlgorithm string `json:"signingAlgorithm"` // Ordered list of certificates starting with leaf certificate // and ending with root certificate. diff --git a/signature/envelope.go b/signature/envelope.go new file mode 100644 index 00000000..7d036a43 --- /dev/null +++ b/signature/envelope.go @@ -0,0 +1,17 @@ +package signature + +import ( + "errors" + + "github.com/notaryproject/notation-core-go/signature" +) + +// ValidateEnvelopeMediaType validetes envelope media type is supported by notation-core-go. +func ValidateEnvelopeMediaType(mediaType string) error { + for _, types := range signature.RegisteredEnvelopeTypes() { + if mediaType == types { + return nil + } + } + return errors.New("invalid envelope media type") +} diff --git a/signature/envelope_test.go b/signature/envelope_test.go new file mode 100644 index 00000000..d228183d --- /dev/null +++ b/signature/envelope_test.go @@ -0,0 +1,46 @@ +package signature + +import ( + "errors" + "testing" + + "github.com/notaryproject/notation-core-go/signature/jws" +) + +const invalidMediaType = "invalid" + +func checkErrorEqual(expected, got error) bool { + if expected == nil && got == nil { + return true + } + if expected != nil && got != nil { + return expected.Error() == got.Error() + } + return false +} + +func TestValidateEnvelopeMediaType(t *testing.T) { + tests := []struct { + name string + mediaType string + expectedErr error + }{ + { + name: "jws signature media type", + mediaType: jws.MediaTypeEnvelope, + expectedErr: nil, + }, + { + name: "invalid media type", + mediaType: invalidMediaType, + expectedErr: errors.New("invalid envelope media type"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := ValidateEnvelopeMediaType(tt.mediaType); !checkErrorEqual(tt.expectedErr, err) { + t.Fatalf("expected validate envelope media type err: %v, got: %v", tt.expectedErr, err) + } + }) + } +} diff --git a/signature/plugin.go b/signature/plugin.go index 80760967..1d4459a0 100644 --- a/signature/plugin.go +++ b/signature/plugin.go @@ -8,33 +8,43 @@ import ( "fmt" "time" - "github.com/notaryproject/notation-core-go/signer" + "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/plugin" ) // pluginSigner signs artifacts and generates signatures. type pluginSigner struct { - runner plugin.Runner - keyID string - pluginConfig map[string]string + sigProvider provider + envelopeMediaType string + keyID string + pluginConfig map[string]string } -// NewSignerPlugin creates a notation.Signer that signs artifacts and generates JWS signatures +// NewSignerPlugin creates a notation.Signer that signs artifacts and generates signatures // by delegating the one or more operations to the named plugin, // as defined in // https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md#signing-interfaces. -func NewSignerPlugin(runner plugin.Runner, keyID string, pluginConfig map[string]string) (notation.Signer, error) { +func NewSignerPlugin(runner plugin.Runner, keyID string, pluginConfig map[string]string, envelopeMediaType string) (notation.Signer, error) { if runner == nil { return nil, errors.New("nil plugin runner") } if keyID == "" { return nil, errors.New("nil signing keyID") } - return &pluginSigner{runner, keyID, pluginConfig}, nil + + if err := ValidateEnvelopeMediaType(envelopeMediaType); err != nil { + return nil, err + } + return &pluginSigner{ + sigProvider: newExternalProvider(runner, keyID), + envelopeMediaType: envelopeMediaType, + keyID: keyID, + pluginConfig: pluginConfig, + }, nil } -// Sign signs the artifact described by its descriptor, and returns the signature. +// Sign signs the artifact described by its descriptor and returns the marshalled envelope. func (s *pluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts notation.SignOptions) ([]byte, error) { metadata, err := s.getMetadata(ctx) if err != nil { @@ -55,7 +65,7 @@ func (s *pluginSigner) Sign(ctx context.Context, desc notation.Descriptor, opts } func (s *pluginSigner) getMetadata(ctx context.Context) (*plugin.Metadata, error) { - out, err := s.runner.Run(ctx, new(plugin.GetMetadataRequest)) + out, err := s.sigProvider.Run(ctx, new(plugin.GetMetadataRequest)) if err != nil { return nil, fmt.Errorf("metadata command failed: %w", err) } @@ -75,7 +85,7 @@ func (s *pluginSigner) describeKey(ctx context.Context, config map[string]string KeyID: s.keyID, PluginConfig: config, } - out, err := s.runner.Run(ctx, req) + out, err := s.sigProvider.Run(ctx, req) if err != nil { return nil, fmt.Errorf("describe-key command failed: %w", err) } @@ -106,28 +116,32 @@ func (s *pluginSigner) generateSignature(ctx context.Context, desc notation.Desc return nil, fmt.Errorf("envelope payload can't be marshaled: %w", err) } - // Create plugin signature provider - psp := pluginSigProvider{ - runner: s.runner, - ctx: ctx, - keyID: s.keyID, - keySpec: key.KeySpec, - } - signReq := signer.SignRequest{ - Payload: payloadBytes, - PayloadContentType: signer.PayloadContentTypeV1, - SignatureProvider: psp, - SigningTime: time.Now(), - ExtendedSignedAttrs: nil, - SigningScheme: signer.SigningSchemeX509, - SigningAgent: "Notation/1.0.0", // TODO: include external signing plugin's name and version. https://github.com/notaryproject/notation-go/issues/80 + // for external plugin, pass keySpec and config before signing + if extProvider, ok := s.sigProvider.(*externalProvider); ok { + ks, err := plugin.ParseKeySpec(key.KeySpec) + if err != nil { + return nil, err + } + extProvider.prepareSigning(config, ks) + } + signReq := &signature.SignRequest{ + Payload: signature.Payload{ + ContentType: notation.MediaTypePayloadV1, + Content: payloadBytes, + }, + Signer: s.sigProvider, + SigningTime: time.Now(), + ExtendedSignedAttributes: nil, + SigningScheme: signature.SigningSchemeX509, + SigningAgent: notation.SigningAgent, // TODO: include external signing plugin's name and version. https://github.com/notaryproject/notation-go/issues/80 } + if !opts.Expiry.IsZero() { signReq.Expiry = opts.Expiry } // perform signing using pluginSigProvider - sigEnv, err := signer.NewSignatureEnvelope(signer.MediaTypeJWSJson) + sigEnv, err := signature.NewEnvelope(s.envelopeMediaType) if err != nil { return nil, err } @@ -137,9 +151,12 @@ func (s *pluginSigner) generateSignature(ctx context.Context, desc notation.Desc return nil, err } - _, verErr := sigEnv.Verify() + envContent, verErr := sigEnv.Verify() if verErr != nil { - return nil, fmt.Errorf("signature returned by generateSignature cannot be verified: %v", err) + return nil, fmt.Errorf("signature returned by generateSignature cannot be verified: %v", verErr) + } + if err := ValidatePayloadContentType(&envContent.Payload); err != nil { + return nil, err } // TODO: re-enable timestamping https://github.com/notaryproject/notation-go/issues/78 @@ -170,11 +187,11 @@ func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc notat ContractVersion: plugin.ContractVersion, KeyID: s.keyID, Payload: payloadBytes, - SignatureEnvelopeType: string(signer.MediaTypeJWSJson), - PayloadType: notation.MediaTypePayload, + SignatureEnvelopeType: s.envelopeMediaType, + PayloadType: notation.MediaTypePayloadV1, PluginConfig: s.mergeConfig(opts.PluginConfig), } - out, err := s.runner.Run(ctx, req) + out, err := s.sigProvider.Run(ctx, req) if err != nil { return nil, fmt.Errorf("generate-envelope command failed: %w", err) } @@ -191,18 +208,21 @@ func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc notat ) } - sigEnv, err := signer.NewSignatureEnvelopeFromBytes(resp.SignatureEnvelope, signer.MediaTypeJWSJson) + sigEnv, err := signature.ParseEnvelope(s.envelopeMediaType, resp.SignatureEnvelope) if err != nil { return nil, err } - sigInfo, err := sigEnv.Verify() + envContent, err := sigEnv.Verify() if err != nil { return nil, err } + if err := ValidatePayloadContentType(&envContent.Payload); err != nil { + return nil, err + } var signedPayload notation.Payload - if err = json.Unmarshal(sigInfo.Payload, &signedPayload); err != nil { + if err = json.Unmarshal(envContent.Payload.Content, &signedPayload); err != nil { return nil, fmt.Errorf("signed envelope payload can't be unmarshaled: %w", err) } @@ -240,49 +260,3 @@ func parseCertChain(certChain [][]byte) ([]*x509.Certificate, error) { } return certs, nil } - -type pluginSigProvider struct { - runner plugin.Runner - ctx context.Context - keyID string - keySpec signer.KeySpec - config map[string]string -} - -func (psp pluginSigProvider) Sign(bytes []byte) ([]byte, []*x509.Certificate, error) { - // Execute plugin sign command. - req := &plugin.GenerateSignatureRequest{ - ContractVersion: plugin.ContractVersion, - KeyID: psp.keyID, - KeySpec: psp.keySpec, - Hash: psp.keySpec.SignatureAlgorithm().Hash(), - Payload: bytes, - PluginConfig: psp.config, - } - - out, err := psp.runner.Run(psp.ctx, req) - if err != nil { - return nil, nil, fmt.Errorf("generate-signature command failed: %w", err) - } - - resp, ok := out.(*plugin.GenerateSignatureResponse) - if !ok { - return nil, nil, fmt.Errorf("plugin runner returned incorrect generate-signature response type '%T'", out) - } - - // Check keyID is honored. - if req.KeyID != resp.KeyID { - return nil, nil, fmt.Errorf("keyID in generateSignature response %q does not match request %q", resp.KeyID, req.KeyID) - } - - certs, err := parseCertChain(resp.CertificateChain) - if err != nil { - return nil, nil, err - } - - return resp.Signature, certs, nil -} - -func (psp pluginSigProvider) KeySpec() (signer.KeySpec, error) { - return psp.keySpec, nil -} diff --git a/signature/plugin_test.go b/signature/plugin_test.go index 63bb20e9..ef952f13 100644 --- a/signature/plugin_test.go +++ b/signature/plugin_test.go @@ -2,79 +2,231 @@ package signature import ( "context" - "crypto/rand" - "crypto/rsa" + "crypto" "crypto/x509" - "encoding/base64" "encoding/json" "errors" + "fmt" "reflect" "strings" "testing" "time" - "github.com/golang-jwt/jwt/v4" - "github.com/notaryproject/notation-core-go/signer" + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-core-go/signature/jws" + "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/plugin" ) -var validMetadata = plugin.Metadata{ - Name: "foo", Description: "friendly", Version: "1", URL: "example.com", - SupportedContractVersions: []string{plugin.ContractVersion}, - Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}, +const unsupported = "unsupported" + +var ( + validMetadata = plugin.Metadata{ + Name: "testPlugin", + Description: "plugin for test", + Version: "1.0", URL: "test.com", + SupportedContractVersions: []string{plugin.ContractVersion}, + Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}, + } + validSignDescriptor, validSignOpts = generateSigningContent(nil) + invalidJwsEnvelope, _ = json.Marshal(struct{}{}) + envelopeTypeToData = map[string][]byte{ + jws.MediaTypeEnvelope: invalidJwsEnvelope, + } + invalidSignatureEnvelope = []byte("invalid") +) + +var ( + validMetaDataWithEnvelopeGeneratorCapabilityFunc = func(ctx context.Context, req plugin.Request) (interface{}, error) { + metaData := validMetadata + metaData.Capabilities = []plugin.Capability{plugin.CapabilityEnvelopeGenerator} + return &metaData, nil + } +) + +var ( + defaultKeyCert *keyCertPair + defaultKeySpec signature.KeySpec +) + +func init() { + keyCertPairCollections = setUpKeyCertPairCollections() + defaultKeyCert = keyCertPairCollections[0] + defaultKeySpec, _ = signature.ExtractKeySpec(defaultKeyCert.certs[0]) +} + +type runnerOptions struct { + metaData runFunc + describeKey runFunc + generateSignature runFunc + generateEnvelope runFunc +} + +type options struct { + signFunc + keySpecFunc + runnerOptions +} + +type optionFunc func(*options) + +type signFunc func([]byte) ([]byte, []*x509.Certificate, error) + +func withSignFunc(f signFunc) optionFunc { + return func(o *options) { + o.signFunc = f + } +} + +type keySpecFunc func() (signature.KeySpec, error) + +func withKeySpecFunc(f keySpecFunc) optionFunc { + return func(o *options) { + o.keySpecFunc = f + } } -type mockRunner struct { - resp []interface{} - err []error - n int +type runFunc func(context.Context, plugin.Request) (interface{}, error) + +func withMetaData(f runFunc) optionFunc { + return func(o *options) { + o.metaData = f + } } -func (r *mockRunner) Run(_ context.Context, _ plugin.Request) (interface{}, error) { - defer func() { r.n++ }() - return r.resp[r.n], r.err[r.n] +func withDescribeKey(f runFunc) optionFunc { + return func(o *options) { + o.describeKey = f + } } -type mockSignerPlugin struct { - KeyID string - KeySpec signer.KeySpec - Sign func(payload []byte) []byte - Certs [][]byte - n int +func withGenerateSignature(f runFunc) optionFunc { + return func(o *options) { + o.generateSignature = f + } } -func (s *mockSignerPlugin) Run(_ context.Context, req plugin.Request) (interface{}, error) { - if req != nil { - // Test json roundtrip. - jsonReq, err := json.Marshal(req) +func withGenerateEnvelope(f runFunc) optionFunc { + return func(o *options) { + o.generateEnvelope = f + } +} + +type mockProvider struct { + options + keyID string + key crypto.PrivateKey + certs []*x509.Certificate + config map[string]string +} + +func (p *mockProvider) apply(opts ...optionFunc) *mockProvider { + for _, opt := range opts { + opt(&p.options) + } + return p +} + +func newMockProvider(key crypto.PrivateKey, certs []*x509.Certificate, keyID string, opts ...optionFunc) *mockProvider { + p := &mockProvider{ + key: key, + certs: certs, + keyID: keyID, + } + return p.apply(opts...) +} + +func (p *mockProvider) SetConfig(config map[string]string) { + p.config = config +} + +func (p *mockProvider) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { + if p.options.signFunc != nil { + return p.options.signFunc(payload) + } + keySpec, err := p.KeySpec() + if err != nil { + return nil, nil, err + } + sig, err := localSign(payload, keySpec.SignatureAlgorithm().Hash(), p.key) + return sig, p.certs, err +} + +func (p *mockProvider) KeySpec() (signature.KeySpec, error) { + if p.options.keySpecFunc != nil { + return p.options.keySpecFunc() + } + return signature.ExtractKeySpec(p.certs[0]) +} + +func (p *mockProvider) Run(ctx context.Context, req plugin.Request) (interface{}, error) { + switch req.Command() { + case plugin.CommandGetMetadata: + if p.metaData != nil { + return p.metaData(ctx, req) + } + return &validMetadata, nil + case plugin.CommandDescribeKey: + if p.describeKey != nil { + return p.describeKey(ctx, req) + } + keySpec, err := p.KeySpec() + if err != nil { + return nil, err + } + return &plugin.DescribeKeyResponse{ + KeyID: p.keyID, + KeySpec: plugin.KeySpecString(keySpec), + }, nil + case plugin.CommandGenerateSignature: + if p.generateSignature != nil { + return p.generateSignature(ctx, req) + } + r := req.(*plugin.GenerateSignatureRequest) + sig, _, err := p.Sign(r.Payload) if err != nil { return nil, err } - err = json.Unmarshal(jsonReq, req) + keySpec, err := p.KeySpec() if err != nil { return nil, err } - } - defer func() { s.n++ }() - switch s.n { - case 0: - return &validMetadata, nil - case 1: - return &plugin.DescribeKeyResponse{KeyID: s.KeyID, KeySpec: s.KeySpec}, nil - case 2: - var signed []byte - if s.Sign != nil { - signed = s.Sign(req.(*plugin.GenerateSignatureRequest).Payload) + var certs [][]byte + for _, cert := range p.certs { + certs = append(certs, cert.Raw) } return &plugin.GenerateSignatureResponse{ - KeyID: s.KeyID, - SigningAlgorithm: s.KeySpec.SignatureAlgorithm(), - Signature: signed, - CertificateChain: s.Certs, + KeyID: p.keyID, + Signature: sig, + SigningAlgorithm: plugin.SigningAlgorithmString(keySpec.SignatureAlgorithm()), + CertificateChain: certs, }, nil + case plugin.CommandGenerateEnvelope: + if p.generateEnvelope != nil { + return p.generateEnvelope(ctx, req) + } + return nil, fmt.Errorf("command %q is not supported", req.Command()) + } + return nil, plugin.RequestError{ + Code: plugin.ErrorCodeGeneric, + Err: fmt.Errorf("command %q is not supported", req.Command()), + } +} + +func newDefaultMockProvider(opts ...optionFunc) *mockProvider { + return newMockProvider(defaultKeyCert.key, defaultKeyCert.certs, "", opts...) +} + +func newTestBuiltInProvider(keyCertPair *keyCertPair) provider { + if keyCertPair == nil { + keyCertPair = defaultKeyCert } - panic("too many calls") + p, err := newBuiltinProvider(keyCertPair.key, keyCertPair.certs) + if err != nil { + panic(fmt.Sprintf("create builtin provider failed: %v", err)) + } + return p } func testSignerError(t *testing.T, signer pluginSigner, wantEr string) { @@ -86,341 +238,592 @@ func testSignerError(t *testing.T, signer pluginSigner, wantEr string) { } func TestSigner_Sign_RunMetadataFails(t *testing.T) { - signer := pluginSigner{ - runner: &mockRunner{[]interface{}{nil}, []error{errors.New("failed")}, 0}, - } - testSignerError(t, signer, "metadata command failed") -} + t.Run("run metadata command failed", func(t *testing.T) { + p := newDefaultMockProvider( + withMetaData(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return nil, errors.New("metadata command fail") + }), + ) + signer := pluginSigner{ + sigProvider: p, + } + testSignerError(t, signer, "metadata command failed") + }) -func TestSigner_Sign_NoCapability(t *testing.T) { - m := validMetadata - m.Capabilities = []plugin.Capability{""} - signer := pluginSigner{ - runner: &mockRunner{[]interface{}{&m}, []error{nil}, 0}, - } - testSignerError(t, signer, "does not have signing capabilities") + t.Run("no capability", func(t *testing.T) { + m := validMetadata + m.Capabilities = []plugin.Capability{""} + p := newDefaultMockProvider( + withMetaData(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return &m, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + } + testSignerError(t, signer, "does not have signing capabilities") + }) + + t.Run("metadata response type error", func(t *testing.T) { + p := newDefaultMockProvider( + withMetaData(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return struct{}{}, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + } + testSignerError(t, signer, "plugin runner returned incorrect get-plugin-metadata response type") + }) + + t.Run("invalid metadata", func(t *testing.T) { + p := newDefaultMockProvider( + withMetaData(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return &plugin.Metadata{}, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + } + testSignerError(t, signer, "invalid plugin metadata") + }) + + t.Run("plugin contract not supported", func(t *testing.T) { + p := newDefaultMockProvider( + withMetaData(func(ctx context.Context, r plugin.Request) (interface{}, error) { + metaData := validMetadata + metaData.SupportedContractVersions = []string{unsupported} + return &metaData, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + } + testSignerError(t, signer, fmt.Sprintf("contract version %q is not in the list of the plugin supported versions %v", plugin.ContractVersion, []string{unsupported})) + }) } func TestSigner_Sign_DescribeKeyFailed(t *testing.T) { - signer := pluginSigner{ - runner: &mockRunner{[]interface{}{&validMetadata, nil}, []error{nil, errors.New("failed")}, 0}, - } - testSignerError(t, signer, "describe-key command failed") + t.Run("run describe-key command failed", func(t *testing.T) { + p := newDefaultMockProvider( + withDescribeKey(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return nil, errors.New("describle-key command failed") + }), + ) + signer := pluginSigner{ + sigProvider: p, + } + testSignerError(t, signer, "describe-key command failed") + }) + + t.Run("describe-key response type error", func(t *testing.T) { + p := newDefaultMockProvider( + withDescribeKey(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return struct{}{}, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + } + testSignerError(t, signer, "plugin runner returned incorrect describe-key response type") + }) } func TestSigner_Sign_DescribeKeyKeyIDMismatch(t *testing.T) { + reqKeyID, respKeyID := "1", "2" + p := newDefaultMockProvider() + p.keyID = respKeyID signer := pluginSigner{ - runner: &mockSignerPlugin{KeyID: "2", KeySpec: signer.RSA_2048}, - keyID: "1", + sigProvider: p, + keyID: reqKeyID, } - testSignerError(t, signer, "keyID in describeKey response \"2\" does not match request \"1\"") + testSignerError(t, signer, fmt.Sprintf("keyID in describeKey response %q does not match request %q", respKeyID, reqKeyID)) } -func TestSigner_Sign_KeySpecNotSupported(t *testing.T) { +func TestSigner_Sign_EnvelopeNotSupported(t *testing.T) { signer := pluginSigner{ - runner: &mockSignerPlugin{KeyID: "1", KeySpec: "custom"}, - keyID: "1", + sigProvider: newDefaultMockProvider(), + envelopeMediaType: unsupported, } - testSignerError(t, signer, "signature algorithm \"\" is not supported") + testSignerError(t, signer, fmt.Sprintf("signature envelope format with media type %q is not supported", signer.envelopeMediaType)) } -func TestSigner_Sign_PayloadNotValid(t *testing.T) { - signer := pluginSigner{ - runner: &mockRunner{[]interface{}{ - &validMetadata, - &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: signer.RSA_2048}, - }, []error{nil, nil}, 0}, - keyID: "1", - } - _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{Expiry: time.Now().Add(-100)}) - wantEr := "expiry cannot be equal or before the signing time" - if err == nil || !strings.Contains(err.Error(), wantEr) { - t.Errorf("Signer.Sign() error = %v, wantErr %v", err, wantEr) +func TestSigner_Sign_KeySpecMisMatchCertChain(t *testing.T) { + // default keySpec would be RSA_2048 + misMatchKeySpec, _ := signature.ExtractKeySpec(keyCertPairCollections[1].certs[0]) + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + p := newDefaultMockProvider( + withKeySpecFunc(func() (signature.KeySpec, error) { + return misMatchKeySpec, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + envelopeMediaType: envelopeType, + } + testSignerError(t, signer, "mismatch between signature algorithm derived from signing certificate") + }) } } -func TestSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) { - signer := pluginSigner{ - runner: &mockRunner{[]interface{}{ - &validMetadata, - &plugin.DescribeKeyResponse{KeyID: "1", KeySpec: signer.RSA_2048}, - &plugin.GenerateSignatureResponse{KeyID: "2"}, - }, []error{nil, nil, nil}, 0}, - keyID: "1", +func TestSigner_Sign_ExpiryInValid(t *testing.T) { + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + signer := pluginSigner{ + sigProvider: newDefaultMockProvider(), + envelopeMediaType: envelopeType, + } + _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{Expiry: time.Now().Add(-100)}) + wantEr := "expiry cannot be equal or before the signing time" + if err == nil || !strings.Contains(err.Error(), wantEr) { + t.Errorf("Signer.Sign() error = %v, wantErr %v", err, wantEr) + } + }) } - testSignerError(t, signer, "keyID in generateSignature response \"2\" does not match request \"1\"") } -func TestSigner_Sign_UnsuportedKeySpec(t *testing.T) { - _, cert, _ := generateKeyCertPair() - signer := pluginSigner{ - runner: &mockSignerPlugin{KeyID: "1", KeySpec: "", Certs: getBytes(cert)}, - keyID: "1", +func TestSigner_Sign_GenerateSignatureKeyIDMismatch(t *testing.T) { + reqKeyID, respKeyID := "1", "2" + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + extRunner := newDefaultMockProvider( + withGenerateSignature(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return &plugin.GenerateSignatureResponse{ + KeyID: respKeyID, + }, nil + }), + ) + extRunner.keyID = reqKeyID + signer := pluginSigner{ + sigProvider: newExternalProvider(extRunner, reqKeyID), + envelopeMediaType: envelopeType, + keyID: reqKeyID, + } + testSignerError(t, signer, fmt.Sprintf("keyID in generateSignature response %q does not match request %q", respKeyID, reqKeyID)) + }) } - testSignerError(t, signer, "signature algorithm \"\" is not supported") } func TestSigner_Sign_NoCertChain(t *testing.T) { - signer := pluginSigner{ - runner: &mockSignerPlugin{ - KeyID: "1", - KeySpec: signer.RSA_2048, - }, - keyID: "1", + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + p := newMockProvider( + defaultKeyCert.key, + defaultKeyCert.certs, + "", + withSignFunc(func(b []byte) ([]byte, []*x509.Certificate, error) { + sig, err := localSign(b, defaultKeySpec.SignatureAlgorithm().Hash(), defaultKeyCert.key) + if err != nil { + return nil, nil, err + } + return sig, nil, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + envelopeMediaType: envelopeType, + } + if _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}); err == nil { + t.Errorf("Signer.Sign() expect error") + } + }) } - testSignerError(t, signer, "certificate-chain not present or is empty") } -func TestSigner_Sign_MalformedCert(t *testing.T) { - signer := pluginSigner{ - runner: &mockSignerPlugin{ - KeyID: "1", - KeySpec: signer.RSA_2048, - Certs: [][]byte{[]byte("mocked")}, - }, - keyID: "1", +func TestSigner_Sign_InvalidCertChain(t *testing.T) { + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + p := newMockProvider( + defaultKeyCert.key, + defaultKeyCert.certs, + "", + withSignFunc(func(b []byte) ([]byte, []*x509.Certificate, error) { + sig, err := localSign(b, defaultKeySpec.SignatureAlgorithm().Hash(), defaultKeyCert.key) + if err != nil { + return nil, nil, err + } + // mismatch certs and signature + return sig, []*x509.Certificate{{}, {}}, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + envelopeMediaType: envelopeType, + } + testSignerError(t, signer, "x509: malformed certificate") + }) } - testSignerError(t, signer, "x509: malformed certificate") } func TestSigner_Sign_SignatureVerifyError(t *testing.T) { - _, cert, err := generateKeyCertPair() - if err != nil { - t.Fatalf("generateKeyCertPair() error = %v", err) - } - signer := pluginSigner{ - runner: &mockSignerPlugin{ - KeyID: "1", - KeySpec: signer.RSA_2048, - Sign: func(payload []byte) []byte { return []byte("r a w") }, - Certs: getBytes(cert), - }, - keyID: "1", - } - testSignerError(t, signer, "signature returned by generateSignature cannot be verified") -} - -func validSign(t *testing.T, key interface{}) func([]byte) []byte { - t.Helper() - return func(payload []byte) []byte { - signed, err := jwt.SigningMethodPS256.Sign(string(payload), key) - if err != nil { - t.Fatal(err) - } - encSigned, err := base64.RawURLEncoding.DecodeString(signed) - if err != nil { - t.Fatal(err) - } - return encSigned + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + p := newMockProvider( + defaultKeyCert.key, + defaultKeyCert.certs, + "", + withSignFunc(func(b []byte) ([]byte, []*x509.Certificate, error) { + return invalidSignatureEnvelope, defaultKeyCert.certs, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + envelopeMediaType: envelopeType, + } + testSignerError(t, signer, "signature returned by generateSignature cannot be verified") + }) } } -func TestSigner_Sign_Valid(t *testing.T) { - key, cert, err := generateKeyCertPair() - if err != nil { - t.Fatal(err) - } - pluginSigner := pluginSigner{ - runner: &mockSignerPlugin{ - KeyID: "1", - KeySpec: signer.RSA_2048, - Sign: validSign(t, key), - Certs: getBytes(cert), - }, - keyID: "1", - } - data, err := pluginSigner.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}) +func basicSignTest(t *testing.T, pluginSigner *pluginSigner) { + data, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts) if err != nil { - t.Errorf("Signer.Sign() error = %v, wantErr nil", err) + t.Fatalf("Signer.Sign() error = %v, wantErr nil", err) } - - env, err := signer.NewSignatureEnvelopeFromBytes(data, signer.MediaTypeJWSJson) + env, err := signature.ParseEnvelope(pluginSigner.envelopeMediaType, data) if err != nil { t.Fatal(err) } - sigInfo, err := env.Verify() + envContent, err := env.Verify() if err != nil { t.Fatal(err) } + if err := ValidatePayloadContentType(&envContent.Payload); err != nil { + t.Fatalf("verification failed. error = %v", err) + } + + payload, signerInfo := envContent.Payload, envContent.SignerInfo + if payload.ContentType != notation.MediaTypePayloadV1 { + t.Fatalf("Signer.Sign() Payload content type changed, expect: %v, got: %v", payload.ContentType, notation.MediaTypePayloadV1) + } + var gotPayload notation.Payload + if err := json.Unmarshal(payload.Content, &gotPayload); err != nil { + t.Fatalf("Signer.Sign() Unmarshal payload failed: %v", err) + } expectedPayload := notation.Payload{ - TargetArtifact: notation.Descriptor{}, + TargetArtifact: validSignDescriptor, + } + if !reflect.DeepEqual(expectedPayload, gotPayload) { + t.Fatalf("Signer.Sign() descriptor subject changed, expect: %v, got: %v", expectedPayload, payload) } - expectPayloadBytes, err := json.Marshal(expectedPayload) + if signerInfo.SignedAttributes.SigningScheme != signature.SigningSchemeX509 { + t.Fatalf("Signer.Sign() signing scheme changed, expect: %v, got: %v", signerInfo.SignedAttributes.SigningScheme, signature.SigningSchemeX509) + } + keySpec, err := pluginSigner.sigProvider.KeySpec() if err != nil { - t.Fatal(err) + t.Fatalf("Signer.Sign() get signer keySpec failed: %v", err) } - - if !reflect.DeepEqual(sigInfo.Payload, expectPayloadBytes) { - t.Errorf("Signer.Sign() payload changed") + if keySpec.SignatureAlgorithm() != signerInfo.SignatureAlgorithm { + t.Fatalf("Signer.Sign() signing algorithm changed") } - - if !reflect.DeepEqual(sigInfo.CertificateChain, cert) { - t.Errorf("Signer.Sign() cert chain changed") + if validSignOpts.Expiry.Unix() != signerInfo.SignedAttributes.Expiry.Unix() { + t.Fatalf("Signer.Sign() expiry changed") } + var certChain []*x509.Certificate - basicVerification(data, cert[len(cert)-1], t) -} - -type mockEnvelopePlugin struct { - err error - envelopeType string - certChain [][]byte - key interface{} + switch s := pluginSigner.sigProvider.(type) { + case *builtinProvider: + certChain, err = s.CertificateChain() + case *mockProvider: + certChain = s.certs + default: + t.Log("Unknown provider type") + return + } + if err != nil { + t.Fatalf("Signer.Sign() get signer cert failed: %v", err) + } + if !reflect.DeepEqual(certChain, signerInfo.CertificateChain) { + t.Fatalf(" Signer.Sign() cert chain changed") + } + basicVerification(t, data, pluginSigner.envelopeMediaType, certChain[len(certChain)-1]) } -func (s *mockEnvelopePlugin) Run(_ context.Context, req plugin.Request) (interface{}, error) { - switch req.Command() { - case plugin.CommandGetMetadata: - m := validMetadata - m.Capabilities[0] = plugin.CapabilityEnvelopeGenerator - return &m, nil - case plugin.CommandGenerateEnvelope: - if s.err != nil { - return nil, s.err - } - key, certs, err := generateKeyCertPair() - if err != nil { - return nil, err - } - if s.key != nil { - key = s.key - } - - var resolvedCertChain []*x509.Certificate - if s.certChain != nil { - // Override cert chain. - resolvedCertChain, err = parseCertChain(s.certChain) - if err != nil { - return nil, err - } - } else { - resolvedCertChain = certs - } - lsp, err := signer.NewLocalSignatureProvider(resolvedCertChain, key) - if err != nil { - return nil, err - } - env, _ := signer.NewSignatureEnvelope(signer.MediaTypeJWSJson) - - req1 := req.(*plugin.GenerateEnvelopeRequest) - - data, err := env.Sign(signer.SignRequest{ - Payload: req1.Payload, - PayloadContentType: signer.PayloadContentType(req1.PayloadType), - SignatureProvider: lsp, - SigningTime: time.Now(), - Expiry: time.Now().AddDate(2, 0, 0), - SigningScheme: signer.SigningSchemeX509SigningAuthority, - SigningAgent: "", - }) - if err != nil { - return nil, err - } - - envType := s.envelopeType - if envType == "" { - envType = req1.SignatureEnvelopeType +func TestSigner_Sign_Valid(t *testing.T) { + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + for _, keyCert := range keyCertPairCollections { + t.Run(fmt.Sprintf("builtin plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) { + pluginSigner := pluginSigner{ + sigProvider: newTestBuiltInProvider(keyCert), + envelopeMediaType: envelopeType, + } + basicSignTest(t, &pluginSigner) + }) + keyID := "Key" + t.Run(fmt.Sprintf("external plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) { + pluginSigner := pluginSigner{ + sigProvider: newMockProvider(keyCert.key, keyCert.certs, keyID), + envelopeMediaType: envelopeType, + keyID: keyID, + } + basicSignTest(t, &pluginSigner) + }) } - return &plugin.GenerateEnvelopeResponse{ - SignatureEnvelope: data, - SignatureEnvelopeType: envType, - }, nil } - panic("too many calls") } + func TestPluginSigner_SignEnvelope_RunFailed(t *testing.T) { - signer := pluginSigner{ - runner: &mockEnvelopePlugin{err: errors.New("failed")}, - keyID: "1", - } - _, err := signer.Sign(context.Background(), notation.Descriptor{ - MediaType: notation.MediaTypePayload, - Size: 1, - }, notation.SignOptions{}) - if err == nil || err.Error() != "generate-envelope command failed: failed" { - t.Errorf("Signer.Sign() error = %v, wantErr nil", err) + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + p := newDefaultMockProvider( + withMetaData(validMetaDataWithEnvelopeGeneratorCapabilityFunc), + ) + signer := pluginSigner{ + sigProvider: p, + envelopeMediaType: envelopeType, + } + testSignerError(t, signer, fmt.Sprintf("generate-envelope command failed: command %q is not supported", plugin.CommandGenerateEnvelope)) + }) } } func TestPluginSigner_SignEnvelope_InvalidEnvelopeType(t *testing.T) { - signer := pluginSigner{ - runner: &mockEnvelopePlugin{envelopeType: "other"}, - keyID: "1", - } - _, err := signer.Sign(context.Background(), notation.Descriptor{ - MediaType: notation.MediaTypePayload, - Size: 1, - }, notation.SignOptions{}) - if err == nil || err.Error() != "signatureEnvelopeType in generateEnvelope response \"other\" does not match request \"application/jose+json\"" { - t.Errorf("Signer.Sign() error = %v, wantErr nil", err) + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + p := newDefaultMockProvider( + withMetaData(validMetaDataWithEnvelopeGeneratorCapabilityFunc), + withGenerateEnvelope(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return &plugin.GenerateEnvelopeResponse{ + SignatureEnvelopeType: unsupported, + }, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + envelopeMediaType: envelopeType, + } + testSignerError(t, signer, fmt.Sprintf("signatureEnvelopeType in generateEnvelope response %q does not match request %q", unsupported, envelopeType)) + }) } } +// newMockEnvelopeProvider creates a mock envelope provider. +func newMockEnvelopeProvider(key crypto.PrivateKey, certs []*x509.Certificate, keyID string, opts ...optionFunc) *mockProvider { + internalProvider := newMockProvider(key, certs, "") + p := newMockProvider( + key, + certs, + keyID, + withMetaData(validMetaDataWithEnvelopeGeneratorCapabilityFunc), + withGenerateEnvelope(func(ctx context.Context, r plugin.Request) (interface{}, error) { + sigGenerator := pluginSigner{ + sigProvider: internalProvider, + envelopeMediaType: r.(*plugin.GenerateEnvelopeRequest).SignatureEnvelopeType, + } + var payload notation.Payload + if err := json.Unmarshal(r.(*plugin.GenerateEnvelopeRequest).Payload, &payload); err != nil { + return nil, err + } + data, err := sigGenerator.Sign( + context.Background(), + payload.TargetArtifact, + validSignOpts) + if err != nil { + return nil, err + } + return &plugin.GenerateEnvelopeResponse{ + SignatureEnvelope: data, + SignatureEnvelopeType: r.(*plugin.GenerateEnvelopeRequest).SignatureEnvelopeType, + }, nil + }), + ) + return p.apply(opts...) +} + +func newDefaultMockEnvelopeProvider(opts ...optionFunc) *mockProvider { + return newMockEnvelopeProvider(defaultKeyCert.key, defaultKeyCert.certs, "", opts...) +} + func TestPluginSigner_SignEnvelope_EmptyCert(t *testing.T) { - signer := pluginSigner{ - runner: &mockEnvelopePlugin{certChain: [][]byte{}}, - keyID: "1", - } - _, err := signer.Sign(context.Background(), notation.Descriptor{ - MediaType: notation.MediaTypePayload, - Size: 1, - }, notation.SignOptions{}) - if err == nil || err.Error() != "generate-envelope command failed: \"certs\" param is malformed" { - t.Errorf("Signer.Sign() error = %v, wantErr nil", err) + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + signer := pluginSigner{ + sigProvider: newDefaultMockEnvelopeProvider( + withGenerateEnvelope(func(ctx context.Context, r plugin.Request) (interface{}, error) { + sigGenerator := pluginSigner{ + sigProvider: newDefaultMockProvider( + withSignFunc(func(b []byte) ([]byte, []*x509.Certificate, error) { + sig, err := localSign(b, defaultKeySpec.SignatureAlgorithm().Hash(), defaultKeyCert.key) + if err != nil { + return nil, nil, err + } + return sig, nil, nil + }), + ), + envelopeMediaType: r.(*plugin.GenerateEnvelopeRequest).SignatureEnvelopeType, + } + data, err := sigGenerator.Sign( + context.Background(), + validSignDescriptor, + validSignOpts) + if err != nil { + return nil, err + } + return &plugin.GenerateEnvelopeResponse{ + SignatureEnvelope: data, + SignatureEnvelopeType: r.(*plugin.GenerateEnvelopeRequest).SignatureEnvelopeType, + }, nil + }), + ), + envelopeMediaType: envelopeType, + } + if _, err := signer.Sign(context.Background(), validSignDescriptor, validSignOpts); err == nil { + t.Errorf("Signer.Sign() expect error") + } + }) } } func TestPluginSigner_SignEnvelope_MalformedCertChain(t *testing.T) { - signer := pluginSigner{ - runner: &mockEnvelopePlugin{certChain: [][]byte{make([]byte, 0)}}, - keyID: "1", - } - _, err := signer.Sign(context.Background(), notation.Descriptor{ - MediaType: notation.MediaTypePayload, - Size: 1, - }, notation.SignOptions{}) - if err == nil || err.Error() != "generate-envelope command failed: x509: malformed certificate" { - t.Errorf("Signer.Sign() error = %v, wantErr nil", err) + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + signer := pluginSigner{ + sigProvider: newDefaultMockEnvelopeProvider( + withGenerateEnvelope(func(ctx context.Context, r plugin.Request) (interface{}, error) { + sigGenerator := pluginSigner{ + sigProvider: newDefaultMockProvider( + withSignFunc(func(b []byte) ([]byte, []*x509.Certificate, error) { + sig, err := localSign(b, defaultKeySpec.SignatureAlgorithm().Hash(), defaultKeyCert.key) + if err != nil { + return nil, nil, err + } + return sig, []*x509.Certificate{{}, {}}, nil + }), + ), + envelopeMediaType: r.(*plugin.GenerateEnvelopeRequest).SignatureEnvelopeType, + } + data, err := sigGenerator.Sign( + context.Background(), + validSignDescriptor, + validSignOpts) + if err != nil { + return nil, err + } + return &plugin.GenerateEnvelopeResponse{ + SignatureEnvelope: data, + SignatureEnvelopeType: r.(*plugin.GenerateEnvelopeRequest).SignatureEnvelopeType, + }, nil + }), + ), + envelopeMediaType: envelopeType, + } + testSignerError(t, signer, "x509: malformed certificate") + }) } } -func TestPluginSigner_SignEnvelope_SignatureVerifyError(t *testing.T) { - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - t.Fatal(err) - } - signer := pluginSigner{ - runner: &mockEnvelopePlugin{key: key}, - keyID: "1", +func TestPluginSigner_SignEnvelope_ResponseTypeError(t *testing.T) { + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + signer := pluginSigner{ + sigProvider: newMockEnvelopeProvider( + defaultKeyCert.key, + defaultKeyCert.certs, + "", + withGenerateEnvelope(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return struct{}{}, nil + }), + ), + envelopeMediaType: envelopeType, + } + testSignerError(t, signer, "plugin runner returned incorrect generate-envelope response type") + }) } - _, err = signer.Sign(context.Background(), notation.Descriptor{ - MediaType: notation.MediaTypePayload, - Size: 1, - }, notation.SignOptions{}) - if err == nil || err.Error() != "signature is invalid. Error: crypto/rsa: verification error" { - t.Errorf("Signer.Sign() error = %v, wantErr nil", err) +} + +func TestPluginSigner_SignEnvelope_MalFormedEnvelope(t *testing.T) { + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + p := newDefaultMockEnvelopeProvider( + withGenerateEnvelope(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return &plugin.GenerateEnvelopeResponse{ + SignatureEnvelope: []byte(unsupported), + SignatureEnvelopeType: r.(*plugin.GenerateEnvelopeRequest).SignatureEnvelopeType, + }, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + envelopeMediaType: envelopeType, + } + var expectedErr *signature.InvalidSignatureError + if _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}); err == nil || !errors.As(err, &expectedErr) { + t.Fatalf("Signer.Sign() error = %v, want MalformedSignatureError", err) + } + }) } } -func TestPluginSigner_SignEnvelope_Valid(t *testing.T) { - signer := pluginSigner{ - runner: &mockEnvelopePlugin{}, - keyID: "1", +func TestPluginSigner_SignEnvelope_DescriptorChanged(t *testing.T) { + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + signer := pluginSigner{ + sigProvider: newDefaultMockEnvelopeProvider( + withGenerateEnvelope(func(ctx context.Context, r plugin.Request) (interface{}, error) { + sigGenerator := pluginSigner{ + sigProvider: newDefaultMockProvider(), + envelopeMediaType: r.(*plugin.GenerateEnvelopeRequest).SignatureEnvelopeType, + } + data, err := sigGenerator.Sign( + context.Background(), + notation.Descriptor{ + MediaType: invalidMediaType, + }, + notation.SignOptions{}) + if err != nil { + return nil, err + } + return &plugin.GenerateEnvelopeResponse{ + SignatureEnvelope: data, + SignatureEnvelopeType: r.(*plugin.GenerateEnvelopeRequest).SignatureEnvelopeType, + }, nil + }), + ), + envelopeMediaType: envelopeType, + } + _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}) + if err == nil || err.Error() != "descriptor subject has changed" { + t.Fatalf("Signer.Sign() error = %v, wnatErr descriptor subject has changed", err) + } + }) } - _, err := signer.Sign(context.Background(), notation.Descriptor{ - MediaType: notation.MediaTypePayload, - Size: 1, - }, notation.SignOptions{}) - if err != nil { - t.Errorf("Signer.Sign() error = %v, wantErr nil", err) +} + +func TestPluginSigner_SignEnvelope_SignatureVerifyError(t *testing.T) { + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) { + p := newDefaultMockEnvelopeProvider( + withGenerateEnvelope(func(ctx context.Context, r plugin.Request) (interface{}, error) { + return &plugin.GenerateEnvelopeResponse{ + SignatureEnvelope: envelopeTypeToData[envelopeType], + SignatureEnvelopeType: r.(*plugin.GenerateEnvelopeRequest).SignatureEnvelopeType, + }, nil + }), + ) + signer := pluginSigner{ + sigProvider: p, + envelopeMediaType: envelopeType, + } + _, err := signer.Sign(context.Background(), notation.Descriptor{}, notation.SignOptions{}) + if err == nil { + t.Fatalf("Signer.Sign() error = %v", err) + } + }) } } -func getBytes(certs []*x509.Certificate) [][]byte { - var chain [][]byte - for _, cert := range certs { - chain = append(chain, cert.Raw) +func TestPluginSigner_SignEnvelope_Valid(t *testing.T) { + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + for _, keyCert := range keyCertPairCollections { + t.Run(fmt.Sprintf("envelopeType=%v, keySpec: %v", envelopeType, keyCert.keySpecName), func(t *testing.T) { + signer := pluginSigner{ + sigProvider: newMockEnvelopeProvider(keyCert.key, keyCert.certs, ""), + envelopeMediaType: envelopeType, + } + basicSignTest(t, &signer) + }) + } } - return chain } diff --git a/signature/provider.go b/signature/provider.go new file mode 100644 index 00000000..b817e283 --- /dev/null +++ b/signature/provider.go @@ -0,0 +1,142 @@ +package signature + +import ( + "context" + "crypto" + "crypto/x509" + "fmt" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-go/plugin" +) + +// builtInPluginMetaData is the metadata used by builtinProvider. +var builtInPluginMetaData = &plugin.Metadata{ + SupportedContractVersions: []string{plugin.ContractVersion}, + Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}, + Name: "built-in", + Description: "Notation built-in signer", + Version: plugin.ContractVersion, + URL: "https://github.com/notaryproject/notation-go", +} + +// provider wraps a plugin.Runner and a signature.Signer. +type provider interface { + plugin.Runner + signature.Signer +} + +// builtinProvider is a builtin provider implementation +// which wraps the signature.Signature to support builtin signing method. +// It only supports describe key and metadata command. +type builtinProvider struct { + signature.LocalSigner +} + +// newBuiltinProvider creates a builtinProvider to support local signing. +func newBuiltinProvider(key crypto.PrivateKey, certChain []*x509.Certificate) (provider, error) { + builtinSigner, err := signature.NewLocalSigner(certChain, key) + if err != nil { + return nil, err + } + return &builtinProvider{ + builtinSigner, + }, nil +} + +// metadata provides metadata for builtinProvider. +func (*builtinProvider) metadata() *plugin.Metadata { + // The only properties that are really relevant + // are the supported contract version and the capabilities. + // All other are just filled with meaningful data. + return builtInPluginMetaData +} + +// Run implements the plugin workflow. +// +// builtinProvider only supports metadata and describe key. +func (p *builtinProvider) Run(_ context.Context, req plugin.Request) (interface{}, error) { + switch req.Command() { + case plugin.CommandGetMetadata: + return p.metadata(), nil + case plugin.CommandDescribeKey: + req1 := req.(*plugin.DescribeKeyRequest) + return &plugin.DescribeKeyResponse{ + KeyID: req1.KeyID, + }, nil + } + return nil, plugin.RequestError{ + Code: plugin.ErrorCodeGeneric, + Err: fmt.Errorf("command %q is not supported", req.Command()), + } +} + +// externalProvider is an external provider implementation which will interact with plugin. +// It supports all plugin commands. +// +// The detail implementation depends on the underlying plugin. +// +// It wraps a signature.Signer to support external signing. +type externalProvider struct { + plugin.Runner + keyID string + config map[string]string + keySpec signature.KeySpec +} + +// newExternalProvider creates an external provider. +func newExternalProvider(runner plugin.Runner, keyID string) provider { + return &externalProvider{ + Runner: runner, + keyID: keyID, + } +} + +// prepareSigning sets up config and keySpec used to sign. +func (p *externalProvider) prepareSigning(cfg map[string]string, keySpec signature.KeySpec) { + p.config = cfg + p.keySpec = keySpec +} + +// Sign signs the digest by calling the underlying plugin. +func (p *externalProvider) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { + // Execute plugin sign command. + keySpec, err := p.KeySpec() + if err != nil { + return nil, nil, err + } + req := &plugin.GenerateSignatureRequest{ + ContractVersion: plugin.ContractVersion, + KeyID: p.keyID, + KeySpec: plugin.KeySpecString(keySpec), + Hash: plugin.KeySpecHashString(keySpec), + Payload: payload, + PluginConfig: p.config, + } + + out, err := p.Run(context.Background(), req) + if err != nil { + return nil, nil, fmt.Errorf("generate-signature command failed: %w", err) + } + + resp, ok := out.(*plugin.GenerateSignatureResponse) + if !ok { + return nil, nil, fmt.Errorf("plugin runner returned incorrect generate-signature response type '%T'", out) + } + + // Check keyID is honored. + if req.KeyID != resp.KeyID { + return nil, nil, fmt.Errorf("keyID in generateSignature response %q does not match request %q", resp.KeyID, req.KeyID) + } + + var certs []*x509.Certificate + if certs, err = parseCertChain(resp.CertificateChain); err != nil { + return nil, nil, err + } + return resp.Signature, certs, nil +} + +// KeySpec returns the keySpec of a keyID by calling describeKey and do some keySpec validation. +func (p *externalProvider) KeySpec() (signature.KeySpec, error) { + return p.keySpec, nil +} diff --git a/signature/signer.go b/signature/signer.go index 5dbc1b0f..b1aa9b71 100644 --- a/signature/signer.go +++ b/signature/signer.go @@ -1,21 +1,18 @@ package signature import ( - "context" "crypto" "crypto/tls" "crypto/x509" "errors" "fmt" - "github.com/notaryproject/notation-core-go/signer" "github.com/notaryproject/notation-go" - "github.com/notaryproject/notation-go/plugin" ) // NewSignerFromFiles creates a signer from key, certificate files // TODO: Add tests for this method. https://github.com/notaryproject/notation-go/issues/80 -func NewSignerFromFiles(keyPath, certPath string) (notation.Signer, error) { +func NewSignerFromFiles(keyPath, certPath, envelopeMediaType string) (notation.Signer, error) { if keyPath == "" { return nil, errors.New("key path not specified") } @@ -42,93 +39,26 @@ func NewSignerFromFiles(keyPath, certPath string) (notation.Signer, error) { } // create signer - return NewSigner(cert.PrivateKey, certs) + return NewSigner(cert.PrivateKey, certs, envelopeMediaType) } // NewSigner creates a signer with the recommended signing method and a signing key bundled // with a certificate chain. // The relation of the provided signing key and its certificate chain is not verified, // and should be verified by the caller. -func NewSigner(key crypto.PrivateKey, certChain []*x509.Certificate) (notation.Signer, error) { - lsp, err := signer.NewLocalSignatureProvider(certChain, key) +func NewSigner(key crypto.PrivateKey, certChain []*x509.Certificate, envelopeMediaType string) (notation.Signer, error) { + builtinProvider, err := newBuiltinProvider(key, certChain) if err != nil { return nil, err } - - return &pluginSigner{ - runner: &builtinPlugin{localSignatureProvider: lsp}, - }, nil -} - -// builtinPlugin is a plugin.Runner implementation which -// signs supports the generate-signature workflow using -// the provided key and certificates. -type builtinPlugin struct { - localSignatureProvider *signer.LocalSignatureProvider - sigAlg signer.SignatureAlgorithm -} - -func (builtinPlugin) metadata() *plugin.Metadata { - // The only properties that are really relevant - // are the supported contract version and the capabilities. - // All other are just filled with meaningful data. - return &plugin.Metadata{ - SupportedContractVersions: []string{plugin.ContractVersion}, - Capabilities: []plugin.Capability{plugin.CapabilitySignatureGenerator}, - Name: "built-in", - Description: "Notation built-in signer", - Version: plugin.ContractVersion, - URL: "https://github.com/notaryproject/notation-go", - } -} - -// Run implement the generate-signature workflow. -func (r *builtinPlugin) Run(_ context.Context, req plugin.Request) (interface{}, error) { - switch req.Command() { - case plugin.CommandGetMetadata: - return r.metadata(), nil - case plugin.CommandDescribeKey: - req1 := req.(*plugin.DescribeKeyRequest) - - ks, err := r.localSignatureProvider.KeySpec() - if err != nil { - return nil, plugin.RequestError{ - Code: plugin.ErrorCodeGeneric, - Err: err, - } - } - - return &plugin.DescribeKeyResponse{ - KeyID: req1.KeyID, - KeySpec: ks, - }, nil - case plugin.CommandGenerateSignature: - req1 := req.(*plugin.GenerateSignatureRequest) - - signed, certChain, err := r.localSignatureProvider.Sign(req1.Payload) - if err != nil { - return nil, plugin.RequestError{ - Code: plugin.ErrorCodeGeneric, - Err: err, - } - } - - rawCerts := make([][]byte, len(certChain)) - for i, cert := range certChain { - rawCerts[i] = cert.Raw - } - - return &plugin.GenerateSignatureResponse{ - KeyID: req1.KeyID, - Signature: signed, - SigningAlgorithm: r.sigAlg, - CertificateChain: rawCerts, - }, nil + if err := ValidateEnvelopeMediaType(envelopeMediaType); err != nil { + return nil, err } - return nil, plugin.RequestError{ - Code: plugin.ErrorCodeGeneric, - Err: fmt.Errorf("command %q is not supported", req.Command()), + signer := &pluginSigner{ + sigProvider: builtinProvider, + envelopeMediaType: envelopeMediaType, } + return signer, nil } // called from plugin.go diff --git a/signature/signer_test.go b/signature/signer_test.go index 7af8c4af..850e0b5c 100644 --- a/signature/signer_test.go +++ b/signature/signer_test.go @@ -8,95 +8,277 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/pem" + "errors" "fmt" - "strconv" + "os" + "path/filepath" "testing" "time" - "github.com/notaryproject/notation-core-go/signer" + "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/testhelper" "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/crypto/timestamp/timestamptest" + "github.com/notaryproject/notation-go/plugin" "github.com/opencontainers/go-digest" ) -func TestNewSignerFromFiles(t *testing.T) { - t.Skip("Please implement TestNewSignerFromFiles test") +type keyCertPair struct { + keySpecName string + key crypto.PrivateKey + certs []*x509.Certificate } -func TestSignWithCertChain(t *testing.T) { - // sign with key - rsaRoot := testhelper.GetRSARootCertificate() +var keyCertPairCollections []*keyCertPair + +const testKeyID = "testKeyID" + +// setUpKeyCertPairCollections setups all combinations of private key and certificates. +func setUpKeyCertPairCollections() []*keyCertPair { + // rsa + var keyCertPairs []*keyCertPair for _, k := range []int{2048, 3072, 4096} { - pk, _ := rsa.GenerateKey(rand.Reader, k) - certTuple := testhelper.GetRSACertTupleWithPK(pk, "TestSignWithCertChain_"+strconv.Itoa(pk.Size()), &rsaRoot) - t.Run(fmt.Sprintf("RSA certificates of size %d", pk.Size()), func(t *testing.T) { - validateSignWithCerts(t, pk, []*x509.Certificate{certTuple.Cert, rsaRoot.Cert}) + rsaRoot := testhelper.GetRSARootCertificate() + certTuple := testhelper.GetRSACertTuple(k) + keySpec, err := signature.ExtractKeySpec(certTuple.Cert) + if err != nil { + panic(fmt.Sprintf("setUpKeyCertPairCollections() failed, invalid keySpec: %v", err)) + } + keyCertPairs = append(keyCertPairs, &keyCertPair{ + keySpecName: plugin.KeySpecString(keySpec), + key: certTuple.PrivateKey, + certs: []*x509.Certificate{certTuple.Cert, rsaRoot.Cert}, }) } - ecRoot := testhelper.GetECRootCertificate() - for _, v := range []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} { - pk, _ := ecdsa.GenerateKey(v, rand.Reader) - certTuple := testhelper.GetECDSACertTupleWithPK(pk, "TestSignWithCertChain_"+strconv.Itoa(pk.Params().BitSize), &ecRoot) - t.Run(fmt.Sprintf("EC certificates of size %d", pk.Params().BitSize), func(t *testing.T) { - validateSignWithCerts(t, pk, []*x509.Certificate{certTuple.Cert, ecRoot.Cert}) + // ecdsa + for _, curve := range []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} { + ecdsaRoot := testhelper.GetECRootCertificate() + certTuple := testhelper.GetECCertTuple(curve) + keySpec, err := signature.ExtractKeySpec(certTuple.Cert) + if err != nil { + panic(fmt.Sprintf("setUpKeyCertPairCollections() failed, invalid keySpec: %v", err)) + } + keyCertPairs = append(keyCertPairs, &keyCertPair{ + keySpecName: plugin.KeySpecString(keySpec), + key: certTuple.PrivateKey, + certs: []*x509.Certificate{certTuple.Cert, ecdsaRoot.Cert}, }) } + return keyCertPairs } -// TODO: Enable once we have timestamping inplace https://github.com/notaryproject/notation-go/issues/78 -func TestSignWithTimestamp(t *testing.T) { - t.Skip("Skipping testing as we dont have timestamping hooked up") - // prepare signer - key, certs, err := generateKeyCertPair() - if err != nil { - t.Fatalf("generateKeyCertPair() error = %v", err) +func init() { + keyCertPairCollections = setUpKeyCertPairCollections() +} + +func generateCertPem(cert *x509.Certificate) []byte { + return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}) +} + +func generateKeyBytes(key crypto.PrivateKey) (keyBytes []byte, err error) { + switch k := key.(type) { + case *rsa.PrivateKey: + keyBytes, err = x509.MarshalPKCS8PrivateKey(k) + case *ecdsa.PrivateKey: + keyBytes, err = x509.MarshalECPrivateKey(k) + default: + return nil, errors.New("private key type not supported") } - s, err := NewSigner(key, certs) if err != nil { - t.Fatalf("NewSigner() error = %v", err) + return nil, err } + keyBytes = pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}) + return keyBytes, nil +} - // configure TSA - tsa, err := timestamptest.NewTSA() +func prepareTestKeyCertFile(keyCert *keyCertPair, envelopeType, dir string) (string, string, error) { + keyPath, certPath := filepath.Join(dir, keyCert.keySpecName+".key"), filepath.Join(dir, keyCert.keySpecName+".cert") + keyBytes, err := generateKeyBytes(keyCert.key) if err != nil { - t.Fatalf("timestamptest.NewTSA() error = %v", err) + return "", "", err + } + var certBytes []byte + for _, cert := range keyCert.certs { + certBytes = append(certBytes, generateCertPem(cert)...) } - // sign content - ctx := context.Background() - desc, sOpts := generateSigningContent(tsa) - sig, err := s.Sign(ctx, desc, sOpts) - if err != nil { - t.Fatalf("Sign() error = %v", err) + if err := os.WriteFile(keyPath, keyBytes, 0666); err != nil { + return "", "", err + } + if err := os.WriteFile(certPath, certBytes, 0666); err != nil { + return "", "", err } + return keyPath, certPath, nil +} +func testSignerFromFile(t *testing.T, keyCert *keyCertPair, envelopeType, dir string) { + keyPath, certPath, err := prepareTestKeyCertFile(keyCert, envelopeType, dir) + if err != nil { + t.Fatalf("prepareTestKeyCertFile() failed: %v", err) + } + s, err := NewSignerFromFiles(keyPath, certPath, envelopeType) + if err != nil { + t.Fatalf("NewSignerFromFiles() failed: %v", err) + } + desc, opts := generateSigningContent(nil) + sig, err := s.Sign(context.Background(), desc, opts) + if err != nil { + t.Fatalf("Sign() failed: %v", err) + } // basic verification - basicVerification(sig, certs[len(certs)-1], t) + basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1]) +} + +func TestNewSignerFromFiles(t *testing.T) { + // sign with key + dir := t.TempDir() + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + for _, keyCert := range keyCertPairCollections { + t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) { + testSignerFromFile(t, keyCert, envelopeType, dir) + }) + } + } +} + +func TestSignWithCertChain(t *testing.T) { + // sign with key + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + for _, keyCert := range keyCertPairCollections { + t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) { + validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs) + }) + } + } +} + +// TODO: Enable once we have timestamping inplace https://github.com/notaryproject/notation-go/issues/78 +func TestSignWithTimestamp(t *testing.T) { + t.Skip("Skipping testing as we dont have timestamping hooked up") + // prepare signer + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + for _, keyCert := range keyCertPairCollections { + t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) { + s, err := NewSigner(keyCert.key, keyCert.certs, envelopeType) + if err != nil { + t.Fatalf("NewSigner() error = %v", err) + } + + // configure TSA + tsa, err := timestamptest.NewTSA() + if err != nil { + t.Fatalf("timestamptest.NewTSA() error = %v", err) + } + + // sign content + ctx := context.Background() + desc, sOpts := generateSigningContent(tsa) + sig, err := s.Sign(ctx, desc, sOpts) + if err != nil { + t.Fatalf("Sign() error = %v", err) + } + + // basic verification + basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1]) + }) + } + } } func TestSignWithoutExpiry(t *testing.T) { // sign with key - key, certs, err := generateKeyCertPair() - if err != nil { - t.Fatalf("generateKeyCertPair() error = %v", err) + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + for _, keyCert := range keyCertPairCollections { + t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) { + s, err := NewSigner(keyCert.key, keyCert.certs, envelopeType) + if err != nil { + t.Fatalf("NewSigner() error = %v", err) + } + + ctx := context.Background() + desc, sOpts := generateSigningContent(nil) + sOpts.Expiry = time.Time{} // reset expiry + sig, err := s.Sign(ctx, desc, sOpts) + if err != nil { + t.Fatalf("Sign() error = %v", err) + } + + // basic verification + basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1]) + }) + } } - s, err := NewSigner(key, certs) +} + +func signRSA(digest []byte, hash crypto.Hash, pk *rsa.PrivateKey) ([]byte, error) { + return rsa.SignPSS(rand.Reader, pk, hash, digest, &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}) +} + +func signECDSA(digest []byte, hash crypto.Hash, pk *ecdsa.PrivateKey) ([]byte, error) { + r, s, err := ecdsa.Sign(rand.Reader, pk, digest) if err != nil { - t.Fatalf("NewSigner() error = %v", err) + return nil, err } + n := (pk.Curve.Params().N.BitLen() + 7) / 8 + sig := make([]byte, 2*n) + r.FillBytes(sig[:n]) + s.FillBytes(sig[n:]) + return sig, nil +} - ctx := context.Background() - desc, sOpts := generateSigningContent(nil) - sOpts.Expiry = time.Time{} // reset expiry - sig, err := s.Sign(ctx, desc, sOpts) - if err != nil { - t.Fatalf("Sign() error = %v", err) +func localSign(payload []byte, hash crypto.Hash, pk crypto.PrivateKey) ([]byte, error) { + h := hash.New() + h.Write(payload) + digest := h.Sum(nil) + switch key := pk.(type) { + case *rsa.PrivateKey: + return signRSA(digest, hash, key) + case *ecdsa.PrivateKey: + return signECDSA(digest, hash, key) + default: + return nil, errors.New("signing private key not supported") } +} - // basic verification - basicVerification(sig, certs[len(certs)-1], t) +func TestExternalSigner_Sign(t *testing.T) { + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + for _, keyCert := range keyCertPairCollections { + externalRunner := newMockProvider(keyCert.key, keyCert.certs, testKeyID) + s, err := NewSignerPlugin(externalRunner, testKeyID, nil, envelopeType) + if err != nil { + t.Fatalf("NewSigner() error = %v", err) + } + sig, err := s.Sign(context.Background(), validSignDescriptor, validSignOpts) + if err != nil { + t.Fatalf("Sign() error = %v", err) + } + // basic verification + basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1]) + } + } +} + +func TestExternalSigner_SignEnvelope(t *testing.T) { + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + for _, keyCert := range keyCertPairCollections { + t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) { + externalRunner := newMockEnvelopeProvider(keyCert.key, keyCert.certs, testKeyID) + p := newExternalProvider(externalRunner, testKeyID) + s, err := NewSignerPlugin(p, testKeyID, nil, envelopeType) + if err != nil { + t.Fatalf("NewSigner() error = %v", err) + } + sig, err := s.Sign(context.Background(), validSignDescriptor, validSignOpts) + if err != nil { + t.Fatalf("Sign() error = %v", err) + } + // basic verification + basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1]) + }) + } + } } // generateSigningContent generates common signing content with options for testing. @@ -133,27 +315,30 @@ func generateKeyCertPair() (crypto.PrivateKey, []*x509.Certificate, error) { return pk, []*x509.Certificate{certTuple.Cert, rsaRoot.Cert}, nil } -func basicVerification(sig []byte, trust *x509.Certificate, t *testing.T) { +func basicVerification(t *testing.T, sig []byte, envelopeType string, trust *x509.Certificate) { // basic verification - sigEnv, err := signer.NewSignatureEnvelopeFromBytes(sig, signer.MediaTypeJWSJson) + sigEnv, err := signature.ParseEnvelope(envelopeType, sig) if err != nil { t.Fatalf("verification failed. error = %v", err) } - sigInfo, vErr := sigEnv.Verify() + envContent, vErr := sigEnv.Verify() if vErr != nil { t.Fatalf("verification failed. error = %v", err) } + if err := ValidatePayloadContentType(&envContent.Payload); err != nil { + t.Fatalf("verification failed. error = %v", err) + } - trustedCert, err := signer.VerifyAuthenticity(sigInfo, []*x509.Certificate{trust}) + trustedCert, err := signature.VerifyAuthenticity(&envContent.SignerInfo, []*x509.Certificate{trust}) - if err !=nil || !trustedCert.Equal(trust) { + if err != nil || !trustedCert.Equal(trust) { t.Fatalf("VerifyAuthenticity failed. error = %v", err) } } -func validateSignWithCerts(t *testing.T, key crypto.PrivateKey, certs []*x509.Certificate) { - s, err := NewSigner(key, certs) +func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.PrivateKey, certs []*x509.Certificate) { + s, err := NewSigner(key, certs, envelopeType) if err != nil { t.Fatalf("NewSigner() error = %v", err) } @@ -166,5 +351,5 @@ func validateSignWithCerts(t *testing.T, key crypto.PrivateKey, certs []*x509.Ce } // basic verification - basicVerification(sig, certs[len(certs)-1], t) + basicVerification(t, sig, envelopeType, certs[len(certs)-1]) } diff --git a/signature/verifier.go b/signature/verifier.go index b69e0757..f67c2462 100644 --- a/signature/verifier.go +++ b/signature/verifier.go @@ -6,7 +6,8 @@ import ( "encoding/json" "fmt" - "github.com/notaryproject/notation-core-go/signer" + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-core-go/signature/jws" x509n "github.com/notaryproject/notation-core-go/x509" "github.com/notaryproject/notation-go" ) @@ -39,32 +40,47 @@ func NewVerifierFromFiles(certPaths []string) (*Verifier, error) { return &Verifier{TrustedCerts: certs}, nil } +func ValidatePayloadContentType(payload *signature.Payload) error { + switch payload.ContentType { + case notation.MediaTypePayloadV1: + return nil + default: + return fmt.Errorf("payload content type %s not supported", payload.ContentType) + } +} + // Verify verifies the signature and returns the verified descriptor and // metadata of the signed artifact. func (v *Verifier) Verify(_ context.Context, sig []byte, opts notation.VerifyOptions) (notation.Descriptor, error) { - sigEnv, err := signer.NewSignatureEnvelopeFromBytes(sig, signer.MediaTypeJWSJson) + // TODO: pass media type as a parameter + sigEnv, err := signature.ParseEnvelope(jws.MediaTypeEnvelope, sig) if err != nil { return notation.Descriptor{}, err } - sigInfo, err := sigEnv.Verify() + envContent, err := sigEnv.Verify() if err != nil { return notation.Descriptor{}, err } - _, authErr := signer.VerifyAuthenticity(sigInfo, v.TrustedCerts) + if err := ValidatePayloadContentType(&envContent.Payload); err != nil { + return notation.Descriptor{}, err + } + + _, authErr := signature.VerifyAuthenticity(&envContent.SignerInfo, v.TrustedCerts) if authErr != nil { return notation.Descriptor{}, authErr } // TODO: validate expiry and timestamp https://github.com/notaryproject/notation-go/issues/78 var payload notation.Payload - if err = json.Unmarshal(sigInfo.Payload, &payload); err != nil { + if err = json.Unmarshal(envContent.Payload.Content, &payload); err != nil { return notation.Descriptor{}, fmt.Errorf("envelope payload can't be decoded: %w", err) } return payload.TargetArtifact, nil } + // // // verifySigner verifies the signing identity and returns the verification key. // func (v *Verifier) verifySigner(sig *notation.JWSEnvelope) (crypto.PublicKey, error) { diff --git a/signature/verifier_test.go b/signature/verifier_test.go index 023de88d..036d5bab 100644 --- a/signature/verifier_test.go +++ b/signature/verifier_test.go @@ -3,9 +3,11 @@ package signature import ( "context" "crypto/x509" + "fmt" "reflect" "testing" + "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-go" ) @@ -15,36 +17,29 @@ func TestVerifierInterface(t *testing.T) { } } -func TestVerifyWithCertChain(t *testing.T) { - // sign with key - key, cert, err := generateKeyCertPair() +func testVerifierFromFile(t *testing.T, keyCert *keyCertPair, envelopeType, dir string) { + keyPath, certPath, err := prepareTestKeyCertFile(keyCert, envelopeType, dir) if err != nil { - t.Fatalf("generateKeyCertPair() error = %v", err) + t.Fatalf("prepare key cert file failed: %v", err) } - s, err := NewSigner(key, cert) + s, err := NewSignerFromFiles(keyPath, certPath, envelopeType) if err != nil { - t.Fatalf("NewSigner() error = %v", err) + t.Fatalf("NewSignerFromFiles() failed: %v", err) } - - ctx := context.Background() - desc, sOpts := generateSigningContent(nil) - sig, err := s.Sign(ctx, desc, sOpts) + desc, opts := generateSigningContent(nil) + sig, err := s.Sign(context.Background(), desc, opts) if err != nil { - t.Fatalf("Sign() error = %v", err) + t.Fatalf("Sign() failed: %v", err) } - // verify signature - v := NewVerifier() - var vOpts notation.VerifyOptions - - // should fail if nothing is trusted - if _, err := v.Verify(ctx, sig, vOpts); err == nil { - t.Errorf("Verify() error = %v, wantErr %v", err, true) + v, err := NewVerifierFromFiles([]string{certPath}) + if err != nil { + t.Fatalf("NewVerifierFromFiles() failed: %v", err) } - - // verify again with certificate trusted - v.TrustedCerts = []*x509.Certificate{cert[len(cert)-1]} - got, err := v.Verify(ctx, sig, vOpts) + vOpts := notation.VerifyOptions{ + SignatureMediaType: envelopeType, + } + got, err := v.Verify(context.Background(), sig, vOpts) if err != nil { t.Fatalf("Verify() error = %v", err) } @@ -56,6 +51,63 @@ func TestVerifyWithCertChain(t *testing.T) { } } +func TestNewVerifierFromFile(t *testing.T) { + dir := t.TempDir() + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + for _, keyCert := range keyCertPairCollections { + t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) { + testVerifierFromFile(t, keyCert, envelopeType, dir) + }) + } + } +} + +func TestVerifyWithCertChain(t *testing.T) { + // sign with key + for _, envelopeType := range signature.RegisteredEnvelopeTypes() { + for _, keyCert := range keyCertPairCollections { + t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) { + s, err := NewSigner(keyCert.key, keyCert.certs, envelopeType) + if err != nil { + t.Fatalf("NewSigner() error = %v", err) + } + + ctx := context.Background() + desc, sOpts := generateSigningContent(nil) + sig, err := s.Sign(ctx, desc, sOpts) + if err != nil { + t.Fatalf("Sign() error = %v", err) + } + + // verify signature + v := NewVerifier() + vOpts := notation.VerifyOptions{ + SignatureMediaType: envelopeType, + } + + // should fail if nothing is trusted + if _, err := v.Verify(ctx, sig, vOpts); err == nil { + t.Errorf("Verify() error = %v, wantErr %v", err, true) + } + + // verify again with certificate trusted + v.TrustedCerts = []*x509.Certificate{keyCert.certs[len(keyCert.certs)-1]} + got, err := v.Verify(ctx, sig, vOpts) + if err != nil { + t.Fatalf("Verify() error = %v", err) + } + if !got.Equal(desc) { + t.Errorf("Verify() Descriptor = %v, want %v", got, desc) + } + if !reflect.DeepEqual(got, desc) { + t.Errorf("Verify() Descriptor = %v, want %v", got, desc) + } + }) + } + } + +} + func TestVerifyWithTimestamp(t *testing.T) { t.Skip("Skipping testing as we dont have timestamping hooked up") // prepare signer diff --git a/verification/helpers.go b/verification/helpers.go index c31769cd..923c42d1 100644 --- a/verification/helpers.go +++ b/verification/helpers.go @@ -3,12 +3,13 @@ package verification import ( "encoding/json" "fmt" - nsigner "github.com/notaryproject/notation-core-go/signer" - "github.com/notaryproject/notation-go/dir" "os" "regexp" "strings" + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-go/dir" + ldapv3 "github.com/go-ldap/ldap/v3" ) @@ -26,11 +27,11 @@ func loadPolicyDocument(policyDocumentPath string) (*PolicyDocument, error) { return policyDocument, nil } -func loadX509TrustStores(scheme nsigner.SigningScheme, policy *TrustPolicy, pathManager *dir.PathManager) (map[string]*X509TrustStore, error) { +func loadX509TrustStores(scheme signature.SigningScheme, policy *TrustPolicy, pathManager *dir.PathManager) (map[string]*X509TrustStore, error) { var prefixToLoad TrustStorePrefix - if scheme == nsigner.SigningSchemeX509 { + if scheme == signature.SigningSchemeX509 { prefixToLoad = TrustStorePrefixCA - } else if scheme == nsigner.SigningSchemeX509SigningAuthority { + } else if scheme == signature.SigningSchemeX509SigningAuthority { prefixToLoad = TrustStorePrefixSigningAuthority } else { return nil, fmt.Errorf("unrecognized signing scheme %q", scheme) diff --git a/verification/helpers_test.go b/verification/helpers_test.go index 9a6e800d..c4ba624d 100644 --- a/verification/helpers_test.go +++ b/verification/helpers_test.go @@ -2,12 +2,13 @@ package verification import ( "encoding/json" - nsigner "github.com/notaryproject/notation-core-go/signer" - "github.com/notaryproject/notation-go/dir" - "io/ioutil" + "os" "path/filepath" "strconv" "testing" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-go/dir" ) func TestGetArtifactDigestFromUri(t *testing.T) { @@ -46,7 +47,10 @@ func TestLoadPolicyDocument(t *testing.T) { } // existing invalid json file path := filepath.Join(t.TempDir(), "invalid.json") - err = ioutil.WriteFile(path, []byte(`{"invalid`), 0644) + err = os.WriteFile(path, []byte(`{"invalid`), 0644) + if err != nil { + t.Fatalf("TestLoadPolicyDocument create invalid policy file failed. Error: %v", err) + } _, err = loadPolicyDocument(path) if err == nil { t.Fatalf("TestLoadPolicyDocument should throw error for invalid policy file. Error: %v", err) @@ -56,7 +60,10 @@ func TestLoadPolicyDocument(t *testing.T) { path = filepath.Join(t.TempDir(), "trustpolicy.json") policyDoc1 := dummyPolicyDocument() policyJson, _ := json.Marshal(policyDoc1) - err = ioutil.WriteFile(path, policyJson, 0644) + err = os.WriteFile(path, policyJson, 0644) + if err != nil { + t.Fatalf("TestLoadPolicyDocument create valid policy file failed. Error: %v", err) + } _, err = loadPolicyDocument(path) if err != nil { t.Fatalf("TestLoadPolicyDocument should not throw error for an existing policy file. Error: %v", err) @@ -74,8 +81,11 @@ func TestLoadX509TrustStore(t *testing.T) { dir.NewRootedFS("testdata", nil), ), } - caTrustStores, err := loadX509TrustStores(nsigner.SigningSchemeX509, &dummyPolicy, path) - saTrustStores, err := loadX509TrustStores(nsigner.SigningSchemeX509SigningAuthority, &dummyPolicy, path) + caTrustStores, err := loadX509TrustStores(signature.SigningSchemeX509, &dummyPolicy, path) + if err != nil { + t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err) + } + saTrustStores, err := loadX509TrustStores(signature.SigningSchemeX509SigningAuthority, &dummyPolicy, path) if err != nil { t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err) } diff --git a/verification/types.go b/verification/types.go index 1f33c6da..49bb58bd 100644 --- a/verification/types.go +++ b/verification/types.go @@ -3,7 +3,8 @@ package verification import ( "context" "fmt" - nsigner "github.com/notaryproject/notation-core-go/signer" + + "github.com/notaryproject/notation-core-go/signature" ) // TrustStorePrefix is an enum for trust store prefixes supported such as "ca", "signingAuthority" @@ -31,8 +32,8 @@ type VerificationResult struct { // SignatureVerificationOutcome encapsulates the SignerInfo (that includes the details of the digital signature) // and results for each verification type that was performed type SignatureVerificationOutcome struct { - // SignerInfo contains the details of the digital signature and associated metadata - SignerInfo *nsigner.SignerInfo + // EnvelopeContent contains the details of the digital signature and associated metadata + EnvelopeContent *signature.EnvelopeContent // VerificationLevel describes what verification level was used for performing signature verification VerificationLevel *VerificationLevel // VerificationResults contains the verifications performed on the signature and their results @@ -221,10 +222,18 @@ func WithPluginConfig(ctx context.Context, config map[string]string) context.Con } // getPluginConfig used to retrieve the config from the context. -func getPluginConfig(ctx context.Context, config map[string]string) map[string]string { +func getPluginConfig(ctx context.Context) map[string]string { config, ok := ctx.Value(pluginConfigCtxKey{}).(map[string]string) if !ok { return nil } return config } + +const ( + // VerificationPlugin specifies the name of the verification plugin that should be used to verify the signature. + VerificationPlugin = "io.cncf.notary.verificationPlugin" + + // VerificationPluginMinVersion specifies the minimum version of the verification plugin that should be used to verify the signature. + VerificationPluginMinVersion = "io.cncf.notary.verificationPluginMinVersion" +) diff --git a/verification/verifier.go b/verification/verifier.go index 28760f11..24441002 100644 --- a/verification/verifier.go +++ b/verification/verifier.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/dir" "github.com/notaryproject/notation-go/plugin" @@ -123,7 +124,7 @@ func (v *Verifier) Verify(ctx context.Context, artifactUri string) ([]*Signature // artifact digest must match the digest from the signature payload payload := ¬ation.Payload{} - err := json.Unmarshal(outcome.SignerInfo.Payload, payload) + err := json.Unmarshal(outcome.EnvelopeContent.Payload.Content, payload) if err != nil || !artifactDescriptor.Equal(payload.TargetArtifact) { outcome.Error = fmt.Errorf("given digest %q does not match the digest %q present in the digital signature", artifactDigest, payload.TargetArtifact.Digest.String()) continue @@ -140,8 +141,8 @@ func (v *Verifier) Verify(ctx context.Context, artifactUri string) ([]*Signature func (v *Verifier) processSignature(ctx context.Context, sigBlob []byte, sigManifest registry.SignatureManifest, trustPolicy *TrustPolicy, outcome *SignatureVerificationOutcome) error { // verify integrity first. notation will always verify integrity no matter what the signing scheme is - signerInfo, integrityResult := v.verifyIntegrity(sigBlob, sigManifest, outcome) - outcome.SignerInfo = signerInfo + envContent, integrityResult := v.verifyIntegrity(sigBlob, sigManifest, outcome) + outcome.EnvelopeContent = envContent outcome.VerificationResults = append(outcome.VerificationResults, integrityResult) if integrityResult.Error != nil { return integrityResult.Error @@ -149,13 +150,20 @@ func (v *Verifier) processSignature(ctx context.Context, sigBlob []byte, sigMani // check if we need to verify using a plugin var pluginCapabilities []plugin.Capability - verificationPluginName := outcome.SignerInfo.SignedAttributes.VerificationPlugin - if verificationPluginName != "" { + verificationPluginName, err := getVerificationPlugin(&outcome.EnvelopeContent.SignerInfo) + // use plugin, but getPluginName returns an error + if err != nil && err != errExtendedAttributeNotExist { + return err + } + if err == nil { installedPlugin, err := v.PluginManager.Get(ctx, verificationPluginName) if err != nil { return ErrorVerificationInconclusive{msg: fmt.Sprintf("error while locating the verification plugin %q, make sure the plugin is installed successfully before verifying the signature. error: %s", verificationPluginName, err)} } + if _, err := getVerificationPluginMinVersion(&outcome.EnvelopeContent.SignerInfo); err != nil && err != errExtendedAttributeNotExist { + return ErrorVerificationInconclusive{msg: fmt.Sprintf("error while getting plugin minimum version, error: %s", err)} + } // TODO verify the plugin's version is equal to or greater than `outcome.SignerInfo.SignedAttributes.VerificationPluginMinVersion` // https://github.com/notaryproject/notation-go/issues/102 @@ -220,7 +228,7 @@ func (v *Verifier) processSignature(ctx context.Context, sigBlob []byte, sigMani } if len(capabilitiesToVerify) > 0 { - response, err := v.executePlugin(ctx, trustPolicy, capabilitiesToVerify, outcome.SignerInfo) + response, err := v.executePlugin(ctx, trustPolicy, capabilitiesToVerify, outcome.EnvelopeContent) if err != nil { return err } @@ -232,10 +240,12 @@ func (v *Verifier) processSignature(ctx context.Context, sigBlob []byte, sigMani } func (v *Verifier) processPluginResponse(capabilitiesToVerify []plugin.VerificationCapability, response *plugin.VerifySignatureResponse, outcome *SignatureVerificationOutcome) error { - verificationPluginName := outcome.SignerInfo.SignedAttributes.VerificationPlugin - + verificationPluginName, err := getVerificationPlugin(&outcome.EnvelopeContent.SignerInfo) + if err != nil { + return err + } // verify all extended critical attributes are processed by the plugin - for _, attr := range outcome.SignerInfo.SignedAttributes.ExtendedAttributes { + for _, attr := range outcome.EnvelopeContent.SignerInfo.SignedAttributes.ExtendedAttributes { if attr.Critical { if !isPresent(attr.Key, response.ProcessedAttributes) { return fmt.Errorf("extended critical attribute %q was not processed by the verification plugin %q (all extended critical attributes must be processed by the verification plugin)", attr.Key, verificationPluginName) diff --git a/verification/verifier_helpers.go b/verification/verifier_helpers.go index c090bf72..a0994944 100644 --- a/verification/verifier_helpers.go +++ b/verification/verifier_helpers.go @@ -3,23 +3,31 @@ package verification import ( "context" "crypto/x509" + "errors" "fmt" - nsigner "github.com/notaryproject/notation-core-go/signer" - "github.com/notaryproject/notation-go/plugin" - "github.com/notaryproject/notation-go/registry" + "regexp" "strings" "time" + + "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-go/plugin" + "github.com/notaryproject/notation-go/registry" + sig "github.com/notaryproject/notation-go/signature" ) +var errExtendedAttributeNotExist = errors.New("extended attribute not exist") + +var semVerRegEx = regexp.MustCompile(`^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$`) + // isCriticalFailure checks whether a VerificationResult fails the entire signature verification workflow. // signature verification workflow is considered failed if there is a VerificationResult with "Enforced" as the action but the result was unsuccessful func isCriticalFailure(result *VerificationResult) bool { return result.Action == Enforced && !result.Success } -func (v *Verifier) verifyIntegrity(sigBlob []byte, sigManifest registry.SignatureManifest, outcome *SignatureVerificationOutcome) (*nsigner.SignerInfo, *VerificationResult) { +func (v *Verifier) verifyIntegrity(sigBlob []byte, sigManifest registry.SignatureManifest, outcome *SignatureVerificationOutcome) (*signature.EnvelopeContent, *VerificationResult) { // parse the signature - sigEnv, err := nsigner.NewSignatureEnvelopeFromBytes(sigBlob, nsigner.SignatureMediaType(sigManifest.Blob.MediaType)) + sigEnv, err := signature.ParseEnvelope(sigManifest.Blob.MediaType, sigBlob) if err != nil { return nil, &VerificationResult{ Success: false, @@ -30,10 +38,10 @@ func (v *Verifier) verifyIntegrity(sigBlob []byte, sigManifest registry.Signatur } // verify integrity - signerInfo, err := sigEnv.Verify() + envContent, err := sigEnv.Verify() if err != nil { switch err.(type) { - case nsigner.SignatureNotFoundError, nsigner.MalformedSignatureError, nsigner.SignatureIntegrityError: + case *signature.SignatureEnvelopeNotFoundError, *signature.InvalidSignatureError, *signature.SignatureIntegrityError: return nil, &VerificationResult{ Success: false, Error: err, @@ -51,8 +59,17 @@ func (v *Verifier) verifyIntegrity(sigBlob []byte, sigManifest registry.Signatur } } + if err := sig.ValidatePayloadContentType(&envContent.Payload); err != nil { + return nil, &VerificationResult{ + Success: false, + Error: err, + Type: Integrity, + Action: outcome.VerificationLevel.VerificationMap[Integrity], + } + } + // integrity has been verified successfully - return signerInfo, &VerificationResult{ + return envContent, &VerificationResult{ Success: true, Type: Integrity, Action: outcome.VerificationLevel.VerificationMap[Integrity], @@ -61,7 +78,7 @@ func (v *Verifier) verifyIntegrity(sigBlob []byte, sigManifest registry.Signatur func (v *Verifier) verifyAuthenticity(trustPolicy *TrustPolicy, outcome *SignatureVerificationOutcome) *VerificationResult { // verify authenticity - trustStores, err := loadX509TrustStores(outcome.SignerInfo.SigningScheme, trustPolicy, v.PathManager) + trustStores, err := loadX509TrustStores(outcome.EnvelopeContent.SignerInfo.SignedAttributes.SigningScheme, trustPolicy, v.PathManager) if err != nil { return &VerificationResult{ @@ -76,7 +93,6 @@ func (v *Verifier) verifyAuthenticity(trustPolicy *TrustPolicy, outcome *Signatu for _, v := range trustStores { trustCerts = append(trustCerts, v.Certificates...) } - if len(trustCerts) < 1 { return &VerificationResult{ Success: false, @@ -85,11 +101,10 @@ func (v *Verifier) verifyAuthenticity(trustPolicy *TrustPolicy, outcome *Signatu Action: outcome.VerificationLevel.VerificationMap[Authenticity], } } - - _, err = nsigner.VerifyAuthenticity(outcome.SignerInfo, trustCerts) + _, err = signature.VerifyAuthenticity(&outcome.EnvelopeContent.SignerInfo, trustCerts) if err != nil { switch err.(type) { - case nsigner.SignatureAuthenticityError: + case *signature.SignatureAuthenticityError: return &VerificationResult{ Success: false, Error: err, @@ -114,10 +129,10 @@ func (v *Verifier) verifyAuthenticity(trustPolicy *TrustPolicy, outcome *Signatu } func (v *Verifier) verifyExpiry(outcome *SignatureVerificationOutcome) *VerificationResult { - if !outcome.SignerInfo.SignedAttributes.Expiry.IsZero() && !time.Now().Before(outcome.SignerInfo.SignedAttributes.Expiry) { + if expiry := outcome.EnvelopeContent.SignerInfo.SignedAttributes.Expiry; !expiry.IsZero() && !time.Now().Before(expiry) { return &VerificationResult{ Success: false, - Error: fmt.Errorf("digital signature has expired on %q", outcome.SignerInfo.SignedAttributes.Expiry.Format(time.RFC1123Z)), + Error: fmt.Errorf("digital signature has expired on %q", expiry.Format(time.RFC1123Z)), Type: Expiry, Action: outcome.VerificationLevel.VerificationMap[Expiry], } @@ -134,13 +149,13 @@ func (v *Verifier) verifyAuthenticTimestamp(outcome *SignatureVerificationOutcom invalidTimestamp := false var err error - if outcome.SignerInfo.SigningScheme == nsigner.SigningSchemeX509 { + if signerInfo := outcome.EnvelopeContent.SignerInfo; signerInfo.SignedAttributes.SigningScheme == signature.SigningSchemeX509 { // TODO verify RFC3161 TSA signature if present (not in RC1) // https://github.com/notaryproject/notation-go/issues/78 - if len(outcome.SignerInfo.TimestampSignature) == 0 { + if len(signerInfo.UnsignedAttributes.TimestampSignature) == 0 { // if there is no TSA signature, then every certificate should be valid at the time of verification now := time.Now() - for _, cert := range outcome.SignerInfo.CertificateChain { + for _, cert := range signerInfo.CertificateChain { if now.Before(cert.NotBefore) { invalidTimestamp = true err = fmt.Errorf("certificate %q is not valid yet, it will be valid from %q", cert.Subject, cert.NotBefore.Format(time.RFC1123Z)) @@ -153,11 +168,11 @@ func (v *Verifier) verifyAuthenticTimestamp(outcome *SignatureVerificationOutcom } } } - } else if outcome.SignerInfo.SigningScheme == nsigner.SigningSchemeX509SigningAuthority { - authenticSigningTime := outcome.SignerInfo.SignedAttributes.SigningTime + } else if signerInfo.SignedAttributes.SigningScheme == signature.SigningSchemeX509SigningAuthority { + authenticSigningTime := signerInfo.SignedAttributes.SigningTime // TODO use authenticSigningTime from signerInfo // https://github.com/notaryproject/notation-core-go/issues/38 - for _, cert := range outcome.SignerInfo.CertificateChain { + for _, cert := range signerInfo.CertificateChain { if authenticSigningTime.Before(cert.NotBefore) || authenticSigningTime.After(cert.NotAfter) { invalidTimestamp = true err = fmt.Errorf("certificate %q was not valid when the digital signature was produced at %q", cert.Subject, authenticSigningTime.Format(time.RFC1123Z)) @@ -185,7 +200,7 @@ func (v *Verifier) verifyAuthenticTimestamp(outcome *SignatureVerificationOutcom // verifyX509TrustedIdentities verified x509 trusted identities. This functions uses the VerificationResult from x509 trust store verification and modifies it func (v *Verifier) verifyX509TrustedIdentities(trustPolicy *TrustPolicy, outcome *SignatureVerificationOutcome, authenticityResult *VerificationResult) { // verify trusted identities - err := verifyX509TrustedIdentities(outcome.SignerInfo.CertificateChain, trustPolicy) + err := verifyX509TrustedIdentities(outcome.EnvelopeContent.SignerInfo.CertificateChain, trustPolicy) if err != nil { authenticityResult.Success = false authenticityResult.Error = err @@ -232,8 +247,12 @@ func verifyX509TrustedIdentities(certs []*x509.Certificate, trustPolicy *TrustPo return fmt.Errorf("signing certificate from the digital signature does not match the X.509 trusted identities %q defined in the trust policy %q", trustedX509Identities, trustPolicy.Name) } -func (v *Verifier) executePlugin(ctx context.Context, trustPolicy *TrustPolicy, capabilitiesToVerify []plugin.VerificationCapability, signerInfo *nsigner.SignerInfo) (*plugin.VerifySignatureResponse, error) { - verificationPluginName := signerInfo.SignedAttributes.VerificationPlugin +func (v *Verifier) executePlugin(ctx context.Context, trustPolicy *TrustPolicy, capabilitiesToVerify []plugin.VerificationCapability, envelopeContent *signature.EnvelopeContent) (*plugin.VerifySignatureResponse, error) { + signerInfo, payloadInfo := &envelopeContent.SignerInfo, envelopeContent.Payload + verificationPluginName, err := getVerificationPlugin(signerInfo) + if err != nil { + return nil, err + } var attributesToProcess []string extendedAttributes := make(map[string]interface{}) @@ -250,7 +269,7 @@ func (v *Verifier) executePlugin(ctx context.Context, trustPolicy *TrustPolicy, certChain = append(certChain, cert.Raw) } var authenticSigningTime *time.Time - if signerInfo.SigningScheme == nsigner.SigningSchemeX509SigningAuthority { + if signerInfo.SignedAttributes.SigningScheme == signature.SigningSchemeX509SigningAuthority { authenticSigningTime = &signerInfo.SignedAttributes.SigningTime // TODO use authenticSigningTime from signerInfo // https://github.com/notaryproject/notation-core-go/issues/38 @@ -258,13 +277,11 @@ func (v *Verifier) executePlugin(ctx context.Context, trustPolicy *TrustPolicy, signature := plugin.Signature{ CriticalAttributes: plugin.CriticalAttributes{ - ContentType: string(signerInfo.PayloadContentType), - SigningScheme: string(signerInfo.SigningScheme), - Expiry: &signerInfo.SignedAttributes.Expiry, - AuthenticSigningTime: authenticSigningTime, - VerificationPlugin: signerInfo.SignedAttributes.VerificationPlugin, - VerificationPluginMinVersion: signerInfo.SignedAttributes.VerificationPluginMinVersion, - ExtendedAttributes: extendedAttributes, + ContentType: payloadInfo.ContentType, + SigningScheme: string(signerInfo.SignedAttributes.SigningScheme), + Expiry: &signerInfo.SignedAttributes.Expiry, + AuthenticSigningTime: authenticSigningTime, + ExtendedAttributes: extendedAttributes, }, UnprocessedAttributes: attributesToProcess, CertificateChain: certChain, @@ -275,12 +292,11 @@ func (v *Verifier) executePlugin(ctx context.Context, trustPolicy *TrustPolicy, SignatureVerification: capabilitiesToVerify, } - pluginConfig := map[string]string{} request := &plugin.VerifySignatureRequest{ ContractVersion: plugin.ContractVersion, Signature: signature, TrustPolicy: policy, - PluginConfig: getPluginConfig(ctx, pluginConfig), + PluginConfig: getPluginConfig(ctx), } pluginRunner, err := v.PluginManager.Runner(verificationPluginName) if err != nil { @@ -298,3 +314,51 @@ func (v *Verifier) executePlugin(ctx context.Context, trustPolicy *TrustPolicy, return response, nil } + +// extractCriticalStringExtendedAttribute extracts a critical string Extended attribute from a signer. +func extractCriticalStringExtendedAttribute(signerInfo *signature.SignerInfo, key string) (string, error) { + attr, err := signerInfo.ExtendedAttribute(key) + // not exist + if err != nil { + return "", errExtendedAttributeNotExist + } + // not critical + if !attr.Critical { + return "", fmt.Errorf("%v is not a critical Extended attribute", key) + } + // not string + val, ok := attr.Value.(string) + if !ok { + return "", fmt.Errorf("%v from extended attribute is not a string", key) + } + return val, nil +} + +// getVerificationPlugin get plugin name from the Extended attributes. +func getVerificationPlugin(signerInfo *signature.SignerInfo) (string, error) { + name, err := extractCriticalStringExtendedAttribute(signerInfo, VerificationPlugin) + if err != nil { + return "", err + } + // not an empty string + if strings.TrimSpace(name) == "" { + return "", fmt.Errorf("%v from extended attribute is an empty string", VerificationPlugin) + } + return name, nil +} + +// getVerificationPlugin get plugin version from the Extended attributes. +func getVerificationPluginMinVersion(signerInfo *signature.SignerInfo) (string, error) { + version, err := extractCriticalStringExtendedAttribute(signerInfo, VerificationPluginMinVersion) + if err != nil { + return "", err + } + // empty version + if strings.TrimSpace(version) == "" { + return "", fmt.Errorf("%v from extended attribute is an empty string", VerificationPluginMinVersion) + } + if !semVerRegEx.MatchString(version) { + return "", fmt.Errorf("%v from extended attribute is not a valid SemVer", VerificationPluginMinVersion) + } + return version, nil +} diff --git a/verification/verifier_test.go b/verification/verifier_test.go index d4d7af70..ba5ede47 100644 --- a/verification/verifier_test.go +++ b/verification/verifier_test.go @@ -4,15 +4,18 @@ import ( "context" "errors" "fmt" - nsigner "github.com/notaryproject/notation-core-go/signer" + "strconv" + "testing" + + "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-go" "github.com/notaryproject/notation-go/dir" "github.com/notaryproject/notation-go/internal/mock" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/manager" "github.com/notaryproject/notation-go/registry" - "strconv" - "testing" + + _ "github.com/notaryproject/notation-core-go/signature/jws" ) func verifyResult(outcome *SignatureVerificationOutcome, expectedResult VerificationResult, expectedErr error, t *testing.T) { @@ -182,20 +185,20 @@ func TestRegistryGetBlobError(t *testing.T) { } func TestNotationVerificationCombinations(t *testing.T) { - assertNotationVerification(t, nsigner.SigningSchemeX509) - assertNotationVerification(t, nsigner.SigningSchemeX509SigningAuthority) + assertNotationVerification(t, signature.SigningSchemeX509) + assertNotationVerification(t, signature.SigningSchemeX509SigningAuthority) } -func assertNotationVerification(t *testing.T, scheme nsigner.SigningScheme) { +func assertNotationVerification(t *testing.T, scheme signature.SigningScheme) { var validSigEnv []byte var invalidSigEnv []byte var expiredSigEnv []byte - if scheme == nsigner.SigningSchemeX509 { + if scheme == signature.SigningSchemeX509 { validSigEnv = mock.MockCaValidSigEnv invalidSigEnv = mock.MockCaInvalidSigEnv expiredSigEnv = mock.MockCaExpiredSigEnv - } else if scheme == nsigner.SigningSchemeX509SigningAuthority { + } else if scheme == signature.SigningSchemeX509SigningAuthority { validSigEnv = mock.MockSaValidSigEnv invalidSigEnv = mock.MockSaInvalidSigEnv expiredSigEnv = mock.MockSaExpiredSigEnv @@ -382,15 +385,15 @@ func assertNotationVerification(t *testing.T, scheme nsigner.SigningScheme) { } func TestVerificationPluginInteractions(t *testing.T) { - assertPluginVerification(nsigner.SigningSchemeX509, t) - assertPluginVerification(nsigner.SigningSchemeX509SigningAuthority, t) + assertPluginVerification(signature.SigningSchemeX509, t) + assertPluginVerification(signature.SigningSchemeX509SigningAuthority, t) } -func assertPluginVerification(scheme nsigner.SigningScheme, t *testing.T) { +func assertPluginVerification(scheme signature.SigningScheme, t *testing.T) { var pluginSigEnv []byte - if scheme == nsigner.SigningSchemeX509 { + if scheme == signature.SigningSchemeX509 { pluginSigEnv = mock.MockCaPluginSigEnv - } else if scheme == nsigner.SigningSchemeX509SigningAuthority { + } else if scheme == signature.SigningSchemeX509SigningAuthority { pluginSigEnv = mock.MockSaPluginSigEnv } @@ -442,7 +445,7 @@ func assertPluginVerification(scheme nsigner.SigningScheme, t *testing.T) { Success: true, }, }, - ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key}, + ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key, VerificationPlugin}, } verifier = Verifier{ @@ -466,7 +469,7 @@ func assertPluginVerification(scheme nsigner.SigningScheme, t *testing.T) { Reason: "i feel like failing today", }, }, - ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key}, + ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key, VerificationPlugin}, } verifier = Verifier{ @@ -489,7 +492,7 @@ func assertPluginVerification(scheme nsigner.SigningScheme, t *testing.T) { Success: true, }, }, - ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key}, + ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key, VerificationPlugin}, } verifier = Verifier{ @@ -513,7 +516,7 @@ func assertPluginVerification(scheme nsigner.SigningScheme, t *testing.T) { Reason: "i feel like failing today", }, }, - ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key}, + ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key, VerificationPlugin}, } verifier = Verifier{ @@ -539,7 +542,7 @@ func assertPluginVerification(scheme nsigner.SigningScheme, t *testing.T) { Success: true, }, }, - ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key}, + ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key, VerificationPlugin}, } verifier = Verifier{ @@ -582,7 +585,6 @@ func assertPluginVerification(scheme nsigner.SigningScheme, t *testing.T) { PathManager: path, } outcomes, err = verifier.Verify(context.Background(), mock.SampleArtifactUri) - outcomes, err = verifier.Verify(context.Background(), mock.SampleArtifactUri) if err == nil || outcomes[0].Error == nil || outcomes[0].Error.Error() != "verification plugin \"plugin-name\" returned unexpected response : \"invalid plugin response\"" { t.Fatalf("verification should fail when the verification plugin returns unexpected response. error : %v", outcomes[0].Error) } @@ -596,7 +598,7 @@ func assertPluginVerification(scheme nsigner.SigningScheme, t *testing.T) { Success: true, }, }, - ProcessedAttributes: []string{}, // exclude the critical attribute + ProcessedAttributes: []string{VerificationPlugin}, // exclude the critical attribute } verifier = Verifier{ @@ -615,7 +617,7 @@ func assertPluginVerification(scheme nsigner.SigningScheme, t *testing.T) { pluginManager.PluginCapabilities = []plugin.Capability{plugin.CapabilityTrustedIdentityVerifier} pluginManager.PluginRunnerExecuteResponse = &plugin.VerifySignatureResponse{ VerificationResults: map[plugin.VerificationCapability]*plugin.VerificationResult{}, - ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key}, + ProcessedAttributes: []string{mock.PluginExtendedCriticalAttribute.Key, VerificationPlugin}, } verifier = Verifier{