diff --git a/notation.go b/notation.go index 868d7d62..8b6ebb8b 100644 --- a/notation.go +++ b/notation.go @@ -30,6 +30,8 @@ import ( orasRegistry "oras.land/oras-go/v2/registry" "github.com/notaryproject/notation-core-go/signature" + "github.com/notaryproject/notation-core-go/signature/cose" + "github.com/notaryproject/notation-core-go/signature/jws" "github.com/notaryproject/notation-go/internal/envelope" "github.com/notaryproject/notation-go/log" "github.com/notaryproject/notation-go/registry" @@ -44,7 +46,7 @@ var reservedAnnotationPrefixes = [...]string{"io.cncf.notary"} // SignerSignOptions contains parameters for Signer.Sign. type SignerSignOptions struct { // SignatureMediaType is the envelope type of the signature. - // Currently both `application/jose+json` and `application/cose` are + // Currently, both `application/jose+json` and `application/cose` are // supported. SignatureMediaType string @@ -59,19 +61,34 @@ type SignerSignOptions struct { SigningAgent string } -// Signer is a generic interface for signing an artifact. +// Signer is a generic interface for signing an OCI artifact. // The interface allows signing with local or remote keys, // and packing in various signature formats. type Signer interface { - // Sign signs the artifact described by its descriptor, + // Sign signs the OCI artifact described by its descriptor, // and returns the signature and SignerInfo. Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) } +// SignBlobOptions contains parameters for notation.SignBlob. +type SignBlobOptions struct { + SignerSignOptions + + ContentMediaType string + // UserMetadata contains key-value pairs that are added to the signature + // payload + UserMetadata map[string]string +} + +type BlobDescriptorGenerator func(digest.Algorithm) (ocispec.Descriptor, error) + +// BlobSigner is a generic interface for signing arbitrary data. +// The interface allows signing with local or remote keys, +// and packing in various signature formats. type BlobSigner interface { - // SignBlob signs the artifact described by its descriptor, - // and returns the signature and SignerInfo. - SignBlob(ctx context.Context, reader io.Reader, opts SignBlobOptions) ([]byte, *signature.SignerInfo, error) + // SignBlob signs the descriptor returned by genDesc , + // and returns the signature and SignerInfo + SignBlob(ctx context.Context, genDesc BlobDescriptorGenerator, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) } // signerAnnotation facilitates return of manifest annotations by signers @@ -94,20 +111,10 @@ type SignOptions struct { UserMetadata map[string]string } -type SignBlobOptions struct { - SignerSignOptions - - ContentMediaType string - - // UserMetadata contains key-value pairs that are added to the signature - // payload - UserMetadata map[string]string -} - -// Sign signs the artifact and push the signature to the Repository. +// Sign signs the OCI artifact and push the signature to the Repository. // The descriptor of the sign content is returned upon successful signing. func Sign(ctx context.Context, signer Signer, repo registry.Repository, signOpts SignOptions) (ocispec.Descriptor, error) { - // sanity checks + // sanity check if err := validate(signer, signOpts.SignerSignOptions); err != nil { return ocispec.Descriptor{}, err } @@ -165,26 +172,41 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, signOpts return targetDesc, nil } -func SignBlob(ctx context.Context, signer BlobSigner, reader io.Reader, signOpts SignBlobOptions) ([]byte, error) { +// SignBlob signs the arbitrary data and returns the signature +func SignBlob(ctx context.Context, signer BlobSigner, reader io.Reader, signBlobOpts SignBlobOptions) ([]byte, *signature.SignerInfo, error) { // sanity checks - if err := validate(signer, signOpts.SignerSignOptions); err != nil { - return nil, err + if err := validate(signer, signBlobOpts.SignerSignOptions); err != nil { + return nil, nil, err } + if reader == nil { - return nil, errors.New("reader cannot be nil") + return nil, nil, errors.New("reader cannot be nil") } - if signOpts.ContentMediaType != "" { - if _, _, err := mime.ParseMediaType(signOpts.ContentMediaType); err != nil { - return nil, fmt.Errorf("invalid content media-type '%s': %v", signOpts.ContentMediaType, err) - } + + if signBlobOpts.ContentMediaType == "" { + return nil, nil, errors.New("content media-type cannot be empty") } - sig, _, err := signer.SignBlob(ctx, reader, signOpts) - if err != nil { - return nil, err + if _, _, err := mime.ParseMediaType(signBlobOpts.ContentMediaType); err != nil { + return nil, nil, fmt.Errorf("invalid content media-type '%s': %v", signBlobOpts.ContentMediaType, err) + } + + getDescFun := func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) { + h := hashAlgo.Hash() + bytes, err := io.Copy(hashAlgo.Hash(), reader) + if err != nil { + return ocispec.Descriptor{}, err + } + + targetDesc := ocispec.Descriptor{ + MediaType: signBlobOpts.ContentMediaType, + Digest: digest.NewDigest(hashAlgo, h), + Size: bytes, + } + return addUserMetadataToDescriptor(ctx, targetDesc, signBlobOpts.UserMetadata) } - return sig, nil + return signer.SignBlob(ctx, getDescFun, signBlobOpts.SignerSignOptions) } func validate(signer any, signOpts SignerSignOptions) error { @@ -192,14 +214,19 @@ func validate(signer any, signOpts SignerSignOptions) error { return errors.New("signer cannot be nil") } if signOpts.ExpiryDuration < 0 { - return fmt.Errorf("expiry duration cannot be a negative value") + return errors.New("expiry duration cannot be a negative value") } if signOpts.ExpiryDuration%time.Second != 0 { - return fmt.Errorf("expiry duration supports minimum granularity of seconds") + return errors.New("expiry duration supports minimum granularity of seconds") } - if _, _, err := mime.ParseMediaType(signOpts.SignatureMediaType); err != nil { - return fmt.Errorf("invalid signature media-type '%s': %v", signOpts.SignatureMediaType, err) + if signOpts.SignatureMediaType == "" { + return errors.New("signature media-type cannot be empty") } + + if !(signOpts.SignatureMediaType == jws.MediaTypeEnvelope || signOpts.SignatureMediaType == cose.MediaTypeEnvelope) { + return fmt.Errorf("invalid signature media-type '%s'", signOpts.SignatureMediaType) + } + return nil } diff --git a/notation_test.go b/notation_test.go index f63971c4..99c54879 100644 --- a/notation_test.go +++ b/notation_test.go @@ -77,7 +77,7 @@ func TestSignBlobSuccess(t *testing.T) { {"zeroExpiry", 0, "video/mp4", "", nil, nil}, //{"invalidContentMediaType", 1 * time.Second, "zap/zop/sop", nil, nil}, {"validContentType", 1 * time.Second, "video/mp4", "", nil, nil}, - {"emptyContentType", 1 * time.Second, "", "", nil, nil}, + //{"emptyContentType", 1 * time.Second, "", "", nil, nil}, {"emptyContentType", 1 * time.Second, "video/mp4", "someDummyAgent", map[string]string{"hi": "hello"}, map[string]string{"bye": "tata"}}, } for _, tc := range testCases { @@ -93,7 +93,7 @@ func TestSignBlobSuccess(t *testing.T) { ContentMediaType: tc.mtype, } - _, err := SignBlob(context.Background(), &dummySigner{}, reader, opts) + _, _, err := SignBlob(context.Background(), &dummySigner{}, reader, opts) if err != nil { b.Fatalf("Sign failed with error: %v", err) } @@ -128,7 +128,7 @@ func TestSignError(t *testing.T) { ContentMediaType: tc.mtype, } - _, err := SignBlob(context.Background(), tc.signer, tc.rdr, opts) + _, _, err := SignBlob(context.Background(), tc.signer, tc.rdr, opts) if err == nil { b.Error("expected error but didnt found") } @@ -414,7 +414,7 @@ func (s *dummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts Si }, nil } -func (s *dummySigner) SignBlob(ctx context.Context, reader io.Reader, opts SignBlobOptions) ([]byte, *signature.SignerInfo, error) { +func (s *dummySigner) SignBlob(ctx context.Context, genDesc BlobDescriptorGenerator, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) { if s.fail { return nil, nil, errors.New("expected SignBlob failure") } diff --git a/signer/plugin.go b/signer/plugin.go index e229275b..e7f36beb 100644 --- a/signer/plugin.go +++ b/signer/plugin.go @@ -85,9 +85,9 @@ func (s *PluginSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts n return nil, nil, err } - logger.Debugf("Using plugin %v with capabilities %v to sign artifact %v in signature media type %v", metadata.Name, metadata.Capabilities, desc.Digest, opts.SignatureMediaType) + logger.Debugf("Using plugin %v with capabilities %v to sign oci artifact %v in signature media type %v", metadata.Name, metadata.Capabilities, desc.Digest, opts.SignatureMediaType) if metadata.HasCapability(proto.CapabilitySignatureGenerator) { - // Get key info and validate. + logger.Debug("Invoking plugin's describe-key command") ks, err := s.getKeySpec(ctx, mergedConfig) if err != nil { return nil, nil, err @@ -100,7 +100,7 @@ func (s *PluginSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts n } // SignBlob signs the arbitrary data and returns the marshalled envelope. -func (s *PluginSigner) SignBlob(ctx context.Context, reader io.Reader, opts notation.SignBlobOptions) ([]byte, *signature.SignerInfo, error) { +func (s *PluginSigner) SignBlob(ctx context.Context, blobGenfunc notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) { logger := log.GetLogger(ctx) mergedConfig := s.mergeConfig(opts.PluginConfig) @@ -110,30 +110,33 @@ func (s *PluginSigner) SignBlob(ctx context.Context, reader io.Reader, opts nota return nil, nil, err } - // Get key info and validate. + logger.Debug("Invoking plugin's describe-key command") ks, err := s.getKeySpec(ctx, mergedConfig) if err != nil { return nil, nil, err } - desc, err := getDescriptor(reader, opts.ContentMediaType, ks) + digestAlg, ok := algorithms[ks.SignatureAlgorithm().Hash()] + if !ok { + return nil, nil, fmt.Errorf("unknown hashing algo %v", ks.SignatureAlgorithm()) + } + + // get descriptor to sign + desc, err := blobGenfunc(digestAlg) if err != nil { return nil, nil, err } - logger.Debugf("Using plugin %v with capabilities %v to sign artifact using descriptor %+v", metadata.Name, metadata.Capabilities, desc) + logger.Debugf("Using plugin %v with capabilities %v to sign blob using descriptor %+v", metadata.Name, metadata.Capabilities, desc) if metadata.HasCapability(proto.CapabilitySignatureGenerator) { - return s.generateSignature(ctx, desc, opts.SignerSignOptions, ks, metadata) + return s.generateSignature(ctx, desc, opts, ks, metadata) } else if metadata.HasCapability(proto.CapabilityEnvelopeGenerator) { - return s.generateSignatureEnvelope(ctx, desc, opts.SignerSignOptions) + return s.generateSignatureEnvelope(ctx, desc, opts) } return nil, nil, fmt.Errorf("plugin does not have signing capabilities") } func (s *PluginSigner) getKeySpec(ctx context.Context, config map[string]string) (signature.KeySpec, error) { - // Get key info and validate. - logger := log.GetLogger(ctx) - logger.Debug("Invoking plugin's describe-key command") descKeyResp, err := s.describeKey(ctx, config) if err != nil { return signature.KeySpec{}, err @@ -149,7 +152,7 @@ func (s *PluginSigner) getKeySpec(ctx context.Context, config map[string]string) func (s *PluginSigner) generateSignature(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions, ks signature.KeySpec, metadata *proto.GetMetadataResponse) ([]byte, *signature.SignerInfo, error) { logger := log.GetLogger(ctx) logger.Debug("Generating signature by plugin") - genericSigner := genericSigner{ + genericSigner := GenericSigner{ Signer: &pluginPrimitiveSigner{ ctx: ctx, plugin: s.plugin, @@ -207,9 +210,9 @@ func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp return nil, nil, err } - content := envContent.Payload.Content + cnt := envContent.Payload.Content var signedPayload envelope.Payload - if err = json.Unmarshal(content, &signedPayload); err != nil { + if err = json.Unmarshal(cnt, &signedPayload); err != nil { return nil, nil, fmt.Errorf("signed envelope payload can't be unmarshalled: %w", err) } @@ -217,7 +220,7 @@ func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp return nil, nil, fmt.Errorf("during signing descriptor subject has changed from %+v to %+v", desc, signedPayload.TargetArtifact) } - if unknownAttributes := areUnknownAttributesAdded(content); len(unknownAttributes) != 0 { + if unknownAttributes := areUnknownAttributesAdded(cnt); len(unknownAttributes) != 0 { return nil, nil, fmt.Errorf("during signing, following unknown attributes were added to subject descriptor: %+q", unknownAttributes) } diff --git a/signer/plugin_test.go b/signer/plugin_test.go index 84576ee5..948c3f0f 100644 --- a/signer/plugin_test.go +++ b/signer/plugin_test.go @@ -32,6 +32,7 @@ import ( "github.com/notaryproject/notation-go/internal/envelope" "github.com/notaryproject/notation-go/plugin" "github.com/notaryproject/notation-go/plugin/proto" + "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -70,6 +71,16 @@ type mockPlugin struct { keySpec signature.KeySpec } +func getDescriptorFunc(throwError bool) func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) { + return func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) { + if throwError { + return ocispec.Descriptor{}, errors.New("") + } + return validSignDescriptor, nil + } + +} + func newMockPlugin(key crypto.PrivateKey, certs []*x509.Certificate, keySpec signature.KeySpec) *mockPlugin { return &mockPlugin{ key: key, @@ -314,13 +325,8 @@ func TestPluginSigner_SignBlob_Valid(t *testing.T) { pluginSigner := PluginSigner{ plugin: newMockPlugin(keyCert.key, keyCert.certs, keySpec), } - sOpts := notation.SignBlobOptions{ - SignerSignOptions: notation.SignerSignOptions{ - SignatureMediaType: envelopeType, - }, - } - - data, signerInfo, err := pluginSigner.SignBlob(context.Background(), strings.NewReader("some content"), sOpts) + validSignOpts.SignatureMediaType = envelopeType + data, signerInfo, err := pluginSigner.SignBlob(context.Background(), getDescriptorFunc(false), validSignOpts) basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err) }) } diff --git a/signer/signer.go b/signer/signer.go index a9743604..b985ee26 100644 --- a/signer/signer.go +++ b/signer/signer.go @@ -24,7 +24,6 @@ import ( "encoding/json" "errors" "fmt" - "io" "time" "github.com/notaryproject/notation-core-go/signature" @@ -37,24 +36,24 @@ import ( // signingAgent is the unprotected header field used by signature. const signingAgent = "Notation/1.0.0" -// genericSigner implements notation.Signer and embeds signature.Signer -type genericSigner struct { +// GenericSigner implements notation.Signer and embeds signature.Signer +type GenericSigner struct { signature.Signer } // New returns a builtinSigner given key and cert chain -func New(key crypto.PrivateKey, certChain []*x509.Certificate) (*genericSigner, error) { +func New(key crypto.PrivateKey, certChain []*x509.Certificate) (*GenericSigner, error) { localSigner, err := signature.NewLocalSigner(certChain, key) if err != nil { return nil, err } - return &genericSigner{ + return &GenericSigner{ Signer: localSigner, }, nil } // NewFromFiles returns a builtinSigner given key and certChain paths. -func NewFromFiles(keyPath, certChainPath string) (*genericSigner, error) { +func NewFromFiles(keyPath, certChainPath string) (*GenericSigner, error) { if keyPath == "" { return nil, errors.New("key path not specified") } @@ -86,7 +85,7 @@ func NewFromFiles(keyPath, certChainPath string) (*genericSigner, error) { // Sign signs the artifact described by its descriptor and returns the // marshalled envelope. -func (s *genericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) { +func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) { logger := log.GetLogger(ctx) logger.Debugf("Generic signing for %v in signature media type %v", desc.Digest, opts.SignatureMediaType) // Generate payload to be signed. @@ -148,9 +147,7 @@ func (s *genericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts return sig, &envContent.SignerInfo, nil } -// SignBlob signs the artifact described by its descriptor and returns the -// marshalled envelope. -func (s *genericSigner) SignBlob(ctx context.Context, reader io.Reader, opts notation.SignBlobOptions) ([]byte, *signature.SignerInfo, error) { +func (s *GenericSigner) SignBlob(ctx context.Context, blobGen notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) { logger := log.GetLogger(ctx) logger.Debugf("Generic blob signing for signature media type %v", opts.SignatureMediaType) @@ -159,10 +156,15 @@ func (s *genericSigner) SignBlob(ctx context.Context, reader io.Reader, opts not return nil, nil, err } - desc, err := getDescriptor(reader, opts.ContentMediaType, ks) + digestAlg, ok := algorithms[ks.SignatureAlgorithm().Hash()] + if !ok { + return nil, nil, fmt.Errorf("unknown hashing algo %v", ks.SignatureAlgorithm().Hash()) + } + + desc, err := blobGen(digestAlg) if err != nil { return nil, nil, err } - return s.Sign(ctx, desc, opts.SignerSignOptions) + return s.Sign(ctx, desc, opts) } diff --git a/signer/signer_test.go b/signer/signer_test.go index 75f823d6..61da89dd 100644 --- a/signer/signer_test.go +++ b/signer/signer_test.go @@ -27,7 +27,6 @@ import ( "os" "path/filepath" "regexp" - "strings" "testing" "time" @@ -203,12 +202,10 @@ func TestSignBlobWithCertChain(t *testing.T) { t.Fatalf("NewSigner() error = %v", err) } - sOpts := notation.SignBlobOptions{ - SignerSignOptions: notation.SignerSignOptions{ - SignatureMediaType: envelopeType, - }, + sOpts := notation.SignerSignOptions{ + SignatureMediaType: envelopeType, } - sig, _, err := s.SignBlob(context.Background(), strings.NewReader("some content"), sOpts) + sig, _, err := s.SignBlob(context.Background(), getDescriptorFunc(false), sOpts) if err != nil { t.Fatalf("Sign() error = %v", err) }