Skip to content

Commit

Permalink
update: check SignatureMediaType in notation.Verify (#208)
Browse files Browse the repository at this point in the history
Signed-off-by: Patrick Zheng <[email protected]>
  • Loading branch information
Two-Hearts authored Nov 25, 2022
1 parent 7e391f7 commit ab113eb
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 40 deletions.
86 changes: 54 additions & 32 deletions notation.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,6 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, opts Sig
return targetDesc, nil
}

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

// SignatureMediaType is the envelope type of the signature.
// Currently both `application/jose+json` and `application/cose` are
// supported.
SignatureMediaType string

// PluginConfig is a map of plugin configs.
PluginConfig map[string]string

// MaxSignatureAttempts is the maximum number of signature envelopes that
// can be associated with the target artifact. If set to less than or equals
// to zero, an error will be returned.
// Note: this option is scoped to notation.Verify(). verifier.Verify() is
// for signle signature verification, and therefore, does not use it.
MaxSignatureAttempts int
}

// ValidationResult encapsulates the verification result (passed or failed)
// for a verification type, including the desired verification action as
// specified in the trust policy
Expand Down Expand Up @@ -132,6 +110,21 @@ type VerificationOutcome struct {
Error error
}

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

// SignatureMediaType is the envelope type of the signature.
// Currently both `application/jose+json` and `application/cose` are
// supported.
SignatureMediaType string

// PluginConfig is a map of plugin configs.
PluginConfig map[string]string
}

// Verifier is a generic interface for verifying an artifact.
type Verifier interface {
// Verify verifies the signature blob and returns the outcome upon
Expand All @@ -141,12 +134,33 @@ type Verifier interface {
Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifyOptions) (*VerificationOutcome, error)
}

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

// PluginConfig is a map of plugin configs.
PluginConfig map[string]string

// MaxSignatureAttempts is the maximum number of signature envelopes that
// will be processed for verification. If set to less than or equals
// to zero, an error will be returned.
MaxSignatureAttempts int
}

// Verify performs signature verification on each of the notation supported
// verification types (like integrity, authenticity, etc.) and return the
// successful signature verification outcomes.
// For more details on signature verification, see
// https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification
func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, opts VerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) {
func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, remoteOpts RemoteVerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) {
// opts to be passed in verifier.Verify()
opts := VerifyOptions{
ArtifactReference: remoteOpts.ArtifactReference,
PluginConfig: remoteOpts.PluginConfig,
}

// passing nil signature to check 'skip'
outcome, err := verifier.Verify(ctx, ocispec.Descriptor{}, nil, opts)
if err != nil {
Expand All @@ -156,31 +170,38 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op
} else if reflect.DeepEqual(outcome.VerificationLevel, trustpolicy.LevelSkip) {
return ocispec.Descriptor{}, []*VerificationOutcome{outcome}, nil
}

// check MaxSignatureAttempts
if remoteOpts.MaxSignatureAttempts <= 0 {
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("verifyOptions.MaxSignatureAttempts expects a positive number, got %d", remoteOpts.MaxSignatureAttempts)}
}

// get signature manifests
artifactRef := opts.ArtifactReference
artifactRef := remoteOpts.ArtifactReference
artifactDescriptor, err := repo.Resolve(ctx, artifactRef)
if err != nil {
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()}
}

