Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for signing blob #379

Merged
merged 15 commits into from
Mar 14, 2024
4 changes: 2 additions & 2 deletions example_localSign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ func Example_localSign() {
// Users should replace `exampleCertTuple.PrivateKey` with their own private
// key and replace `exampleCerts` with the corresponding full certificate
// chain, following the Notary certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
exampleSigner, err := signer.New(exampleCertTuple.PrivateKey, exampleCerts)
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
if err != nil {
panic(err) // Handle error
}
Expand Down
7 changes: 4 additions & 3 deletions example_remoteSign_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ import (
"crypto/x509"
"fmt"

"oras.land/oras-go/v2/registry/remote"

"github.com/notaryproject/notation-core-go/signature/cose"
"github.com/notaryproject/notation-core-go/testhelper"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/signer"
"oras.land/oras-go/v2/registry/remote"
)

// Both COSE ("application/cose") and JWS ("application/jose+json")
Expand All @@ -45,8 +46,8 @@ func Example_remoteSign() {
// Users should replace `exampleCertTuple.PrivateKey` with their own private
// key and replace `exampleCerts` with the corresponding full certificate
// chain, following the Notary certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
exampleSigner, err := signer.New(exampleCertTuple.PrivateKey, exampleCerts)
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
if err != nil {
panic(err) // Handle error
}
Expand Down
113 changes: 97 additions & 16 deletions notation.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,22 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"strings"
"time"

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"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
orasRegistry "oras.land/oras-go/v2/registry"
)

var errDoneVerification = errors.New("done verification")
Expand All @@ -41,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

Expand All @@ -56,15 +61,37 @@ 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 is the media-type of the blob being signed.
ContentMediaType string
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
// UserMetadata contains key-value pairs that are added to the signature
// payload
UserMetadata map[string]string
}

// BlobDescriptorGenerator creates descriptor using the digest Algorithm.
type BlobDescriptorGenerator func(digest.Algorithm) (ocispec.Descriptor, error)
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved

