Skip to content

Commit ec42378

Browse files
authored
feat: add support for signing blob (#379)
This PR adds support for signing blobs. Signed-off-by: Pritesh Bandi <[email protected]>
1 parent 7fa8404 commit ec42378

8 files changed

+472
-83
lines changed

example_localSign_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ func Example_localSign() {
4747
// Users should replace `exampleCertTuple.PrivateKey` with their own private
4848
// key and replace `exampleCerts` with the corresponding full certificate
4949
// chain, following the Notary certificate requirements:
50-
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
51-
exampleSigner, err := signer.New(exampleCertTuple.PrivateKey, exampleCerts)
50+
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
51+
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
5252
if err != nil {
5353
panic(err) // Handle error
5454
}

example_remoteSign_test.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ import (
1818
"crypto/x509"
1919
"fmt"
2020

21+
"oras.land/oras-go/v2/registry/remote"
22+
2123
"github.com/notaryproject/notation-core-go/signature/cose"
2224
"github.com/notaryproject/notation-core-go/testhelper"
2325
"github.com/notaryproject/notation-go"
2426
"github.com/notaryproject/notation-go/registry"
2527
"github.com/notaryproject/notation-go/signer"
26-
"oras.land/oras-go/v2/registry/remote"
2728
)
2829

2930
// Both COSE ("application/cose") and JWS ("application/jose+json")
@@ -45,8 +46,8 @@ func Example_remoteSign() {
4546
// Users should replace `exampleCertTuple.PrivateKey` with their own private
4647
// key and replace `exampleCerts` with the corresponding full certificate
4748
// chain, following the Notary certificate requirements:
48-
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
49-
exampleSigner, err := signer.New(exampleCertTuple.PrivateKey, exampleCerts)
49+
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
50+
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
5051
if err != nil {
5152
panic(err) // Handle error
5253
}

notation.go

+97-16
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,22 @@ import (
2222
"encoding/json"
2323
"errors"
2424
"fmt"
25+
"io"
26+
"mime"
2527
"strings"
2628
"time"
2729

30+
orasRegistry "oras.land/oras-go/v2/registry"
31+
2832
"github.com/notaryproject/notation-core-go/signature"
33+
"github.com/notaryproject/notation-core-go/signature/cose"
34+
"github.com/notaryproject/notation-core-go/signature/jws"
2935
"github.com/notaryproject/notation-go/internal/envelope"
3036
"github.com/notaryproject/notation-go/log"
3137
"github.com/notaryproject/notation-go/registry"
3238
"github.com/notaryproject/notation-go/verifier/trustpolicy"
3339
"github.com/opencontainers/go-digest"
3440
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
35-
orasRegistry "oras.land/oras-go/v2/registry"
3641
)
3742

3843
var errDoneVerification = errors.New("done verification")
@@ -41,7 +46,7 @@ var reservedAnnotationPrefixes = [...]string{"io.cncf.notary"}
4146
// SignerSignOptions contains parameters for Signer.Sign.
4247
type SignerSignOptions struct {
4348
// SignatureMediaType is the envelope type of the signature.
44-
// Currently both `application/jose+json` and `application/cose` are
49+
// Currently, both `application/jose+json` and `application/cose` are
4550
// supported.
4651
SignatureMediaType string
4752

@@ -56,15 +61,37 @@ type SignerSignOptions struct {
5661
SigningAgent string
5762
}
5863

59-
// Signer is a generic interface for signing an artifact.
64+
// Signer is a generic interface for signing an OCI artifact.
6065
// The interface allows signing with local or remote keys,
6166
// and packing in various signature formats.
6267
type Signer interface {
63-
// Sign signs the artifact described by its descriptor,
68+
// Sign signs the OCI artifact described by its descriptor,
6469
// and returns the signature and SignerInfo.
6570
Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error)
6671
}
6772

73+
// SignBlobOptions contains parameters for notation.SignBlob.
74+
type SignBlobOptions struct {
75+
SignerSignOptions
76+
// ContentMediaType is the media-type of the blob being signed.
77+
ContentMediaType string
78+
// UserMetadata contains key-value pairs that are added to the signature
79+
// payload
80+
UserMetadata map[string]string
81+
}
82+
83+
// BlobDescriptorGenerator creates descriptor using the digest Algorithm.
84+
type BlobDescriptorGenerator func(digest.Algorithm) (ocispec.Descriptor, error)
85+
86+
// BlobSigner is a generic interface for signing arbitrary data.
87+
// The interface allows signing with local or remote keys,
88+
// and packing in various signature formats.
89+
type BlobSigner interface {
90+
// SignBlob signs the descriptor returned by genDesc ,
91+
// and returns the signature and SignerInfo
92+
SignBlob(ctx context.Context, genDesc BlobDescriptorGenerator, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error)
93+
}
94+
6895
// signerAnnotation facilitates return of manifest annotations by signers
6996
type signerAnnotation interface {
7097
// PluginAnnotations returns signature manifest annotations returned from
@@ -85,22 +112,16 @@ type SignOptions struct {
85112
UserMetadata map[string]string
86113
}
87114

88-
// Sign signs the artifact and push the signature to the Repository.
89-
// The descriptor of the sign content is returned upon sucessful signing.
115+
// Sign signs the OCI artifact and push the signature to the Repository.
116+
// The descriptor of the sign content is returned upon successful signing.
90117
func Sign(ctx context.Context, signer Signer, repo registry.Repository, signOpts SignOptions) (ocispec.Descriptor, error) {
91118
// sanity check
92-
if signer == nil {
93-
return ocispec.Descriptor{}, errors.New("signer cannot be nil")
119+
if err := validateSignArguments(signer, signOpts.SignerSignOptions); err != nil {
120+
return ocispec.Descriptor{}, err
94121
}
95122
if repo == nil {
96123
return ocispec.Descriptor{}, errors.New("repo cannot be nil")
97124
}
98-
if signOpts.ExpiryDuration < 0 {
99-
return ocispec.Descriptor{}, fmt.Errorf("expiry duration cannot be a negative value")
100-
}
101-
if signOpts.ExpiryDuration%time.Second != 0 {
102-
return ocispec.Descriptor{}, fmt.Errorf("expiry duration supports minimum granularity of seconds")
103-
}
104125

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

176+
// SignBlob signs the arbitrary data and returns the signature
177+
func SignBlob(ctx context.Context, signer BlobSigner, blobReader io.Reader, signBlobOpts SignBlobOptions) ([]byte, *signature.SignerInfo, error) {
178+
// sanity checks
179+
if err := validateSignArguments(signer, signBlobOpts.SignerSignOptions); err != nil {
180+
return nil, nil, err
181+
}
182+
183+
if blobReader == nil {
184+
return nil, nil, errors.New("blobReader cannot be nil")
185+
}
186+
187+
if signBlobOpts.ContentMediaType == "" {
188+
return nil, nil, errors.New("content media-type cannot be empty")
189+
}
190+
191+
if _, _, err := mime.ParseMediaType(signBlobOpts.ContentMediaType); err != nil {
192+
return nil, nil, fmt.Errorf("invalid content media-type '%s': %v", signBlobOpts.ContentMediaType, err)
193+
}
194+
195+
getDescFunc := getDescriptorFunc(ctx, blobReader, signBlobOpts.ContentMediaType, signBlobOpts.UserMetadata)
196+
return signer.SignBlob(ctx, getDescFunc, signBlobOpts.SignerSignOptions)
197+
}
198+
199+
func validateSignArguments(signer any, signOpts SignerSignOptions) error {
200+
if signer == nil {
201+
return errors.New("signer cannot be nil")
202+
}
203+
if signOpts.ExpiryDuration < 0 {
204+
return errors.New("expiry duration cannot be a negative value")
205+
}
206+
if signOpts.ExpiryDuration%time.Second != 0 {
207+
return errors.New("expiry duration supports minimum granularity of seconds")
208+
}
209+
if signOpts.SignatureMediaType == "" {
210+
return errors.New("signature media-type cannot be empty")
211+
}
212+
213+
if !(signOpts.SignatureMediaType == jws.MediaTypeEnvelope || signOpts.SignatureMediaType == cose.MediaTypeEnvelope) {
214+
return fmt.Errorf("invalid signature media-type '%s'", signOpts.SignatureMediaType)
215+
}
216+
217+
return nil
218+
}
219+
155220
func addUserMetadataToDescriptor(ctx context.Context, desc ocispec.Descriptor, userMetadata map[string]string) (ocispec.Descriptor, error) {
156221
logger := log.GetLogger(ctx)
157222

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

237302
// VerifierVerifyOptions contains parameters for Verifier.Verify.
238303
type VerifierVerifyOptions struct {
239-
// ArtifactReference is the reference of the artifact that is been
304+
// ArtifactReference is the reference of the artifact that is being
240305
// verified against to. It must be a full reference.
241306
ArtifactReference string
242307

@@ -270,7 +335,7 @@ type verifySkipper interface {
270335

271336
// VerifyOptions contains parameters for notation.Verify.
272337
type VerifyOptions struct {
273-
// ArtifactReference is the reference of the artifact that is been
338+
// ArtifactReference is the reference of the artifact that is being
274339
// verified against to.
275340
ArtifactReference string
276341

@@ -449,3 +514,19 @@ func generateAnnotations(signerInfo *signature.SignerInfo, annotations map[strin
449514
annotations[ocispec.AnnotationCreated] = signingTime.Format(time.RFC3339)
450515
return annotations, nil
451516
}
517+
518+
func getDescriptorFunc(ctx context.Context, reader io.Reader, contentMediaType string, userMetadata map[string]string) BlobDescriptorGenerator {
519+
return func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) {
520+
digester := hashAlgo.Digester()
521+
bytes, err := io.Copy(digester.Hash(), reader)
522+
if err != nil {
523+
return ocispec.Descriptor{}, err
524+
}
525+
targetDesc := ocispec.Descriptor{
526+
MediaType: contentMediaType,
527+
Digest: digester.Digest(),
528+
Size: bytes,
529+
}
530+
return addUserMetadataToDescriptor(ctx, targetDesc, userMetadata)
531+
}
532+
}

notation_test.go

+108-1
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,22 @@ import (
1717
"context"
1818
"errors"
1919
"fmt"
20+
"io"
2021
"math"
2122
"os"
2223
"path/filepath"
24+
"strings"
2325
"testing"
2426
"time"
2527

2628
"github.com/notaryproject/notation-core-go/signature"
2729
"github.com/notaryproject/notation-core-go/signature/cose"
30+
"github.com/notaryproject/notation-core-go/signature/jws"
2831
"github.com/notaryproject/notation-go/internal/mock"
2932
"github.com/notaryproject/notation-go/plugin"
3033
"github.com/notaryproject/notation-go/registry"
3134
"github.com/notaryproject/notation-go/verifier/trustpolicy"
35+
"github.com/opencontainers/go-digest"
3236
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
3337
)
3438

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

@@ -58,11 +63,91 @@ func TestSignSuccess(t *testing.T) {
5863
}
5964
}
6065

66+
func TestSignBlobSuccess(t *testing.T) {
67+
reader := strings.NewReader("some content")
68+
testCases := []struct {
69+
name string
70+
dur time.Duration
71+
mtype string
72+
agent string
73+
pConfig map[string]string
74+
metadata map[string]string
75+
}{
76+
{"expiryInHours", 24 * time.Hour, "video/mp4", "", nil, nil},
77+
{"oneSecondExpiry", 1 * time.Second, "video/mp4", "", nil, nil},
78+
{"zeroExpiry", 0, "video/mp4", "", nil, nil},
79+
{"validContentType", 1 * time.Second, "video/mp4", "", nil, nil},
80+
{"emptyContentType", 1 * time.Second, "video/mp4", "someDummyAgent", map[string]string{"hi": "hello"}, map[string]string{"bye": "tata"}},
81+
}
82+
for _, tc := range testCases {
83+
t.Run(tc.name, func(b *testing.T) {
84+
opts := SignBlobOptions{
85+
SignerSignOptions: SignerSignOptions{
86+
SignatureMediaType: jws.MediaTypeEnvelope,
87+
ExpiryDuration: tc.dur,
88+
PluginConfig: tc.pConfig,
89+
SigningAgent: tc.agent,
90+
},
91+
UserMetadata: expectedMetadata,
92+
ContentMediaType: tc.mtype,
93+
}
94+
95+
_, _, err := SignBlob(context.Background(), &dummySigner{}, reader, opts)
96+
if err != nil {
97+
b.Fatalf("Sign failed with error: %v", err)
98+
}
99+
})
100+
}
101+
}
102+
103+
func TestSignBlobError(t *testing.T) {
104+
reader := strings.NewReader("some content")
105+
testCases := []struct {
106+
name string
107+
signer BlobSigner
108+
dur time.Duration
109+
rdr io.Reader
110+
sigMType string
111+
ctMType string
112+
errMsg string
113+
}{
114+
{"negativeExpiry", &dummySigner{}, -1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration cannot be a negative value"},
115+
{"milliSecExpiry", &dummySigner{}, 1 * time.Millisecond, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration supports minimum granularity of seconds"},
116+
{"invalidContentMediaType", &dummySigner{}, 1 * time.Second, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type 'video/mp4/zoping': mime: unexpected content after media subtype"},
117+
{"emptyContentMediaType", &dummySigner{}, 1 * time.Second, reader, "", jws.MediaTypeEnvelope, "content media-type cannot be empty"},
118+
{"invalidSignatureMediaType", &dummySigner{}, 1 * time.Second, reader, "", "", "content media-type cannot be empty"},
119+
{"nilReader", &dummySigner{}, 1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"},
120+
{"nilSigner", nil, 1 * time.Second, reader, "video/mp4", jws.MediaTypeEnvelope, "signer cannot be nil"},
121+
{"signerError", &dummySigner{fail: true}, 1 * time.Second, reader, "video/mp4", jws.MediaTypeEnvelope, "expected SignBlob failure"},
122+
}
123+
for _, tc := range testCases {
124+
t.Run(tc.name, func(t *testing.T) {
125+
opts := SignBlobOptions{
126+
SignerSignOptions: SignerSignOptions{
127+
SignatureMediaType: jws.MediaTypeEnvelope,
128+
ExpiryDuration: tc.dur,
129+
PluginConfig: nil,
130+
},
131+
ContentMediaType: tc.sigMType,
132+
}
133+
134+
_, _, err := SignBlob(context.Background(), tc.signer, tc.rdr, opts)
135+
if err == nil {
136+
t.Fatalf("expected error but didnt found")
137+
}
138+
if err.Error() != tc.errMsg {
139+
t.Fatalf("expected err message to be '%s' but found '%s'", tc.errMsg, err.Error())
140+
}
141+
})
142+
}
143+
}
144+
61145
func TestSignSuccessWithUserMetadata(t *testing.T) {
62146
repo := mock.NewRepository()
63147
opts := SignOptions{}
64148
opts.ArtifactReference = mock.SampleArtifactUri
65149
opts.UserMetadata = expectedMetadata
150+
opts.SignatureMediaType = jws.MediaTypeEnvelope
66151

67152
_, err := Sign(context.Background(), &verifyMetadataSigner{}, repo, opts)
68153
if err != nil {
@@ -182,6 +267,9 @@ func TestSignDigestNotMatchResolve(t *testing.T) {
182267
repo := mock.NewRepository()
183268
repo.MissMatchDigest = true
184269
signOpts := SignOptions{
270+
SignerSignOptions: SignerSignOptions{
271+
SignatureMediaType: jws.MediaTypeEnvelope,
272+
},
185273
ArtifactReference: mock.SampleArtifactUri,
186274
}
187275

@@ -320,7 +408,9 @@ func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
320408
return
321409
}
322410

323-
type dummySigner struct{}
411+
type dummySigner struct {
412+
fail bool
413+
}
324414

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

423+
func (s *dummySigner) SignBlob(_ context.Context, descGenFunc BlobDescriptorGenerator, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
424+
if s.fail {
425+
return nil, nil, errors.New("expected SignBlob failure")
426+
}
427+
428+
_, err := descGenFunc(digest.SHA384)
429+
if err != nil {
430+
return nil, nil, err
431+
}
432+
433+
return []byte("ABC"), &signature.SignerInfo{
434+
SignedAttributes: signature.SignedAttributes{
435+
SigningTime: time.Now(),
436+
},
437+
}, nil
438+
}
439+
333440
type verifyMetadataSigner struct{}
334441

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

0 commit comments

Comments
 (0)