var verificationOutcomes []*VerificationOutcome
if opts.MaxSignatureAttempts <= 0 {
return ocispec.Descriptor{}, verificationOutcomes, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("verifyOptions.MaxSignatureAttempts expects a positive number, got %d", opts.MaxSignatureAttempts)}
}
errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("total number of signatures associated with an artifact should be less than: %d", opts.MaxSignatureAttempts)}
errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("total number of signatures associated with an artifact should be less than: %d", remoteOpts.MaxSignatureAttempts)}
numOfSignatureProcessed := 0
err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error {
// process signatures
for _, sigManifestDesc := range signatureManifests {
if numOfSignatureProcessed >= opts.MaxSignatureAttempts {
if numOfSignatureProcessed >= remoteOpts.MaxSignatureAttempts {
break
}
numOfSignatureProcessed++
// get signature envelope
sigBlob, _, err := repo.FetchSignatureBlob(ctx, sigManifestDesc)
sigBlob, sigDesc, err := repo.FetchSignatureBlob(ctx, sigManifestDesc)
if err != nil {
return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : %v", sigManifestDesc.Digest, artifactRef, err.Error())}
}
// using signature media type fetched from registry
opts.SignatureMediaType = sigDesc.MediaType

// verify each signature
outcome, err := verifier.Verify(ctx, artifactDescriptor, sigBlob, opts)
if err != nil {
if outcome == nil {
Expand All @@ -190,14 +211,15 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, op
continue
}

// At this point, the signature is verified successfully. Add
// at this point, the signature is verified successfully. Add
// it to the verificationOutcomes.
verificationOutcomes = append(verificationOutcomes, outcome)

// early break on success
return errDoneVerification
}

if numOfSignatureProcessed >= opts.MaxSignatureAttempts {
if numOfSignatureProcessed >= remoteOpts.MaxSignatureAttempts {
return errExceededMaxVerificationLimit
}

Expand Down
60 changes: 52 additions & 8 deletions notation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func TestRegistryResolveError(t *testing.T) {

// mock the repository
repo.ResolveError = errors.New(errorMessage)
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), &verifier, repo, opts)

if err == nil || !errors.Is(err, expectedErr) {
Expand All @@ -35,7 +35,7 @@ func TestSkippedSignatureVerification(t *testing.T) {
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelSkip}

opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, outcomes, err := Verify(context.Background(), &verifier, repo, opts)

if err != nil || outcomes[0].VerificationLevel.Name != trustpolicy.LevelSkip.Name {
Expand All @@ -52,7 +52,7 @@ func TestRegistryNoSignatureManifests(t *testing.T) {

// mock the repository
repo.ListSignaturesResponse = []ocispec.Descriptor{}
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), &verifier, repo, opts)

if err == nil || !errors.Is(err, expectedErr) {
Expand All @@ -69,11 +69,55 @@ func TestRegistryFetchSignatureBlobError(t *testing.T) {

// mock the repository
repo.FetchSignatureBlobError = errors.New("network error")
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), &verifier, repo, opts)

if err == nil || !errors.Is(err, expectedErr) {
t.Fatalf("RegistryGetBlob expected: %v got: %v", expectedErr, err)
t.Fatalf("RegistryFetchSignatureBlob expected: %v got: %v", expectedErr, err)
}
}

func TestVerifyValid(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}

// mock the repository
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), &verifier, repo, opts)

if err != nil {
t.Fatalf("SignaureMediaTypeMismatch expected: %v got: %v", nil, err)
}
}

func TestMaxSignatureAttemptsMissing(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
expectedErr := ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("verifyOptions.MaxSignatureAttempts expects a positive number, got %d", 0)}

// mock the repository
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri}
_, _, err := Verify(context.Background(), &verifier, repo, opts)

if err == nil || !errors.Is(err, expectedErr) {
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
}
}

func TestVerifyFailed(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict}
expectedErr := ErrorVerificationFailed{}

// mock the repository
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), &verifier, repo, opts)

if err == nil || !errors.Is(err, expectedErr) {
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
}
}

Expand Down Expand Up @@ -104,13 +148,13 @@ type dummyVerifier struct {
}

func (v *dummyVerifier) Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifyOptions) (*VerificationOutcome, error) {
if v.FailVerify {
return nil, errors.New("failed verify")
}
outcome := &VerificationOutcome{
VerificationResults: []*ValidationResult{},
VerificationLevel: &v.VerificationLevel,
}
if v.FailVerify {
return outcome, errors.New("failed verify")
}
return outcome, nil
}

Expand Down

0 comments on commit ab113eb

Please sign in to comment.