// 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 {
priteshbandi marked this conversation as resolved.
Show resolved Hide resolved
Two-Hearts marked this conversation as resolved.
Show resolved Hide resolved
// 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
type signerAnnotation interface {
// PluginAnnotations returns signature manifest annotations returned from
Expand All @@ -85,22 +112,16 @@ type SignOptions struct {
UserMetadata map[string]string
}

// Sign signs the artifact and push the signature to the Repository.
// The descriptor of the sign content is returned upon sucessful signing.
// 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 check
if signer == nil {
return ocispec.Descriptor{}, errors.New("signer cannot be nil")
if err := validateSignArguments(signer, signOpts.SignerSignOptions); err != nil {
return ocispec.Descriptor{}, err
}
if repo == nil {
return ocispec.Descriptor{}, errors.New("repo cannot be nil")
}
if signOpts.ExpiryDuration < 0 {
return ocispec.Descriptor{}, fmt.Errorf("expiry duration cannot be a negative value")
}
if signOpts.ExpiryDuration%time.Second != 0 {
return ocispec.Descriptor{}, fmt.Errorf("expiry duration supports minimum granularity of seconds")
}

logger := log.GetLogger(ctx)
artifactRef := signOpts.ArtifactReference
Expand Down Expand Up @@ -152,6 +173,50 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, signOpts
return targetDesc, nil
}

// SignBlob signs the arbitrary data and returns the signature
func SignBlob(ctx context.Context, signer BlobSigner, blobReader io.Reader, signBlobOpts SignBlobOptions) ([]byte, *signature.SignerInfo, error) {
// sanity checks
if err := validateSignArguments(signer, signBlobOpts.SignerSignOptions); err != nil {
return nil, nil, err
}

if blobReader == nil {
return nil, nil, errors.New("blobReader cannot be nil")
}

if signBlobOpts.ContentMediaType == "" {
return nil, nil, errors.New("content media-type cannot be empty")
}

if _, _, err := mime.ParseMediaType(signBlobOpts.ContentMediaType); err != nil {
return nil, nil, fmt.Errorf("invalid content media-type '%s': %v", signBlobOpts.ContentMediaType, err)
}

getDescFunc := getDescriptorFunc(ctx, blobReader, signBlobOpts.ContentMediaType, signBlobOpts.UserMetadata)
return signer.SignBlob(ctx, getDescFunc, signBlobOpts.SignerSignOptions)
}

func validateSignArguments(signer any, signOpts SignerSignOptions) error {
if signer == nil {
return errors.New("signer cannot be nil")
}
if signOpts.ExpiryDuration < 0 {
return errors.New("expiry duration cannot be a negative value")
}
if signOpts.ExpiryDuration%time.Second != 0 {
return errors.New("expiry duration supports minimum granularity of seconds")
}
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
}

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

Expand Down Expand Up @@ -236,7 +301,7 @@ func (outcome *VerificationOutcome) UserMetadata() (map[string]string, error) {

// VerifierVerifyOptions contains parameters for Verifier.Verify.
type VerifierVerifyOptions struct {
// ArtifactReference is the reference of the artifact that is been
// ArtifactReference is the reference of the artifact that is being
// verified against to. It must be a full reference.
ArtifactReference string

Expand Down Expand Up @@ -270,7 +335,7 @@ type verifySkipper interface {

// VerifyOptions contains parameters for notation.Verify.
type VerifyOptions struct {
// ArtifactReference is the reference of the artifact that is been
// ArtifactReference is the reference of the artifact that is being
// verified against to.
ArtifactReference string

Expand Down Expand Up @@ -449,3 +514,19 @@ func generateAnnotations(signerInfo *signature.SignerInfo, annotations map[strin
annotations[ocispec.AnnotationCreated] = signingTime.Format(time.RFC3339)
return annotations, nil
}

func getDescriptorFunc(ctx context.Context, reader io.Reader, contentMediaType string, userMetadata map[string]string) BlobDescriptorGenerator {
return func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) {
digester := hashAlgo.Digester()
bytes, err := io.Copy(digester.Hash(), reader)
if err != nil {
return ocispec.Descriptor{}, err
}
targetDesc := ocispec.Descriptor{
MediaType: contentMediaType,
Digest: digester.Digest(),
Size: bytes,
}
return addUserMetadataToDescriptor(ctx, targetDesc, userMetadata)
}
}
109 changes: 108 additions & 1 deletion notation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,22 @@ import (
"context"
"errors"
"fmt"
"io"
"math"
"os"
"path/filepath"
"strings"
"testing"
"time"

"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/mock"
"github.com/notaryproject/notation-go/plugin"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)

Expand All @@ -47,6 +51,7 @@ func TestSignSuccess(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(b *testing.T) {
opts := SignOptions{}
opts.SignatureMediaType = jws.MediaTypeEnvelope
opts.ExpiryDuration = tc.dur
opts.ArtifactReference = mock.SampleArtifactUri

Expand All @@ -58,11 +63,91 @@ func TestSignSuccess(t *testing.T) {
}
}

func TestSignBlobSuccess(t *testing.T) {
reader := strings.NewReader("some content")
testCases := []struct {
name string
dur time.Duration
mtype string
agent string
pConfig map[string]string
metadata map[string]string
}{
{"expiryInHours", 24 * time.Hour, "video/mp4", "", nil, nil},
{"oneSecondExpiry", 1 * time.Second, "video/mp4", "", nil, nil},
{"zeroExpiry", 0, "video/mp4", "", nil, nil},
{"validContentType", 1 * time.Second, "video/mp4", "", nil, nil},
{"emptyContentType", 1 * time.Second, "video/mp4", "someDummyAgent", map[string]string{"hi": "hello"}, map[string]string{"bye": "tata"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(b *testing.T) {
opts := SignBlobOptions{
SignerSignOptions: SignerSignOptions{
SignatureMediaType: jws.MediaTypeEnvelope,
ExpiryDuration: tc.dur,
PluginConfig: tc.pConfig,
SigningAgent: tc.agent,
},
UserMetadata: expectedMetadata,
ContentMediaType: tc.mtype,
}

_, _, err := SignBlob(context.Background(), &dummySigner{}, reader, opts)
if err != nil {
b.Fatalf("Sign failed with error: %v", err)
}
})
}
}

func TestSignBlobError(t *testing.T) {
reader := strings.NewReader("some content")
testCases := []struct {
name string
signer BlobSigner
dur time.Duration
rdr io.Reader
sigMType string
ctMType string
errMsg string
}{
{"negativeExpiry", &dummySigner{}, -1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration cannot be a negative value"},
{"milliSecExpiry", &dummySigner{}, 1 * time.Millisecond, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration supports minimum granularity of seconds"},
{"invalidContentMediaType", &dummySigner{}, 1 * time.Second, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type 'video/mp4/zoping': mime: unexpected content after media subtype"},
{"emptyContentMediaType", &dummySigner{}, 1 * time.Second, reader, "", jws.MediaTypeEnvelope, "content media-type cannot be empty"},
{"invalidSignatureMediaType", &dummySigner{}, 1 * time.Second, reader, "", "", "content media-type cannot be empty"},
{"nilReader", &dummySigner{}, 1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"},
{"nilSigner", nil, 1 * time.Second, reader, "video/mp4", jws.MediaTypeEnvelope, "signer cannot be nil"},
{"signerError", &dummySigner{fail: true}, 1 * time.Second, reader, "video/mp4", jws.MediaTypeEnvelope, "expected SignBlob failure"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := SignBlobOptions{
SignerSignOptions: SignerSignOptions{
SignatureMediaType: jws.MediaTypeEnvelope,
ExpiryDuration: tc.dur,
PluginConfig: nil,
},
ContentMediaType: tc.sigMType,
}

_, _, err := SignBlob(context.Background(), tc.signer, tc.rdr, opts)
if err == nil {
t.Fatalf("expected error but didnt found")
}
if err.Error() != tc.errMsg {
t.Fatalf("expected err message to be '%s' but found '%s'", tc.errMsg, err.Error())
}
})
}
}

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

_, err := Sign(context.Background(), &verifyMetadataSigner{}, repo, opts)
if err != nil {
Expand Down Expand Up @@ -182,6 +267,9 @@ func TestSignDigestNotMatchResolve(t *testing.T) {
repo := mock.NewRepository()
repo.MissMatchDigest = true
signOpts := SignOptions{
SignerSignOptions: SignerSignOptions{
SignatureMediaType: jws.MediaTypeEnvelope,
},
ArtifactReference: mock.SampleArtifactUri,
}

Expand Down Expand Up @@ -320,7 +408,9 @@ func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
return
}

type dummySigner struct{}
type dummySigner struct {
fail bool
}

func (s *dummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
return []byte("ABC"), &signature.SignerInfo{
Expand All @@ -330,6 +420,23 @@ func (s *dummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts Si
}, nil
}

func (s *dummySigner) SignBlob(_ context.Context, descGenFunc BlobDescriptorGenerator, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
if s.fail {
return nil, nil, errors.New("expected SignBlob failure")
}

_, err := descGenFunc(digest.SHA384)
if err != nil {
return nil, nil, err
}

return []byte("ABC"), &signature.SignerInfo{
SignedAttributes: signature.SignedAttributes{
SigningTime: time.Now(),
},
}, nil
}

type verifyMetadataSigner struct{}

func (s *verifyMetadataSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
Expand Down
Loading
Loading