Compare commits
No commits in common. "main" and "v1.0.0" have entirely different histories.
|
@ -14,8 +14,5 @@
|
|||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: 80%
|
||||
patch:
|
||||
default:
|
||||
target: 80%
|
|
@ -15,16 +15,10 @@ name: build
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
branches: main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
branches: main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: notaryproject/notation-core-go/.github/workflows/reusable-build.yml@main
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
|
@ -15,13 +15,9 @@ name: "CodeQL"
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
branches: main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
branches: main
|
||||
schedule:
|
||||
- cron: '29 2 * * 5'
|
||||
|
||||
|
|
|
@ -15,13 +15,9 @@ name: License Checker
|
|||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
branches: main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
branches: main
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
# Copyright The Notary Project Authors.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
name: "Close stale issues and PRs"
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
with:
|
||||
stale-issue-message: "This issue is stale because it has been opened for 60 days with no activity. Remove stale label or comment. Otherwise, it will be closed in 30 days."
|
||||
stale-pr-message: "This PR is stale because it has been opened for 45 days with no activity. Remove stale label or comment. Otherwise, it will be closed in 30 days."
|
||||
close-issue-message: "Issue closed due to no activity in the past 30 days."
|
||||
close-pr-message: "PR closed due to no activity in the past 30 days."
|
||||
days-before-issue-stale: 60
|
||||
days-before-pr-stale: 45
|
||||
days-before-issue-close: 30
|
||||
days-before-pr-close: 30
|
||||
exempt-all-milestones: true
|
|
@ -18,7 +18,4 @@
|
|||
*.sublime-workspace
|
||||
|
||||
# Custom
|
||||
coverage.txt
|
||||
|
||||
# tmp directory was generated by example_remoteVerify_test.go
|
||||
tmp/
|
||||
coverage.txt
|
|
@ -1,3 +1,3 @@
|
|||
# Repo-Level Owners (in alphabetical order)
|
||||
# Note: This is only for the notaryproject/notation-go repo
|
||||
* @gokarnm @niazfk @priteshbandi @rgnote @shizhMSFT @toddysm @Two-Hearts @vaninrao10 @yizha1
|
||||
* @gokarnm @JeyJeyGao @justincormack @niazfk @priteshbandi @rgnote @shizhMSFT @stevelasker @Two-Hearts
|
||||
|
|
17
MAINTAINERS
17
MAINTAINERS
|
@ -1,21 +1,14 @@
|
|||
# Org-Level Maintainers (in alphabetical order)
|
||||
# Pattern: [First Name] [Last Name] <[Email Address]> ([GitHub Handle])
|
||||
Justin Cormack <justin.cormack@docker.com> (@justincormack)
|
||||
Niaz Khan <niazfk@amazon.com> (@niazfk)
|
||||
Pritesh Bandi <priteshbandi@gmail.com> (@priteshbandi)
|
||||
Shiwei Zhang <shizh@microsoft.com> (@shizhMSFT)
|
||||
Toddy Mladenov <toddysm@gmail.com> (@toddysm)
|
||||
Vani Rao <vaninrao@amazon.com> (@vaninrao10)
|
||||
Yi Zha <yizha1@microsoft.com> (@yizha1)
|
||||
Steve Lasker <StevenLasker@hotmail.com> (@stevelasker)
|
||||
|
||||
# Repo-Level Maintainers (in alphabetical order)
|
||||
# Note: This is for the notaryproject/notation-go repo
|
||||
Junjie Gao <junjiegao@microsoft.com> (@JeyJeyGao)
|
||||
Milind Gokarn <gokarnm@amazon.com> (@gokarnm)
|
||||
Patrick Zheng <patrickzheng@microsoft.com> (@Two-Hearts)
|
||||
Pritesh Bandi <priteshbandi@gmail.com> (@priteshbandi)
|
||||
Rakesh Gariganti <garigant@amazon.com> (@rgnote)
|
||||
|
||||
# Emeritus Org Maintainers (in alphabetical order)
|
||||
Justin Cormack <justin.cormack@docker.com> (@justincormack)
|
||||
Steve Lasker <StevenLasker@hotmail.com> (@stevelasker)
|
||||
|
||||
# Emeritus Repo-Level Maintainers (in alphabetical order)
|
||||
Junjie Gao <junjiegao@microsoft.com> (@JeyJeyGao)
|
||||
Shiwei Zhang <shizh@microsoft.com> (@shizhMSFT)
|
||||
|
|
2
Makefile
2
Makefile
|
@ -29,7 +29,7 @@ clean:
|
|||
.PHONY: check-line-endings
|
||||
check-line-endings: ## check line endings
|
||||
! find . -name "*.go" -type f -exec file "{}" ";" | grep CRLF
|
||||
! find . -name "*.sh" -type f -exec file "{}" ";" | grep CRLF
|
||||
! find scripts -name "*.sh" -type f -exec file "{}" ";" | grep CRLF
|
||||
|
||||
.PHONY: fix-line-endings
|
||||
fix-line-endings: ## fix line endings
|
||||
|
|
24
README.md
24
README.md
|
@ -1,28 +1,20 @@
|
|||
# notation-go
|
||||
# Notation
|
||||
|
||||
[](https://github.com/notaryproject/notation-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain)
|
||||
[](https://codecov.io/gh/notaryproject/notation-go)
|
||||
[](https://codecov.io/gh/notaryproject/notation-go)
|
||||
[](https://pkg.go.dev/github.com/notaryproject/notation-go@main)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/notaryproject/notation-go)
|
||||
|
||||
notation-go contains libraries for signing and verification of artifacts as per [Notary Project specifications](https://github.com/notaryproject/specifications). notation-go is being used by [notation](https://github.com/notaryproject/notation) CLI for signing and verifying artifacts.
|
||||
|
||||
notation-go reached a stable release as of July 2023 and continues to be actively developed and maintained.
|
||||
|
||||
Please visit [README](https://github.com/notaryproject/.github/blob/main/README.md) to know more about Notary Project.
|
||||
|
||||
> [!NOTE]
|
||||
> The Notary Project documentation is available [here](https://notaryproject.dev/docs/).
|
||||
A collection of libraries for supporting Notation sign, verify, push, pull of oci artifacts. Based on Notary Project standard.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Documentation](#documentation)
|
||||
- [Core Documents](#core-documents)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [License](#license)
|
||||
|
||||
## Documentation
|
||||
|
||||
Library documentation is available at [Go Reference](https://pkg.go.dev/github.com/notaryproject/notation-go).
|
||||
## Core Documents
|
||||
|
||||
* [Governance for Notation](https://github.com/notaryproject/notary/blob/master/GOVERNANCE.md)
|
||||
* [Maintainers and reviewers list](https://github.com/notaryproject/notary/blob/master/MAINTAINERS)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
# Release Checklist
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the checklist to publish a release for notation-go.
|
||||
|
||||
## Release Process from main
|
||||
|
||||
1. Check if there are any security vulnerabilities fixed and security advisories published before a release. Security advisories should be linked on the release notes.
|
||||
2. Determine a [SemVer2](https://semver.org/)-valid version prefixed with the letter `v` for release. For example, `version="v1.0.0-rc.1"`.
|
||||
3. If there is new release in [notation-core-go](https://github.com/notaryproject/notation-core-go) library that are required to be upgraded in notation-go, update the dependency versions in the follow `go.mod` and `go.sum` files of notation-go:
|
||||
- [go.mod](go.mod), [go.sum](go.sum)
|
||||
4. Open a bump up PR and submit the changes in step 3 to the notation-go repository.
|
||||
5. After PR from step 4 is merged. Create another PR to update the value of `signingAgent` defined in file [signer/signer.go](signer/signer.go) with `notation-go/<version>`, where `<version>` is `$version` from step 2 without the `v` prefix. For example, `notation-go/1.0.0-rc.1`. The commit message MUST follow the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) and could be `bump: release $version`. Record the digest of that commit as `<commit_digest>`. This PR is also used for voting purpose of the new release. Add the link of change logs and repo-level maintainer list in the PR's description. The PR title could be `bump: release $version`. Make sure to reach a majority of approvals from the [repo-level maintainers](MAINTAINERS) before merging it. This PR MUST be merged using [Create a merge commit](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github) method in GitHub.
|
||||
6. After the voting PR is merged, execute `git clone https://github.com/notaryproject/notation-go.git` to clone the repository to your local file system.
|
||||
7. Enter the cloned repository and execute `git checkout <commit_digest>` to switch to the specified branch based on the voting result.
|
||||
8. Create a tag by running `git tag -am $version $version -s`.
|
||||
9. Run `git tag` and ensure the desired tag name in the list looks correct, then push the new tag directly to the repository by running `git push origin $version`.
|
||||
10. On notation-go GitHub page, goto [Tags](https://github.com/notaryproject/notation-go/tags). Your newly pushed tag should be shown on the top. Create a new release from the tag. Generate the release notes, revise the release description and change logs, and publish the release.
|
||||
11. Announce the new release in the Notary Project community.
|
||||
|
||||
## Release Process from a release branch
|
||||
|
||||
1. Check if there are any security vulnerabilities fixed and security advisories published before a release. Security advisories should be linked on the release notes.
|
||||
2. Determine a [SemVer2](https://semver.org/)-valid version prefixed with the letter `v` for release. For example, `version="v1.2.0-rc.1"`.
|
||||
3. If a new release branch is needed, from main branch's [commit list](https://github.com/notaryproject/notation-go/commits/main/), find the commit that you want to cut the release. Click `<>` (Browse repository at this point). Create branch with name `release-<version>` from the commit, where `<version>` is `$version` from step 2 with the major and minor versions only. For example `release-1.2`. If the release branch already exists, skip this step.
|
||||
4. If there is new release in [notation-core-go](https://github.com/notaryproject/notation-core-go) library that are required to be upgraded in notation-go, update the dependency versions in the follow `go.mod` and `go.sum` files of notation-go:
|
||||
- [go.mod](go.mod), [go.sum](go.sum)
|
||||
5. Open a bump up PR and submit the changes in step 4 to the release branch.
|
||||
6. After PR from step 5 is merged. Create another PR to update the value of `signingAgent` defined in file `signer/signer.go` with `notation-go/<version>`, where `<version>` is `$version` from step 2 without the `v` prefix. For example, `notation-go/1.2.0-rc.1`. The commit message MUST follow the [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) and could be `bump: release $version`. Record the digest of that commit as `<commit_digest>`. This PR is also used for voting purpose of the new release. Add the link of change logs and repo-level maintainer list in the PR's description. The PR title could be `bump: release $version`. Make sure to reach a majority of approvals from the [repo-level maintainers](MAINTAINERS) before merging it. This PR MUST be merged using [Create a merge commit](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/about-merge-methods-on-github) method in GitHub.
|
||||
7. After the voting PR is merged, execute `git clone https://github.com/notaryproject/notation-go.git` to clone the repository to your local file system.
|
||||
8. Enter the cloned repository and execute `git checkout <commit_digest>` to switch to the specified branch based on the voting result.
|
||||
9. Create a tag by running `git tag -am $version $version -s`.
|
||||
10. Run `git tag` and ensure the desired tag name in the list looks correct, then push the new tag directly to the repository by running `git push origin $version`.
|
||||
11. On notation-go GitHub page, goto [Tags](https://github.com/notaryproject/notation-go/tags). Your newly pushed tag should be shown on the top. Create a new release from the tag. Generate the release notes, revise the release description and change logs, and publish the release.
|
||||
12. Announce the new release in the Notary Project community.
|
|
@ -17,7 +17,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
|
@ -34,9 +33,6 @@ func TestLoadNonExistentFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoadSymlink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
root := t.TempDir()
|
||||
dir.UserConfigDir = root
|
||||
fileName := "symlink"
|
||||
|
|
|
@ -51,15 +51,3 @@ func TestSaveFile(t *testing.T) {
|
|||
t.Fatal("save config file failed.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNonExistedConfig(t *testing.T) {
|
||||
dir.UserConfigDir = "./testdata/non-existed"
|
||||
got, err := LoadConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error. err = %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, NewConfig()) {
|
||||
t.Errorf("loadFile() = %v, want %v", got, NewConfig())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrKeyNameEmpty is used when key name is empty.
|
||||
var ErrKeyNameEmpty = errors.New("key name cannot be empty")
|
||||
|
||||
// KeyNotFoundError is used when key is not found in the signingkeys.json file.
|
||||
type KeyNotFoundError struct {
|
||||
KeyName string
|
||||
}
|
||||
|
||||
// Error returns the error message.
|
||||
func (e KeyNotFoundError) Error() string {
|
||||
if e.KeyName != "" {
|
||||
return fmt.Sprintf("signing key %s not found", e.KeyName)
|
||||
}
|
||||
return "signing key not found"
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestErrorKeyNotFound(t *testing.T) {
|
||||
e := KeyNotFoundError{}
|
||||
if e.Error() != "signing key not found" {
|
||||
t.Fatalf("ErrorKeyNotFound.Error() = %v, want %v", e.Error(), "signing key not found")
|
||||
}
|
||||
|
||||
e = KeyNotFoundError{KeyName: "testKey"}
|
||||
if e.Error() != `signing key testKey not found` {
|
||||
t.Fatalf("ErrorKeyNotFound.Error() = %v, want %v", e.Error(), "signing key testKey not found")
|
||||
}
|
||||
}
|
|
@ -50,6 +50,9 @@ type KeySuite struct {
|
|||
*ExternalKey
|
||||
}
|
||||
|
||||
var errorKeyNameEmpty = errors.New("key name cannot be empty")
|
||||
var errKeyNotFound = errors.New("signing key not found")
|
||||
|
||||
// SigningKeys reflects the signingkeys.json file.
|
||||
type SigningKeys struct {
|
||||
Default *string `json:"default,omitempty"`
|
||||
|
@ -64,12 +67,13 @@ func NewSigningKeys() *SigningKeys {
|
|||
// Add adds new signing key
|
||||
func (s *SigningKeys) Add(name, keyPath, certPath string, markDefault bool) error {
|
||||
if name == "" {
|
||||
return ErrKeyNameEmpty
|
||||
return errorKeyNameEmpty
|
||||
}
|
||||
_, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ks := KeySuite{
|
||||
Name: name,
|
||||
X509KeyPair: &X509KeyPair{
|
||||
|
@ -84,20 +88,25 @@ func (s *SigningKeys) Add(name, keyPath, certPath string, markDefault bool) erro
|
|||
func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName string, pluginConfig map[string]string, markDefault bool) error {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debugf("Adding key with name %v and plugin name %v", keyName, pluginName)
|
||||
|
||||
if keyName == "" {
|
||||
return ErrKeyNameEmpty
|
||||
return errorKeyNameEmpty
|
||||
}
|
||||
|
||||
if id == "" {
|
||||
return errors.New("missing key id")
|
||||
}
|
||||
|
||||
if pluginName == "" {
|
||||
return errors.New("plugin name cannot be empty")
|
||||
}
|
||||
|
||||
mgr := plugin.NewCLIManager(dir.PluginFS())
|
||||
_, err := mgr.Get(ctx, pluginName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ks := KeySuite{
|
||||
Name: keyName,
|
||||
ExternalKey: &ExternalKey{
|
||||
|
@ -106,6 +115,7 @@ func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName str
|
|||
PluginConfig: pluginConfig,
|
||||
},
|
||||
}
|
||||
|
||||
if err = s.add(ks, markDefault); err != nil {
|
||||
logger.Error("Failed to add key with error: %v", err)
|
||||
return err
|
||||
|
@ -117,12 +127,14 @@ func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName str
|
|||
// Get returns signing key for the given name
|
||||
func (s *SigningKeys) Get(keyName string) (KeySuite, error) {
|
||||
if keyName == "" {
|
||||
return KeySuite{}, ErrKeyNameEmpty
|
||||
return KeySuite{}, errorKeyNameEmpty
|
||||
}
|
||||
|
||||
idx := slices.IndexIsser(s.Keys, keyName)
|
||||
if idx < 0 {
|
||||
return KeySuite{}, KeyNotFoundError{KeyName: keyName}
|
||||
return KeySuite{}, errKeyNotFound
|
||||
}
|
||||
|
||||
return s.Keys[idx], nil
|
||||
}
|
||||
|
||||
|
@ -132,6 +144,7 @@ func (s *SigningKeys) GetDefault() (KeySuite, error) {
|
|||
return KeySuite{}, errors.New("default signing key not set." +
|
||||
" Please set default signing key or specify a key name")
|
||||
}
|
||||
|
||||
return s.Get(*s.Default)
|
||||
}
|
||||
|
||||
|
@ -140,11 +153,12 @@ func (s *SigningKeys) Remove(keyName ...string) ([]string, error) {
|
|||
var deletedNames []string
|
||||
for _, name := range keyName {
|
||||
if name == "" {
|
||||
return deletedNames, ErrKeyNameEmpty
|
||||
return deletedNames, errorKeyNameEmpty
|
||||
}
|
||||
|
||||
idx := slices.IndexIsser(s.Keys, name)
|
||||
if idx < 0 {
|
||||
return deletedNames, KeyNotFoundError{KeyName: name}
|
||||
return deletedNames, errors.New(name + ": not found")
|
||||
}
|
||||
s.Keys = slices.Delete(s.Keys, idx)
|
||||
deletedNames = append(deletedNames, name)
|
||||
|
@ -158,11 +172,13 @@ func (s *SigningKeys) Remove(keyName ...string) ([]string, error) {
|
|||
// UpdateDefault updates default signing key
|
||||
func (s *SigningKeys) UpdateDefault(keyName string) error {
|
||||
if keyName == "" {
|
||||
return ErrKeyNameEmpty
|
||||
return errorKeyNameEmpty
|
||||
}
|
||||
|
||||
if !slices.ContainsIsser(s.Keys, keyName) {
|
||||
return KeyNotFoundError{KeyName: keyName}
|
||||
return fmt.Errorf("key with name '%s' not found", keyName)
|
||||
}
|
||||
|
||||
s.Default = &keyName
|
||||
return nil
|
||||
}
|
||||
|
@ -173,9 +189,11 @@ func (s *SigningKeys) Save() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateKeys(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return save(path, s)
|
||||
}
|
||||
|
||||
|
@ -190,9 +208,11 @@ func LoadSigningKeys() (*SigningKeys, error) {
|
|||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateKeys(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
|
@ -204,9 +224,11 @@ func LoadExecSaveSigningKeys(fn func(keys *SigningKeys) error) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fn(signingKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return signingKeys.Save()
|
||||
}
|
||||
|
||||
|
@ -219,10 +241,12 @@ func (s *SigningKeys) add(key KeySuite, markDefault bool) error {
|
|||
if slices.ContainsIsser(s.Keys, key.Name) {
|
||||
return fmt.Errorf("signing key with name %q already exists", key.Name)
|
||||
}
|
||||
|
||||
s.Keys = append(s.Keys, key)
|
||||
if markDefault {
|
||||
s.Default = &key.Name
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -238,14 +262,17 @@ func validateKeys(config *SigningKeys) error {
|
|||
}
|
||||
uniqueKeyNames.Add(key.Name)
|
||||
}
|
||||
|
||||
if config.Default != nil {
|
||||
defaultKey := *config.Default
|
||||
if len(defaultKey) == 0 {
|
||||
return fmt.Errorf("malformed %s: default key name cannot be empty", dir.PathSigningKeys)
|
||||
}
|
||||
|
||||
if !uniqueKeyNames.Contains(defaultKey) {
|
||||
return fmt.Errorf("malformed %s: default key '%s' not found", dir.PathSigningKeys, defaultKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import (
|
|||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
@ -311,22 +310,14 @@ func TestGet(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("NonExistent", func(t *testing.T) {
|
||||
_, err := sampleSigningKeysInfo.Get("nonExistent")
|
||||
if err == nil {
|
||||
if _, err := sampleSigningKeysInfo.Get("nonExistent"); err == nil {
|
||||
t.Error("expected Get() to fail for nonExistent key name")
|
||||
}
|
||||
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
|
||||
t.Error("expected Get() to return ErrorKeyNotFound")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyName", func(t *testing.T) {
|
||||
_, err := sampleSigningKeysInfo.Get("")
|
||||
if err == nil {
|
||||
t.Error("expected Get() to fail for empty key name")
|
||||
}
|
||||
if !errors.Is(err, ErrKeyNameEmpty) {
|
||||
t.Error("expected Get() to return ErrorKeyNameEmpty")
|
||||
t.Run("InvalidName", func(t *testing.T) {
|
||||
if _, err := sampleSigningKeysInfo.Get(""); err == nil {
|
||||
t.Error("expected Get() to fail for invalid key name")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -367,22 +358,14 @@ func TestUpdateDefault(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("NonExistent", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.UpdateDefault("nonExistent")
|
||||
if err == nil {
|
||||
if err := sampleSigningKeysInfo.UpdateDefault("nonExistent"); err == nil {
|
||||
t.Error("expected Get() to fail for nonExistent key name")
|
||||
}
|
||||
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
|
||||
t.Error("expected Get() to return ErrorKeyNotFound")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyName", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.UpdateDefault("")
|
||||
if err == nil {
|
||||
t.Error("expected Get() to fail for empty key name")
|
||||
}
|
||||
if !errors.Is(err, ErrKeyNameEmpty) {
|
||||
t.Error("expected Get() to return ErrorKeyNameEmpty")
|
||||
t.Run("InvalidName", func(t *testing.T) {
|
||||
if err := sampleSigningKeysInfo.UpdateDefault(""); err == nil {
|
||||
t.Error("expected Get() to fail for invalid key name")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -399,28 +382,21 @@ func TestRemove(t *testing.T) {
|
|||
if _, err := testSigningKeysInfo.Get(testKeyName); err == nil {
|
||||
t.Error("Delete() filed to delete key")
|
||||
}
|
||||
|
||||
if keys[0] != testKeyName {
|
||||
t.Error("Delete() deleted key name mismatch")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NonExistent", func(t *testing.T) {
|
||||
_, err := testSigningKeysInfo.Remove("nonExistent")
|
||||
if err == nil {
|
||||
if _, err := testSigningKeysInfo.Remove(testKeyName); err == nil {
|
||||
t.Error("expected Get() to fail for nonExistent key name")
|
||||
}
|
||||
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
|
||||
t.Error("expected Get() to return ErrorKeyNotFound")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyName", func(t *testing.T) {
|
||||
_, err := testSigningKeysInfo.Remove("")
|
||||
if err == nil {
|
||||
t.Error("expected Get() to fail for empty key name")
|
||||
}
|
||||
if !errors.Is(err, ErrKeyNameEmpty) {
|
||||
t.Error("expected Get() to return ErrorKeyNameEmpty")
|
||||
t.Run("InvalidName", func(t *testing.T) {
|
||||
if _, err := testSigningKeysInfo.Remove(""); err == nil {
|
||||
t.Error("expected Get() to fail for invalid key name")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
11
dir/fs.go
11
dir/fs.go
|
@ -51,17 +51,10 @@ func NewSysFS(root string) SysFS {
|
|||
|
||||
// ConfigFS is the config SysFS
|
||||
func ConfigFS() SysFS {
|
||||
return NewSysFS(userConfigDirPath())
|
||||
return NewSysFS(UserConfigDir)
|
||||
}
|
||||
|
||||
// PluginFS is the plugin SysFS
|
||||
func PluginFS() SysFS {
|
||||
return NewSysFS(filepath.Join(userLibexecDirPath(), PathPlugins))
|
||||
}
|
||||
|
||||
// CacheFS is the cache SysFS.
|
||||
//
|
||||
// To get the root of crl file cache, use `CacheFS().SysFS(PathCRLCache)`.
|
||||
func CacheFS() SysFS {
|
||||
return NewSysFS(userCacheDirPath())
|
||||
return NewSysFS(filepath.Join(UserLibexecDir, PathPlugins))
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func TestSysFS_SysPath(t *testing.T) {
|
||||
func Test_sysFS_SysPath(t *testing.T) {
|
||||
wantPath := filepath.FromSlash("/path/notation/config.json")
|
||||
fsys := NewSysFS("/path/notation")
|
||||
path, err := fsys.SysPath(PathConfigFile)
|
||||
|
@ -31,7 +31,7 @@ func TestSysFS_SysPath(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestOsFs(t *testing.T) {
|
||||
func Test_OsFs(t *testing.T) {
|
||||
wantData := []byte("data")
|
||||
fsys := NewSysFS("./testdata")
|
||||
|
||||
|
@ -49,36 +49,3 @@ func TestOsFs(t *testing.T) {
|
|||
t.Fatalf("SysFS read failed. got data = %v, want %v", data, wantData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigFS(t *testing.T) {
|
||||
configFS := ConfigFS()
|
||||
path, err := configFS.SysPath(PathConfigFile)
|
||||
if err != nil {
|
||||
t.Fatalf("SysPath() failed. err = %v", err)
|
||||
}
|
||||
if path != filepath.Join(UserConfigDir, PathConfigFile) {
|
||||
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(UserConfigDir, PathConfigFile))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginFS(t *testing.T) {
|
||||
pluginFS := PluginFS()
|
||||
path, err := pluginFS.SysPath("plugin")
|
||||
if err != nil {
|
||||
t.Fatalf("SysPath() failed. err = %v", err)
|
||||
}
|
||||
if path != filepath.Join(userLibexecDirPath(), PathPlugins, "plugin") {
|
||||
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(userLibexecDirPath(), PathPlugins, "plugin"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRLFileCacheFS(t *testing.T) {
|
||||
cacheFS := CacheFS()
|
||||
path, err := cacheFS.SysPath(PathCRLCache)
|
||||
if err != nil {
|
||||
t.Fatalf("SysPath() failed. err = %v", err)
|
||||
}
|
||||
if path != filepath.Join(UserCacheDir, PathCRLCache) {
|
||||
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, UserConfigDir)
|
||||
}
|
||||
}
|
||||
|
|
83
dir/path.go
83
dir/path.go
|
@ -11,8 +11,8 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package dir implements Notation directory structure.
|
||||
// [directory spec]: https://notaryproject.dev/docs/user-guides/how-to/directory-structure/
|
||||
// package dir implements Notation directory structure.
|
||||
// [directory spec]: https://github.com/notaryproject/notation/blob/main/specs/directory.md
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
|
@ -31,7 +31,7 @@
|
|||
// - Set custom configurations directory:
|
||||
// dir.UserConfigDir = '/path/to/configurations/'
|
||||
//
|
||||
// Only user level directory is supported, and system level directory
|
||||
// Only user level directory is supported for RC.1, and system level directory
|
||||
// may be added later.
|
||||
package dir
|
||||
|
||||
|
@ -44,7 +44,6 @@ import (
|
|||
var (
|
||||
UserConfigDir string // Absolute path of user level {NOTATION_CONFIG}
|
||||
UserLibexecDir string // Absolute path of user level {NOTATION_LIBEXEC}
|
||||
UserCacheDir string // Absolute path of user level {NOTATION_CACHE}
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -58,15 +57,10 @@ const (
|
|||
PathConfigFile = "config.json"
|
||||
// PathSigningKeys is the signingkeys file relative path.
|
||||
PathSigningKeys = "signingkeys.json"
|
||||
// PathTrustPolicy is the OCI trust policy file relative path.
|
||||
//
|
||||
// Deprecated: PathTrustPolicy exists for historical compatibility and should not be used.
|
||||
// To get OCI trust policy path, use PathOCITrustPolicy.
|
||||
// PathTrustPolicy is the trust policy file relative path.
|
||||
PathTrustPolicy = "trustpolicy.json"
|
||||
// PathOCITrustPolicy is the OCI trust policy file relative path.
|
||||
PathOCITrustPolicy = "trustpolicy.oci.json"
|
||||
// PathBlobTrustPolicy is the Blob trust policy file relative path.
|
||||
PathBlobTrustPolicy = "trustpolicy.blob.json"
|
||||
// PathPlugins is the plugins directory relative path.
|
||||
PathPlugins = "plugins"
|
||||
// LocalKeysDir is the directory name for local key relative path.
|
||||
LocalKeysDir = "localkeys"
|
||||
// LocalCertificateExtension defines the extension of the certificate files.
|
||||
|
@ -77,62 +71,23 @@ const (
|
|||
TrustStoreDir = "truststore"
|
||||
)
|
||||
|
||||
// The relative path to {NOTATION_LIBEXEC}
|
||||
const (
|
||||
// PathPlugins is the plugins directory relative path.
|
||||
PathPlugins = "plugins"
|
||||
)
|
||||
var userConfigDir = os.UserConfigDir // for unit test
|
||||
|
||||
// The relative path to {NOTATION_CACHE}
|
||||
const (
|
||||
// PathCRLCache is the crl file cache directory relative path.
|
||||
PathCRLCache = "crl"
|
||||
)
|
||||
|
||||
// for unit tests
|
||||
var (
|
||||
userConfigDir = os.UserConfigDir
|
||||
|
||||
userCacheDir = os.UserCacheDir
|
||||
)
|
||||
|
||||
// userConfigDirPath returns the user level {NOTATION_CONFIG} path.
|
||||
func userConfigDirPath() string {
|
||||
if UserConfigDir == "" {
|
||||
userDir, err := userConfigDir()
|
||||
if err != nil {
|
||||
// fallback to current directory
|
||||
UserConfigDir = "." + notation
|
||||
return UserConfigDir
|
||||
}
|
||||
// set user config
|
||||
UserConfigDir = filepath.Join(userDir, notation)
|
||||
}
|
||||
return UserConfigDir
|
||||
func init() {
|
||||
loadUserPath()
|
||||
}
|
||||
|
||||
// userLibexecDirPath returns the user level {NOTATION_LIBEXEC} path.
|
||||
func userLibexecDirPath() string {
|
||||
if UserLibexecDir == "" {
|
||||
// set user libexec
|
||||
UserLibexecDir = userConfigDirPath()
|
||||
// loadUserPath function defines UserConfigDir and UserLibexecDir.
|
||||
func loadUserPath() {
|
||||
// set user config
|
||||
userDir, err := userConfigDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return UserLibexecDir
|
||||
}
|
||||
UserConfigDir = filepath.Join(userDir, notation)
|
||||
|
||||
// userCacheDirPath returns the user level {NOTATION_CACHE} path.
|
||||
func userCacheDirPath() string {
|
||||
if UserCacheDir == "" {
|
||||
userDir, err := userCacheDir()
|
||||
if err != nil {
|
||||
// fallback to current directory
|
||||
UserCacheDir = filepath.Join("."+notation, "cache")
|
||||
return UserCacheDir
|
||||
}
|
||||
// set user cache
|
||||
UserCacheDir = filepath.Join(userDir, notation)
|
||||
}
|
||||
return UserCacheDir
|
||||
// set user libexec
|
||||
UserLibexecDir = UserConfigDir
|
||||
}
|
||||
|
||||
// LocalKeyPath returns the local key and local cert relative paths.
|
||||
|
@ -145,7 +100,7 @@ func LocalKeyPath(name string) (keyPath, certPath string) {
|
|||
//
|
||||
// items includes named-store and cert-file names.
|
||||
// the directory follows the pattern of
|
||||
// {NOTATION_CONFIG}/truststore/x509/{store-type}/{named-store}/{cert-file}
|
||||
// {NOTATION_CONFIG}/truststore/x509/{named-store}/{cert-file}
|
||||
func X509TrustStoreDir(items ...string) string {
|
||||
pathItems := []string{TrustStoreDir, "x509"}
|
||||
pathItems = append(pathItems, items...)
|
||||
|
|
|
@ -14,70 +14,30 @@
|
|||
package dir
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mockUserPath() (string, error) {
|
||||
func mockGetUserConfig() (string, error) {
|
||||
return "/path/", nil
|
||||
}
|
||||
|
||||
func setup() {
|
||||
UserConfigDir = ""
|
||||
UserLibexecDir = ""
|
||||
UserCacheDir = ""
|
||||
}
|
||||
|
||||
func Test_UserConfigDirPath(t *testing.T) {
|
||||
userConfigDir = mockUserPath
|
||||
setup()
|
||||
got := userConfigDirPath()
|
||||
if got != "/path/notation" {
|
||||
t.Fatalf(`UserConfigDirPath() = %q, want "/path/notation"`, got)
|
||||
func Test_loadPath(t *testing.T) {
|
||||
wantDir := filepath.FromSlash("/path/notation")
|
||||
userConfigDir = mockGetUserConfig
|
||||
loadUserPath()
|
||||
if UserConfigDir != wantDir {
|
||||
t.Fatalf(`loadPath() UserConfigDir is incorrect. got: %q, want: %q`, UserConfigDir, wantDir)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NoHomeVariable(t *testing.T) {
|
||||
t.Setenv("HOME", "")
|
||||
t.Setenv("XDG_CONFIG_HOME", "")
|
||||
t.Setenv("XDG_CACHE_HOME", "")
|
||||
setup()
|
||||
userConfigDir = os.UserConfigDir
|
||||
got := userConfigDirPath()
|
||||
if got != ".notation" {
|
||||
t.Fatalf(`userConfigDirPath() = %q, want ".notation"`, got)
|
||||
}
|
||||
got = userCacheDirPath()
|
||||
want := filepath.Join("."+notation, "cache")
|
||||
if got != want {
|
||||
t.Fatalf(`userCacheDirPath() = %q, want %q`, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UserLibexecDirPath(t *testing.T) {
|
||||
userConfigDir = mockUserPath
|
||||
setup()
|
||||
got := userLibexecDirPath()
|
||||
if got != "/path/notation" {
|
||||
t.Fatalf(`UserConfigDirPath() = %q, want "/path/notation"`, got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UserCacheDirPath(t *testing.T) {
|
||||
userCacheDir = mockUserPath
|
||||
setup()
|
||||
got := userCacheDirPath()
|
||||
if got != "/path/notation" {
|
||||
t.Fatalf(`UserCacheDirPath() = %q, want "/path/notation"`, got)
|
||||
if UserLibexecDir != UserConfigDir {
|
||||
t.Fatalf(`loadPath() UserLibexecDir is incorrect. got: %q, want: %q`, UserLibexecDir, wantDir)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalKeyPath(t *testing.T) {
|
||||
userConfigDir = mockUserPath
|
||||
setup()
|
||||
_ = userConfigDirPath()
|
||||
_ = userLibexecDirPath()
|
||||
userConfigDir = mockGetUserConfig
|
||||
loadUserPath()
|
||||
gotKeyPath, gotCertPath := LocalKeyPath("web")
|
||||
if gotKeyPath != "localkeys/web.key" {
|
||||
t.Fatalf(`LocalKeyPath() gotKeyPath = %q, want "localkeys/web.key"`, gotKeyPath)
|
||||
|
@ -88,10 +48,8 @@ func TestLocalKeyPath(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestX509TrustStoreDir(t *testing.T) {
|
||||
userConfigDir = mockUserPath
|
||||
setup()
|
||||
_ = userConfigDirPath()
|
||||
_ = userLibexecDirPath()
|
||||
userConfigDir = mockGetUserConfig
|
||||
loadUserPath()
|
||||
if got := X509TrustStoreDir("ca", "web"); got != "truststore/x509/ca/web" {
|
||||
t.Fatalf(`X509TrustStoreDir() = %q, want "truststore/x509/ca/web"`, got)
|
||||
}
|
||||
|
|
60
errors.go
60
errors.go
|
@ -15,17 +15,11 @@ package notation
|
|||
|
||||
// ErrorPushSignatureFailed is used when failed to push signature to the
|
||||
// target registry.
|
||||
//
|
||||
// Deprecated: Use PushSignatureFailedError instead.
|
||||
type ErrorPushSignatureFailed = PushSignatureFailedError
|
||||
|
||||
// PushSignatureFailedError is used when failed to push signature to the
|
||||
// target registry.
|
||||
type PushSignatureFailedError struct {
|
||||
type ErrorPushSignatureFailed struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e PushSignatureFailedError) Error() string {
|
||||
func (e ErrorPushSignatureFailed) Error() string {
|
||||
if e.Msg != "" {
|
||||
return "failed to push signature to registry with error: " + e.Msg
|
||||
}
|
||||
|
@ -34,17 +28,11 @@ func (e PushSignatureFailedError) Error() string {
|
|||
|
||||
// ErrorVerificationInconclusive is used when signature verification fails due
|
||||
// to a runtime error (e.g. a network error)
|
||||
//
|
||||
// Deprecated: Use VerificationInconclusiveError instead.
|
||||
type ErrorVerificationInconclusive = VerificationInconclusiveError
|
||||
|
||||
// VerificationInconclusiveError is used when signature verification fails due
|
||||
// to a runtime error (e.g. a network error)
|
||||
type VerificationInconclusiveError struct {
|
||||
type ErrorVerificationInconclusive struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e VerificationInconclusiveError) Error() string {
|
||||
func (e ErrorVerificationInconclusive) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
|
@ -53,17 +41,11 @@ func (e VerificationInconclusiveError) Error() string {
|
|||
|
||||
// ErrorNoApplicableTrustPolicy is used when there is no trust policy that
|
||||
// applies to the given artifact
|
||||
//
|
||||
// Deprecated: Use NoApplicableTrustPolicyError instead.
|
||||
type ErrorNoApplicableTrustPolicy = NoApplicableTrustPolicyError
|
||||
|
||||
// NoApplicableTrustPolicyError is used when there is no trust policy that
|
||||
// applies to the given artifact
|
||||
type NoApplicableTrustPolicyError struct {
|
||||
type ErrorNoApplicableTrustPolicy struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e NoApplicableTrustPolicyError) Error() string {
|
||||
func (e ErrorNoApplicableTrustPolicy) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
|
@ -72,17 +54,11 @@ func (e NoApplicableTrustPolicyError) Error() string {
|
|||
|
||||
// ErrorSignatureRetrievalFailed is used when notation is unable to retrieve the
|
||||
// digital signature/s for the given artifact
|
||||
//
|
||||
// Deprecated: Use SignatureRetrievalFailedError instead.
|
||||
type ErrorSignatureRetrievalFailed = SignatureRetrievalFailedError
|
||||
|
||||
// SignatureRetrievalFailedError is used when notation is unable to retrieve the
|
||||
// digital signature/s for the given artifact
|
||||
type SignatureRetrievalFailedError struct {
|
||||
type ErrorSignatureRetrievalFailed struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e SignatureRetrievalFailedError) Error() string {
|
||||
func (e ErrorSignatureRetrievalFailed) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
|
@ -91,17 +67,11 @@ func (e SignatureRetrievalFailedError) Error() string {
|
|||
|
||||
// ErrorVerificationFailed is used when it is determined that the digital
|
||||
// signature/s is not valid for the given artifact
|
||||
//
|
||||
// Deprecated: Use VerificationFailedError instead.
|
||||
type ErrorVerificationFailed = VerificationFailedError
|
||||
|
||||
// VerificationFailedError is used when it is determined that the digital
|
||||
// signature/s is not valid for the given artifact
|
||||
type VerificationFailedError struct {
|
||||
type ErrorVerificationFailed struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e VerificationFailedError) Error() string {
|
||||
func (e ErrorVerificationFailed) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
|
@ -110,17 +80,11 @@ func (e VerificationFailedError) Error() string {
|
|||
|
||||
// ErrorUserMetadataVerificationFailed is used when the signature does not
|
||||
// contain the user specified metadata
|
||||
//
|
||||
// Deprecated: Use UserMetadataVerificationFailedError instead.
|
||||
type ErrorUserMetadataVerificationFailed = UserMetadataVerificationFailedError
|
||||
|
||||
// UserMetadataVerificationFailedError is used when the signature does not
|
||||
// contain the user specified metadata
|
||||
type UserMetadataVerificationFailedError struct {
|
||||
type ErrorUserMetadataVerificationFailed struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e UserMetadataVerificationFailedError) Error() string {
|
||||
func (e ErrorUserMetadataVerificationFailed) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
|
|
170
errors_test.go
170
errors_test.go
|
@ -1,170 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notation
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestErrorMessages(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "ErrorPushSignatureFailed with message",
|
||||
err: ErrorPushSignatureFailed{Msg: "test message"},
|
||||
want: "failed to push signature to registry with error: test message",
|
||||
},
|
||||
{
|
||||
name: "ErrorPushSignatureFailed without message",
|
||||
err: ErrorPushSignatureFailed{},
|
||||
want: "failed to push signature to registry",
|
||||
},
|
||||
{
|
||||
name: "ErrorVerificationInconclusive with message",
|
||||
err: ErrorVerificationInconclusive{Msg: "test message"},
|
||||
want: "test message",
|
||||
},
|
||||
{
|
||||
name: "ErrorVerificationInconclusive without message",
|
||||
err: ErrorVerificationInconclusive{},
|
||||
want: "signature verification was inclusive due to an unexpected error",
|
||||
},
|
||||
{
|
||||
name: "ErrorNoApplicableTrustPolicy with message",
|
||||
err: ErrorNoApplicableTrustPolicy{Msg: "test message"},
|
||||
want: "test message",
|
||||
},
|
||||
{
|
||||
name: "ErrorNoApplicableTrustPolicy without message",
|
||||
err: ErrorNoApplicableTrustPolicy{},
|
||||
want: "there is no applicable trust policy for the given artifact",
|
||||
},
|
||||
{
|
||||
name: "ErrorSignatureRetrievalFailed with message",
|
||||
err: ErrorSignatureRetrievalFailed{Msg: "test message"},
|
||||
want: "test message",
|
||||
},
|
||||
{
|
||||
name: "ErrorSignatureRetrievalFailed without message",
|
||||
err: ErrorSignatureRetrievalFailed{},
|
||||
want: "unable to retrieve the digital signature from the registry",
|
||||
},
|
||||
{
|
||||
name: "ErrorVerificationFailed with message",
|
||||
err: ErrorVerificationFailed{Msg: "test message"},
|
||||
want: "test message",
|
||||
},
|
||||
{
|
||||
name: "ErrorVerificationFailed without message",
|
||||
err: ErrorVerificationFailed{},
|
||||
want: "signature verification failed",
|
||||
},
|
||||
{
|
||||
name: "ErrorUserMetadataVerificationFailed with message",
|
||||
err: ErrorUserMetadataVerificationFailed{Msg: "test message"},
|
||||
want: "test message",
|
||||
},
|
||||
{
|
||||
name: "ErrorUserMetadataVerificationFailed without message",
|
||||
err: ErrorUserMetadataVerificationFailed{},
|
||||
want: "unable to find specified metadata in the signature",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.err.Error(); got != tt.want {
|
||||
t.Errorf("Error() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomErrorPrintsCorrectMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "PushSignatureFailedError with message",
|
||||
err: PushSignatureFailedError{Msg: "test message"},
|
||||
want: "failed to push signature to registry with error: test message",
|
||||
},
|
||||
{
|
||||
name: "PushSignatureFailedError without message",
|
||||
err: PushSignatureFailedError{},
|
||||
want: "failed to push signature to registry",
|
||||
},
|
||||
{
|
||||
name: "VerificationInconclusiveError with message",
|
||||
err: VerificationInconclusiveError{Msg: "test message"},
|
||||
want: "test message",
|
||||
},
|
||||
{
|
||||
name: "VerificationInconclusiveError without message",
|
||||
err: VerificationInconclusiveError{},
|
||||
want: "signature verification was inclusive due to an unexpected error",
|
||||
},
|
||||
{
|
||||
name: "NoApplicableTrustPolicyError with message",
|
||||
err: NoApplicableTrustPolicyError{Msg: "test message"},
|
||||
want: "test message",
|
||||
},
|
||||
{
|
||||
name: "NoApplicableTrustPolicyError without message",
|
||||
err: NoApplicableTrustPolicyError{},
|
||||
want: "there is no applicable trust policy for the given artifact",
|
||||
},
|
||||
{
|
||||
name: "SignatureRetrievalFailedError with message",
|
||||
err: SignatureRetrievalFailedError{Msg: "test message"},
|
||||
want: "test message",
|
||||
},
|
||||
{
|
||||
name: "SignatureRetrievalFailedError without message",
|
||||
err: SignatureRetrievalFailedError{},
|
||||
want: "unable to retrieve the digital signature from the registry",
|
||||
},
|
||||
{
|
||||
name: "VerificationFailedError with message",
|
||||
err: VerificationFailedError{Msg: "test message"},
|
||||
want: "test message",
|
||||
},
|
||||
{
|
||||
name: "VerificationFailedError without message",
|
||||
err: VerificationFailedError{},
|
||||
want: "signature verification failed",
|
||||
},
|
||||
{
|
||||
name: "UserMetadataVerificationFailedError with message",
|
||||
err: UserMetadataVerificationFailedError{Msg: "test message"},
|
||||
want: "test message",
|
||||
},
|
||||
{
|
||||
name: "UserMetadataVerificationFailedError without message",
|
||||
err: UserMetadataVerificationFailedError{},
|
||||
want: "unable to find specified metadata in the signature",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.err.Error(); got != tt.want {
|
||||
t.Errorf("Error() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -46,9 +46,9 @@ func Example_localSign() {
|
|||
// exampleSigner is a notation.Signer given key and X509 certificate chain.
|
||||
// Users should replace `exampleCertTuple.PrivateKey` with their own private
|
||||
// key and replace `exampleCerts` with the corresponding full certificate
|
||||
// chain, following the Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
|
||||
// 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)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
|
|
@ -31,10 +31,10 @@ import (
|
|||
|
||||
// examplePolicyDocument is an example of a valid trust policy document.
|
||||
// trust policy document should follow this spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy
|
||||
var examplePolicyDocument = trustpolicy.OCIDocument{
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-policy
|
||||
var examplePolicyDocument = trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"example/software"},
|
||||
|
@ -73,9 +73,9 @@ func Example_localVerify() {
|
|||
}
|
||||
|
||||
// createTrustStore creates a trust store directory for demo purpose.
|
||||
// Users could use the default trust store from Notary Project and
|
||||
// add trusted certificates into it following the trust store spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
|
||||
// Users could use the default trust store from Notary and add trusted
|
||||
// certificates into it following the trust store spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-store
|
||||
if err := createTrustStore(); err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
@ -172,8 +172,8 @@ func createTrustStore() error {
|
|||
// generate the `exampleSignatureEnvelopePem` above.)
|
||||
// Users should replace `exampleX509Certificate` with their own trusted
|
||||
// certificate and add to the trust store, following the
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
// Notary certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
|
||||
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
|
||||
|
|
|
@ -18,13 +18,12 @@ 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")
|
||||
|
@ -45,9 +44,9 @@ func Example_remoteSign() {
|
|||
// exampleSigner is a notation.Signer given key and X509 certificate chain.
|
||||
// Users should replace `exampleCertTuple.PrivateKey` with their own private
|
||||
// key and replace `exampleCerts` with the corresponding full certificate
|
||||
// chain, following the Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
|
||||
// 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)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
@ -70,16 +69,13 @@ func Example_remoteSign() {
|
|||
// remote sign core process
|
||||
// upon successful signing, descriptor of the sign content is returned and
|
||||
// the generated signature is pushed into remote registry.
|
||||
targetManifestDesc, sigManifestDesc, err := notation.SignOCI(context.Background(), exampleSigner, exampleRepo, exampleSignOptions)
|
||||
targetDesc, err := notation.Sign(context.Background(), exampleSigner, exampleRepo, exampleSignOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully signed")
|
||||
fmt.Println("targetManifestDesc.MediaType:", targetManifestDesc.MediaType)
|
||||
fmt.Println("targetManifestDesc.Digest:", targetManifestDesc.Digest)
|
||||
fmt.Println("targetManifestDesc.Size:", targetManifestDesc.Size)
|
||||
fmt.Println("sigManifestDesc.MediaType:", sigManifestDesc.MediaType)
|
||||
fmt.Println("sigManifestDesc.Digest:", sigManifestDesc.Digest)
|
||||
fmt.Println("sigManifestDesc.Size:", sigManifestDesc.Size)
|
||||
fmt.Println("targetDesc MediaType:", targetDesc.MediaType)
|
||||
fmt.Println("targetDesc Digest:", targetDesc.Digest)
|
||||
fmt.Println("targetDesc Size:", targetDesc.Size)
|
||||
}
|
||||
|
|
|
@ -37,10 +37,10 @@ func Example_remoteVerify() {
|
|||
|
||||
// examplePolicyDocument is an example of a valid trust policy document.
|
||||
// trust policy document should follow this spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy
|
||||
examplePolicyDocument := trustpolicy.OCIDocument{
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-policy
|
||||
examplePolicyDocument := trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
|
@ -52,9 +52,9 @@ func Example_remoteVerify() {
|
|||
}
|
||||
|
||||
// generateTrustStore generates a trust store directory for demo purpose.
|
||||
// Users should configure their own trust store and add trusted certificates
|
||||
// into it following the trust store spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
|
||||
// Users could use the default trust store from Notary and add trusted
|
||||
// certificates into it following the trust store spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-store
|
||||
if err := generateTrustStore(); err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
@ -101,8 +101,8 @@ func generateTrustStore() error {
|
|||
// an example of a valid X509 self-signed certificate for demo purpose ONLY.
|
||||
// Users should replace `exampleX509Certificate` with their own trusted
|
||||
// certificate and add to the trust store, following the
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
// Notary certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
|
||||
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
|
||||
|
|
|
@ -1,88 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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/signer"
|
||||
)
|
||||
|
||||
// ExampleSignBlob demonstrates how to use [notation.SignBlob] to sign arbitrary
|
||||
// data.
|
||||
func Example_signBlob() {
|
||||
// exampleSigner implements [notation.Signer] and [notation.BlobSigner].
|
||||
// Given key and X509 certificate chain, it provides method to sign OCI
|
||||
// artifacts or blobs.
|
||||
// Users should replace `exampleCertTuple.PrivateKey` with their own private
|
||||
// key and replace `exampleCerts` with the corresponding certificate chain,
|
||||
// following the Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/specifications/tree/9c81dc773508dedc5a81c02c8d805de04f65050b/specs/signature-specification.md#certificate-requirements
|
||||
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// Both COSE ("application/cose") and JWS ("application/jose+json")
|
||||
// signature mediaTypes are supported.
|
||||
exampleSignatureMediaType := jws.MediaTypeEnvelope
|
||||
exampleContentMediaType := "video/mp4"
|
||||
|
||||
// exampleSignOptions is an example of [notation.SignBlobOptions].
|
||||
exampleSignOptions := notation.SignBlobOptions{
|
||||
SignerSignOptions: notation.SignerSignOptions{
|
||||
SignatureMediaType: exampleSignatureMediaType,
|
||||
SigningAgent: "example signing agent",
|
||||
},
|
||||
ContentMediaType: exampleContentMediaType,
|
||||
UserMetadata: map[string]string{"buildId": "101"},
|
||||
}
|
||||
|
||||
// exampleReader reads the data that needs to be signed.
|
||||
// This data can be in a file or in memory.
|
||||
exampleReader := strings.NewReader("example blob")
|
||||
|
||||
// Upon successful signing, signature envelope and signerInfo are returned.
|
||||
// signatureEnvelope can be used in a verification process later on.
|
||||
signatureEnvelope, signerInfo, err := notation.SignBlob(context.Background(), exampleSigner, exampleReader, exampleSignOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully signed")
|
||||
|
||||
// a peek of the signature envelope generated
|
||||
sigBlob, err := signature.ParseEnvelope(exampleSignatureMediaType, signatureEnvelope)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
sigContent, err := sigBlob.Content()
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
fmt.Println("signature Payload ContentType:", sigContent.Payload.ContentType)
|
||||
fmt.Println("signature Payload Content:", string(sigContent.Payload.Content))
|
||||
fmt.Println("signerInfo SigningAgent:", signerInfo.UnsignedAttributes.SigningAgent)
|
||||
|
||||
// Output:
|
||||
// Successfully signed
|
||||
// signature Payload ContentType: application/vnd.cncf.notary.payload.v1+json
|
||||
// signature Payload Content: {"targetArtifact":{"annotations":{"buildId":"101"},"digest":"sha384:b8ab24dafba5cf7e4c89c562f811cf10493d4203da982d3b1345f366ca863d9c2ed323dbd0fb7ff83a80302ceffa5a61","mediaType":"video/mp4","size":12}}
|
||||
// signerInfo SigningAgent: example signing agent
|
||||
}
|
|
@ -1,113 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/revocation"
|
||||
"github.com/notaryproject/notation-core-go/revocation/purpose"
|
||||
"github.com/notaryproject/notation-core-go/testhelper"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/signer"
|
||||
"github.com/notaryproject/tspclient-go"
|
||||
)
|
||||
|
||||
// Example_signWithTimestamp demonstrates how to use notation.Sign to sign an
|
||||
// artifact with a RFC 3161 compliant timestamp countersignature and
|
||||
// user trusted TSA root certificate
|
||||
func Example_signWithTimestamp() {
|
||||
// exampleArtifactReference is an example of the target artifact reference
|
||||
var exampleArtifactReference = "localhost:5000/software@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e"
|
||||
|
||||
// exampleCertTuple contains a RSA privateKey and a self-signed X509
|
||||
// certificate generated for demo purpose ONLY.
|
||||
exampleCertTuple := testhelper.GetRSASelfSignedSigningCertTuple("Notation Example self-signed")
|
||||
exampleCerts := []*x509.Certificate{exampleCertTuple.Cert}
|
||||
|
||||
// exampleSigner is a notation.Signer given key and X509 certificate chain.
|
||||
// Users should replace `exampleCertTuple.PrivateKey` with their own private
|
||||
// key and replace `exampleCerts` with the corresponding full certificate
|
||||
// chain, following the Notary Project certificate requirements:
|
||||
// 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
|
||||
}
|
||||
|
||||
// exampleRepo is an example of registry.Repository.
|
||||
remoteRepo, err := remote.NewRepository(exampleArtifactReference)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
exampleRepo := registry.NewRepository(remoteRepo)
|
||||
|
||||
// replace exampleRFC3161TSAServer with your trusted TSA server URL.
|
||||
exampleRFC3161TSAServer := "<TSA server URL>"
|
||||
httpTimestamper, err := tspclient.NewHTTPTimestamper(nil, exampleRFC3161TSAServer)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// replace exampleTSARootCertPem with your trusted TSA root cert.
|
||||
exampleTSARootCertPem := "<TSA root cert>"
|
||||
block, _ := pem.Decode([]byte(exampleTSARootCertPem))
|
||||
if block == nil {
|
||||
panic("failed to parse tsa root certificate PEM")
|
||||
}
|
||||
tsaRootCert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
panic("failed to parse tsa root certificate: " + err.Error())
|
||||
}
|
||||
tsaRootCAs := x509.NewCertPool()
|
||||
tsaRootCAs.AddCert(tsaRootCert)
|
||||
|
||||
// enable timestamping certificate chain revocation check
|
||||
tsaRevocationValidator, err := revocation.NewWithOptions(revocation.Options{
|
||||
CertChainPurpose: purpose.Timestamping,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleSignOptions is an example of notation.SignOptions.
|
||||
exampleSignOptions := notation.SignOptions{
|
||||
SignerSignOptions: notation.SignerSignOptions{
|
||||
SignatureMediaType: exampleSignatureMediaType,
|
||||
Timestamper: httpTimestamper,
|
||||
TSARootCAs: tsaRootCAs,
|
||||
TSARevocationValidator: tsaRevocationValidator,
|
||||
},
|
||||
ArtifactReference: exampleArtifactReference,
|
||||
}
|
||||
|
||||
targetManifestDesc, sigManifestDesc, err := notation.SignOCI(context.Background(), exampleSigner, exampleRepo, exampleSignOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully signed")
|
||||
fmt.Println("targetManifestDesc.MediaType:", targetManifestDesc.MediaType)
|
||||
fmt.Println("targetManifestDesc.Digest:", targetManifestDesc.Digest)
|
||||
fmt.Println("targetManifestDesc.Size:", targetManifestDesc.Size)
|
||||
fmt.Println("sigManifestDesc.MediaType:", sigManifestDesc.MediaType)
|
||||
fmt.Println("sigManifestDesc.Digest:", sigManifestDesc.Digest)
|
||||
fmt.Println("sigManifestDesc.Size:", sigManifestDesc.Size)
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature/jws"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-go/verifier"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
)
|
||||
|
||||
// exampleBlobPolicyDocument is an example of a valid blob trust policy document.
|
||||
// blob trust policy document should follow this spec:
|
||||
// https://github.com/notaryproject/specifications/tree/9c81dc773508dedc5a81c02c8d805de04f65050b/specs/trust-store-trust-policy.md#blob-trust-policy
|
||||
var exampleBlobPolicyDocument = trustpolicy.BlobDocument{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.BlobTrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelStrict.Name, Override: map[trustpolicy.ValidationType]trustpolicy.ValidationAction{trustpolicy.TypeRevocation: trustpolicy.ActionSkip}},
|
||||
TrustStores: []string{"ca:valid-trust-store"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ExampleVerifyBlob demonstrates how to use [notation.VerifyBlob] to verify a
|
||||
// signature of an arbitrary blob.
|
||||
func Example_verifyBlob() {
|
||||
// Both COSE ("application/cose") and JWS ("application/jose+json")
|
||||
// signature mediaTypes are supported.
|
||||
exampleSignatureMediaType := jws.MediaTypeEnvelope
|
||||
|
||||
// exampleSignatureEnvelope is a valid signature envelope.
|
||||
exampleSignatureEnvelope := getSignatureEnvelope()
|
||||
|
||||
// createTrustStoreForBlobVerify creates a trust store directory for demo purpose.
|
||||
// Users could use the default trust store from Notary Project and add trusted
|
||||
// certificates into it following the trust store spec:
|
||||
// https://github.com/notaryproject/specifications/tree/9c81dc773508dedc5a81c02c8d805de04f65050b/specs/trust-store-trust-policy.md#trust-store
|
||||
if err := createTrustStoreForBlobVerify(); err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleVerifier implements [notation.Verify] and [notation.VerifyBlob].
|
||||
exampleVerifier, err := verifier.NewVerifierWithOptions(truststore.NewX509TrustStore(dir.ConfigFS()), verifier.VerifierOptions{
|
||||
BlobTrustPolicy: &exampleBlobPolicyDocument,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleReader reads the data that needs to be verified.
|
||||
// This data can be in a file or in memory.
|
||||
exampleReader := strings.NewReader("example blob")
|
||||
|
||||
// exampleVerifyOptions is an example of [notation.VerifyBlobOptions]
|
||||
exampleVerifyOptions := notation.VerifyBlobOptions{
|
||||
BlobVerifierVerifyOptions: notation.BlobVerifierVerifyOptions{
|
||||
SignatureMediaType: exampleSignatureMediaType,
|
||||
TrustPolicyName: "test-statement-name",
|
||||
},
|
||||
}
|
||||
|
||||
// upon successful verification, the signature verification outcome is
|
||||
// returned.
|
||||
_, outcome, err := notation.VerifyBlob(context.Background(), exampleVerifier, exampleReader, []byte(exampleSignatureEnvelope), exampleVerifyOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully verified")
|
||||
|
||||
// a peek of the payload inside the signature envelope
|
||||
fmt.Println("payload ContentType:", outcome.EnvelopeContent.Payload.ContentType)
|
||||
|
||||
// Note, upon successful verification, payload.TargetArtifact from the
|
||||
// signature envelope matches exactly with our exampleTargetDescriptor.
|
||||
// (This check has been done for the user inside verifier.Verify.)
|
||||
fmt.Println("payload Content:", string(outcome.EnvelopeContent.Payload.Content))
|
||||
|
||||
// Output:
|
||||
// Successfully verified
|
||||
// payload ContentType: application/vnd.cncf.notary.payload.v1+json
|
||||
// payload Content: {"targetArtifact":{"digest":"sha384:b8ab24dafba5cf7e4c89c562f811cf10493d4203da982d3b1345f366ca863d9c2ed323dbd0fb7ff83a80302ceffa5a61","mediaType":"video/mp4","size":12}}
|
||||
}
|
||||
|
||||
func createTrustStoreForBlobVerify() error {
|
||||
// changing the path of the trust store for demo purpose.
|
||||
// Users could keep the default value, i.e. os.UserConfigDir.
|
||||
dir.UserConfigDir = "tmp"
|
||||
|
||||
// an example of a valid X509 self-signed certificate for demo purpose ONLY.
|
||||
// (This self-signed cert is paired with the private key used to
|
||||
// generate the `exampleSignatureEnvelopePem` above.)
|
||||
// Users should replace `exampleX509Certificate` with their own trusted
|
||||
// certificate and add to the trust store, following the
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/specifications/tree/9c81dc773508dedc5a81c02c8d805de04f65050b/specs/signature-specification.md#certificate-requirements
|
||||
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIEbDCCAtSgAwIBAgIBUzANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEl
|
||||
MCMGA1UEAxMcTm90YXRpb24gRXhhbXBsZSBzZWxmLXNpZ25lZDAgFw0yNDA0MDQy
|
||||
MTIwMjBaGA8yMTI0MDQwNDIxMjAyMFowZDELMAkGA1UEBhMCVVMxCzAJBgNVBAgT
|
||||
AldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxJTAjBgNVBAMT
|
||||
HE5vdGF0aW9uIEV4YW1wbGUgc2VsZi1zaWduZWQwggGiMA0GCSqGSIb3DQEBAQUA
|
||||
A4IBjwAwggGKAoIBgQDGIiN4yCjSVqFELZwxK/BMb8BokP587L8oPrZ1g8H7LudB
|
||||
moLNDT7vF9xccbCfU3yNuOd0WaOgnENiCs81VHidyJsj1Oz3u+0Zn3ng7V+uZr6m
|
||||
AIO74efA9ClMiY4i4HIt8IAZF57AL2mzDnCITgSWxikf030Il85MI42STvA+qYuz
|
||||
ZEOp3XvKo8bDgQFvbtgK0HYYMfrka7VDmIWVo0rBMGm5btI8HOYQ0r9aqsrCxLAv
|
||||
1AQeOQm+wbRcp4R5PIUJr+REGn7JCbOyXg/7qqHXKKmvV5yrGaraw8gZ5pqP/RHK
|
||||
XUJIfvD0Vf2epJmsvC+6vXkSWtz+cA8J4GQx4J4SXL57hoYkC5qv39SOLzlWls3I
|
||||
6fgeO+SZ0sceMd8NKlom/L5eOJBfB3bTQB83hq/3bRtjT7/qCMsL3VcndKkS+vGF
|
||||
JPw5uTH+pmBgHrLr6tRoRRjwRFuZ0dO05AbdjCaxgVDtFI3wNbaXn/1VlRGySQIS
|
||||
UNWxCrUsSzndeqwmjqsCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM
|
||||
MAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBgQBdi0SaJAaeKBB0I+Fjcbmc
|
||||
4zRvHE4GDSMSDnAK97nrZCZ9iwKuY4x6mv9lwQe2P3VXROoL9JmONNf0yaObOwQj
|
||||
ILGnbe2rzYtUardz2gzh+6KNzJHspRvk1f06mp4496XQ3STMRSr8kno1svKQMy0Y
|
||||
FRsGMKs4fWHavIAqNXg9ymrZvvXiatN2UiVtAA/jBFScZAWskeb2WHNzORi7H5Z1
|
||||
mp5+IlNYQpzdIu/dvLVxzhh2UvkRdsQqsMgt/MOU84RncwUNZM4yI5EGPoaSJdsj
|
||||
AGNd+UV6ur7QmVI2Q9EZNRlaDJtaoZmKns5j1SlmDXWKbdRmw42ORDudODj/pHA9
|
||||
+u+ca9t3uLsbqO9yPm8m+6fyxffWS11QAH6O7EjydJWcEe5tYkPpL6kcaEyQKESm
|
||||
5CDlsk+W3ElpaUu6tsnGKODvgdAN3m0noC+qxzCMqoCM4+M5V6OptR98MDl2FK0B
|
||||
5+WF6YHBxf/uqDvFktUczjrIWuyfECywp05bpGAErGE=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// Adding the certificate into the trust store.
|
||||
if err := os.MkdirAll("tmp/truststore/x509/ca/valid-trust-store", 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile("tmp/truststore/x509/ca/valid-trust-store/NotationBlobExample.pem", []byte(exampleX509Certificate), 0600)
|
||||
}
|
||||
|
||||
func getSignatureEnvelope() string {
|
||||
return `{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEzODQ6YjhhYjI0ZGFmYmE1Y2Y3ZTRjODljNTYyZjgxMWNmMTA0OTNkNDIwM2RhOTgyZDNiMTM0NWYzNjZjYTg2M2Q5YzJlZDMyM2RiZDBmYjdmZjgzYTgwMzAyY2VmZmE1YTYxIiwibWVkaWFUeXBlIjoidmlkZW8vbXA0Iiwic2l6ZSI6MTJ9fQ","protected":"eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDI0LTA0LTA0VDE0OjIwOjIxLTA3OjAwIn0","header":{"x5c":["MIIEbDCCAtSgAwIBAgIBUzANBgkqhkiG9w0BAQsFADBkMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTElMCMGA1UEAxMcTm90YXRpb24gRXhhbXBsZSBzZWxmLXNpZ25lZDAgFw0yNDA0MDQyMTIwMjBaGA8yMTI0MDQwNDIxMjAyMFowZDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxJTAjBgNVBAMTHE5vdGF0aW9uIEV4YW1wbGUgc2VsZi1zaWduZWQwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDGIiN4yCjSVqFELZwxK/BMb8BokP587L8oPrZ1g8H7LudBmoLNDT7vF9xccbCfU3yNuOd0WaOgnENiCs81VHidyJsj1Oz3u+0Zn3ng7V+uZr6mAIO74efA9ClMiY4i4HIt8IAZF57AL2mzDnCITgSWxikf030Il85MI42STvA+qYuzZEOp3XvKo8bDgQFvbtgK0HYYMfrka7VDmIWVo0rBMGm5btI8HOYQ0r9aqsrCxLAv1AQeOQm+wbRcp4R5PIUJr+REGn7JCbOyXg/7qqHXKKmvV5yrGaraw8gZ5pqP/RHKXUJIfvD0Vf2epJmsvC+6vXkSWtz+cA8J4GQx4J4SXL57hoYkC5qv39SOLzlWls3I6fgeO+SZ0sceMd8NKlom/L5eOJBfB3bTQB83hq/3bRtjT7/qCMsL3VcndKkS+vGFJPw5uTH+pmBgHrLr6tRoRRjwRFuZ0dO05AbdjCaxgVDtFI3wNbaXn/1VlRGySQISUNWxCrUsSzndeqwmjqsCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBgQBdi0SaJAaeKBB0I+Fjcbmc4zRvHE4GDSMSDnAK97nrZCZ9iwKuY4x6mv9lwQe2P3VXROoL9JmONNf0yaObOwQjILGnbe2rzYtUardz2gzh+6KNzJHspRvk1f06mp4496XQ3STMRSr8kno1svKQMy0YFRsGMKs4fWHavIAqNXg9ymrZvvXiatN2UiVtAA/jBFScZAWskeb2WHNzORi7H5Z1mp5+IlNYQpzdIu/dvLVxzhh2UvkRdsQqsMgt/MOU84RncwUNZM4yI5EGPoaSJdsjAGNd+UV6ur7QmVI2Q9EZNRlaDJtaoZmKns5j1SlmDXWKbdRmw42ORDudODj/pHA9+u+ca9t3uLsbqO9yPm8m+6fyxffWS11QAH6O7EjydJWcEe5tYkPpL6kcaEyQKESm5CDlsk+W3ElpaUu6tsnGKODvgdAN3m0noC+qxzCMqoCM4+M5V6OptR98MDl2FK0B5+WF6YHBxf/uqDvFktUczjrIWuyfECywp05bpGAErGE="],"io.cncf.notary.signingAgent":"example signing agent"},"signature":"liOjdgQ9BKuQTZGXRh3o6P8AMUIq_MKQReEcqA5h8M4RYs3DV_wXfaLCr2x_NRcwjTZsoO1_J77hmzkkk4L0IuFP8Qw0KKtmc83G0yFi4yYV5fwzrIbnhC2GRLuqLPnK-C4qYmv52ld3ebvo7XWwRHu30-VXePmTRFp6iG-eSAgkNgwhxSZ0ZmTFLG3ceNiX2bxpLHlXdPwA3aFKbd6nKrzo4CZ1ZyLNmAIaoA5-kmc0Hyt45trpxaaiWusI_pcTLw71YCqEAs32tEq3q6hRAgAZZN-Qvm9GyNp9EuaPiKjMbJFqtjome5ITxyNd-5t09dDCUgSe3t-iqv2Blm4E080AP1TYwUKLYklGniUP1dAtOau5G2juZLpl7tr4LQ99mycflnAmV7e79eEWXffvy5EAl77dW4_vM7lEemm08m2wddGuDOWXYb1j1r2_a5Xb92umHq6ZMhAp200A0pUkm9640x8z5jdudi_7KeezdqUK7ZMmSxHohiylyKD_20Cy"}`
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "github.com/notaryproject/notation-core-go/signature/cose"
|
||||
_ "github.com/notaryproject/notation-core-go/signature/jws"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/verifier"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
)
|
||||
|
||||
// Example_verifyWithTimestamp demonstrates how to use notation.Verify to verify
|
||||
// signature of an artifact including RFC 3161 compliant timestamp countersignature
|
||||
func Example_verifyWithTimestamp() {
|
||||
// exampleArtifactReference is an example of the target artifact reference
|
||||
exampleArtifactReference := "localhost:5000/software@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e"
|
||||
|
||||
// examplePolicyDocument is an example of a valid trust policy document with
|
||||
// timestamping configurations.
|
||||
// trust policy document should follow this spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy
|
||||
examplePolicyDocument := trustpolicy.OCIDocument{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
|
||||
// verify timestamp countersignature only if the signing
|
||||
// certificate chain has expired.
|
||||
// Default: trustpolicy.OptionAlways
|
||||
VerifyTimestamp: trustpolicy.OptionAfterCertExpiry,
|
||||
},
|
||||
|
||||
// `tsa` trust store type MUST be configured to enable
|
||||
// timestamp verification
|
||||
TrustStores: []string{"ca:valid-trust-store", "tsa:valid-tsa"},
|
||||
|
||||
// TrustedIdentities only contains trusted identities of `ca`
|
||||
// and `signingAuthority`
|
||||
TrustedIdentities: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// generateTrustStoreWithTimestamp generates a trust store directory for demo purpose.
|
||||
// Users should configure their own trust store and add trusted certificates
|
||||
// into it following the trust store spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
|
||||
if err := generateTrustStoreWithTimestamp(); err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleVerifier is an example of notation.Verifier given
|
||||
// trust policy document and X509 trust store.
|
||||
exampleVerifier, err := verifier.New(&examplePolicyDocument, truststore.NewX509TrustStore(dir.ConfigFS()), nil)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleRepo is an example of registry.Repository.
|
||||
remoteRepo, err := remote.NewRepository(exampleArtifactReference)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
exampleRepo := registry.NewRepository(remoteRepo)
|
||||
|
||||
// exampleVerifyOptions is an example of notation.VerifyOptions.
|
||||
exampleVerifyOptions := notation.VerifyOptions{
|
||||
ArtifactReference: exampleArtifactReference,
|
||||
MaxSignatureAttempts: 50,
|
||||
}
|
||||
|
||||
// remote verify core process
|
||||
// upon successful verification, the target manifest descriptor
|
||||
// and signature verification outcome are returned.
|
||||
targetDesc, _, err := notation.Verify(context.Background(), exampleVerifier, exampleRepo, exampleVerifyOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully verified")
|
||||
fmt.Println("targetDesc MediaType:", targetDesc.MediaType)
|
||||
fmt.Println("targetDesc Digest:", targetDesc.Digest)
|
||||
fmt.Println("targetDesc Size:", targetDesc.Size)
|
||||
}
|
||||
|
||||
func generateTrustStoreWithTimestamp() error {
|
||||
// changing the path of the trust store for demo purpose.
|
||||
// Users could keep the default value, i.e. os.UserConfigDir.
|
||||
dir.UserConfigDir = "tmp"
|
||||
|
||||
// an example of a valid X509 self-signed certificate for demo purpose ONLY.
|
||||
// Users should replace `exampleX509Certificate` with their own trusted
|
||||
// certificate and add to the trust store, following the
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
|
||||
MA0GA1UEAxMGYWxwaW5lMCAXDTAwMDgyOTEzNTAwMFoYDzIxMjMwODI5MTM1MDAw
|
||||
WjBOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUx
|
||||
DzANBgNVBAoTBk5vdGFyeTEPMA0GA1UEAxMGYWxwaW5lMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEAocg3qEsyNDDLfB8OHD4dhi+M1NPK1Asy5NX84c+g
|
||||
vacZuoPLTwmpOfm6nPt7GPPB9G7S6xxhFNbRxTYfYUjK+kaCj38XjBRf5lGewbSJ
|
||||
KVkxQ82/axU70ceSW3JpazrageN9JUTZ/Jfi4MfnipITwcmMoiij8eGrHskjyVyZ
|
||||
bJd0WMMKRDWVhLPUiPMVWt/4d7YtZItzacaQKtXmXgsTCTWpIols3gftNYjrQoMs
|
||||
UelUdD8vOAWN9J28/SyC+uSh/K1KfyUlbqufn4di8DEBxntP5wnXYbJL1jtjsUgE
|
||||
xAVjQxT1zI59X36m3t3YKqCQh1cud02L5onObY6zj57N6QIDAQABoycwJTAOBgNV
|
||||
HQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQAD
|
||||
ggEBAC8AjBLy7EsRpi6oguCdFSb6nRGjvF17N+b6mDb3sARnB8T1pxvzTT26ya+A
|
||||
yWR+jjodEwbMIS+13lV+9qT2LwqlbOUNY519Pa2GRRY72JjeowWI3iKkKaMzfZUB
|
||||
7lRTGXdEuZApLbTO/3JVcR9ffu00N1UaAP9YGElSt4JDJYA9M+d/Qto+HiIsE0Kj
|
||||
+jdnwIYovPPOlryKOLfFb/r1GEq7n63xFZz83iyWNaZdsJ5N3YHxdOpkbBbCalOE
|
||||
BDJTjQKqeAYBLoANNU0OBslmqHCSBTEnhbqJHN6QKyF09ScOl5LwM1QsTl0UY5si
|
||||
GLAfj/jSf9OH9VLTPHOS8/N0Ka4=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// Adding the certificate into the trust store.
|
||||
if err := os.MkdirAll("tmp/truststore/x509/ca/valid-trust-store", 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile("tmp/truststore/x509/ca/valid-trust-store/NotationExample.pem", []byte(exampleX509Certificate), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// an example of a valid TSA root certificate for demo purpose ONLY.
|
||||
// Users should replace `exampleTSARootCertificate` with their own trusted
|
||||
// TSA root certificate and add to the trust store, following the
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleTSARootCertificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
|
||||
RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV
|
||||
UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
|
||||
Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG
|
||||
SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y
|
||||
ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If
|
||||
xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV
|
||||
ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO
|
||||
DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ
|
||||
jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/
|
||||
CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi
|
||||
EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM
|
||||
fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY
|
||||
uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK
|
||||
chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t
|
||||
9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
|
||||
hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD
|
||||
ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2
|
||||
SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd
|
||||
+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc
|
||||
fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa
|
||||
sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N
|
||||
cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N
|
||||
0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie
|
||||
4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI
|
||||
r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1
|
||||
/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm
|
||||
gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// Adding the tsa root certificate into the trust store.
|
||||
if err := os.MkdirAll("tmp/truststore/x509/tsa/valid-tsa", 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile("tmp/truststore/x509/tsa/valid-tsa/NotationTSAExample.pem", []byte(exampleTSARootCertificate), 0600)
|
||||
}
|
27
go.mod
27
go.mod
|
@ -1,26 +1,23 @@
|
|||
module github.com/notaryproject/notation-go
|
||||
|
||||
go 1.23.0
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/go-ldap/ldap/v3 v3.4.11
|
||||
github.com/notaryproject/notation-core-go v1.3.0
|
||||
github.com/notaryproject/notation-plugin-framework-go v1.0.0
|
||||
github.com/notaryproject/tspclient-go v1.0.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.5
|
||||
github.com/notaryproject/notation-core-go v1.0.0
|
||||
github.com/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/veraison/go-cose v1.3.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/mod v0.25.0
|
||||
oras.land/oras-go/v2 v2.6.0
|
||||
github.com/opencontainers/image-spec v1.1.0-rc4
|
||||
github.com/veraison/go-cose v1.1.0
|
||||
golang.org/x/crypto v0.11.0
|
||||
golang.org/x/mod v0.12.0
|
||||
oras.land/oras-go/v2 v2.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sync v0.3.0 // indirect
|
||||
)
|
||||
|
|
115
go.sum
115
go.sum
|
@ -1,60 +1,79 @@
|
|||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA=
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/notaryproject/notation-core-go v1.3.0 h1:mWJaw1QBpBxpjLSiKOjzbZvB+xh2Abzk14FHWQ+9Kfs=
|
||||
github.com/notaryproject/notation-core-go v1.3.0/go.mod h1:hzvEOit5lXfNATGNBT8UQRx2J6Fiw/dq/78TQL8aE64=
|
||||
github.com/notaryproject/notation-plugin-framework-go v1.0.0 h1:6Qzr7DGXoCgXEQN+1gTZWuJAZvxh3p8Lryjn5FaLzi4=
|
||||
github.com/notaryproject/notation-plugin-framework-go v1.0.0/go.mod h1:RqWSrTOtEASCrGOEffq0n8pSg2KOgKYiWqFWczRSics=
|
||||
github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4=
|
||||
github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.5 h1:ekEKmaDrpvR2yf5Nc/DClsGG9lAmdDixe44mLzlW5r8=
|
||||
github.com/go-ldap/ldap/v3 v3.4.5/go.mod h1:bMGIq3AGbytbaMwf8wdv5Phdxz0FWHTIYMSzyrYgnQs=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/notaryproject/notation-core-go v1.0.0 h1:FgOAihtFW4XU9JYyTzItg1xW3OaN4eCasw5Bp00Ydu4=
|
||||
github.com/notaryproject/notation-core-go v1.0.0/go.mod h1:eoHFJ2e6b31GZO9hckCms5kfXvHLTySvJ1QwRLB9ZCk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7rk=
|
||||
github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o=
|
||||
github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
|
||||
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
|
||||
oras.land/oras-go/v2 v2.2.1 h1:3VJTYqy5KfelEF9c2jo1MLSpr+TM3mX8K42wzZcd6qE=
|
||||
oras.land/oras-go/v2 v2.2.1/go.mod h1:GeAwLuC4G/JpNwkd+bSZ6SkDMGaaYglt6YK2WvZP7uQ=
|
||||
|
|
|
@ -13,141 +13,9 @@
|
|||
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// tempFileNamePrefix is the prefix of the temporary file
|
||||
tempFileNamePrefix = "notation-*"
|
||||
)
|
||||
|
||||
// ErrNotRegularFile is returned when the file is not an regular file.
|
||||
var ErrNotRegularFile = errors.New("not regular file")
|
||||
|
||||
// ErrNotDirectory is returned when the path is not a directory.
|
||||
var ErrNotDirectory = errors.New("not directory")
|
||||
import "regexp"
|
||||
|
||||
// IsValidFileName checks if a file name is cross-platform compatible
|
||||
func IsValidFileName(fileName string) bool {
|
||||
return regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`).MatchString(fileName)
|
||||
}
|
||||
|
||||
// CopyToDir copies the src file to dst dir. All parent directories are created
|
||||
// with permissions 0755.
|
||||
//
|
||||
// Source file's read and execute permissions are preserved for everyone.
|
||||
// Write permission is preserved for owner. Group and others cannot write.
|
||||
// Existing file will be overwritten.
|
||||
func CopyToDir(src, dst string) error {
|
||||
sourceFileInfo, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !sourceFileInfo.Mode().IsRegular() {
|
||||
return ErrNotRegularFile
|
||||
}
|
||||
source, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
if err := os.MkdirAll(dst, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
dstFile := filepath.Join(dst, filepath.Base(src))
|
||||
destination, err := os.Create(dstFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destination.Close()
|
||||
err = destination.Chmod(sourceFileInfo.Mode() & os.FileMode(0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(destination, source)
|
||||
return err
|
||||
}
|
||||
|
||||
// CopyDirToDir copies contents in src dir to dst dir. Only regular files are
|
||||
// copied. Existing files will be overwritten.
|
||||
func CopyDirToDir(src, dst string) error {
|
||||
fi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !fi.Mode().IsDir() {
|
||||
return ErrNotDirectory
|
||||
}
|
||||
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// skip sub-directories
|
||||
if d.IsDir() && d.Name() != filepath.Base(path) {
|
||||
return fs.SkipDir
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// only copy regular files
|
||||
if info.Mode().IsRegular() {
|
||||
return CopyToDir(path, dst)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// TrimFileExtension returns the file name without extension.
|
||||
//
|
||||
// For example,
|
||||
//
|
||||
// when input is xyz.exe, output is xyz
|
||||
//
|
||||
// when input is xyz.tar.gz, output is xyz.tar
|
||||
func TrimFileExtension(fileName string) string {
|
||||
return strings.TrimSuffix(fileName, filepath.Ext(fileName))
|
||||
}
|
||||
|
||||
// WriteFile writes content to a temporary file and moves it to path.
|
||||
// If path already exists and is a file, WriteFile overwrites it.
|
||||
//
|
||||
// Parameters:
|
||||
// - tempDir is the directory to create the temporary file. It should be
|
||||
// in the same mount point as path. If tempDir is empty, the default
|
||||
// directory for temporary files is used.
|
||||
// - path is the destination file path.
|
||||
// - content is the content to write.
|
||||
func WriteFile(tempDir, path string, content []byte) (writeErr error) {
|
||||
tempFile, err := os.CreateTemp(tempDir, tempFileNamePrefix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
// remove the temp file in case of error
|
||||
if writeErr != nil {
|
||||
tempFile.Close()
|
||||
os.Remove(tempFile.Name())
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := tempFile.Write(content); err != nil {
|
||||
return fmt.Errorf("failed to write content to temp file: %w", err)
|
||||
}
|
||||
|
||||
// close before moving
|
||||
if err := tempFile.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close temp file: %w", err)
|
||||
}
|
||||
|
||||
// rename is atomic on UNIX-like platforms
|
||||
return os.Rename(tempFile.Name(), path)
|
||||
}
|
||||
|
|
|
@ -1,214 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCopyToDir(t *testing.T) {
|
||||
t.Run("copy file", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := []byte("data")
|
||||
filename := filepath.Join(tempDir, "a", "file.txt")
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := WriteFile(tempDir, filename, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
destDir := filepath.Join(tempDir, "b")
|
||||
if err := CopyToDir(filename, destDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("source directory permission error", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
destDir := t.TempDir()
|
||||
data := []byte("data")
|
||||
filename := filepath.Join(tempDir, "a", "file.txt")
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := WriteFile(tempDir, filename, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(tempDir, 0000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chmod(tempDir, 0700)
|
||||
|
||||
if err := CopyToDir(filename, destDir); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("not a regular file", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
destDir := t.TempDir()
|
||||
if err := CopyToDir(tempDir, destDir); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("source file permission error", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
destDir := t.TempDir()
|
||||
data := []byte("data")
|
||||
// prepare file
|
||||
filename := filepath.Join(tempDir, "a", "file.txt")
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := WriteFile(tempDir, filename, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// forbid reading
|
||||
if err := os.Chmod(filename, 0000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chmod(filename, 0600)
|
||||
if err := CopyToDir(filename, destDir); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dest directory permission error", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
destTempDir := t.TempDir()
|
||||
data := []byte("data")
|
||||
// prepare file
|
||||
filename := filepath.Join(tempDir, "a", "file.txt")
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := WriteFile(tempDir, filename, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// forbid dest directory operation
|
||||
if err := os.Chmod(destTempDir, 0000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chmod(destTempDir, 0700)
|
||||
if err := CopyToDir(filename, filepath.Join(destTempDir, "a")); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("dest directory permission error 2", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
|
||||
tempDir := t.TempDir()
|
||||
destTempDir := t.TempDir()
|
||||
data := []byte("data")
|
||||
// prepare file
|
||||
filename := filepath.Join(tempDir, "a", "file.txt")
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := WriteFile(tempDir, filename, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// forbid writing to destTempDir
|
||||
if err := os.Chmod(destTempDir, 0000); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chmod(destTempDir, 0700)
|
||||
if err := CopyToDir(filename, destTempDir); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("copy file and check content", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
data := []byte("data")
|
||||
filename := filepath.Join(tempDir, "a", "file.txt")
|
||||
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := WriteFile(tempDir, filename, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
destDir := filepath.Join(tempDir, "b")
|
||||
if err := CopyToDir(filename, destDir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
validFileContent(t, filepath.Join(destDir, "file.txt"), data)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileNameWithoutExtension(t *testing.T) {
|
||||
input := "testfile.tar.gz"
|
||||
expectedOutput := "testfile.tar"
|
||||
actualOutput := TrimFileExtension(input)
|
||||
if actualOutput != expectedOutput {
|
||||
t.Errorf("expected '%s', but got '%s'", expectedOutput, actualOutput)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
content := []byte("test WriteFile")
|
||||
|
||||
t.Run("permission denied", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
err := os.Chmod(tempDir, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = WriteFile(tempDir, filepath.Join(tempDir, "testFile"), content)
|
||||
if err == nil || !strings.Contains(err.Error(), "permission denied") {
|
||||
t.Fatalf("expected permission denied error, but got %s", err)
|
||||
}
|
||||
err = os.Chmod(tempDir, 0700)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func validFileContent(t *testing.T, filename string, content []byte) {
|
||||
b, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(content, b) {
|
||||
t.Fatal("file content is not correct")
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package io provides a LimitWriter that writes to an underlying writer up to
|
||||
// a limit.
|
||||
|
||||
package io
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
// ErrLimitExceeded is returned when the write limit is exceeded.
|
||||
var ErrLimitExceeded = errors.New("write limit exceeded")
|
||||
|
||||
// LimitedWriter is a writer that writes to an underlying writer up to a limit.
|
||||
type LimitedWriter struct {
|
||||
W io.Writer // underlying writer
|
||||
N int64 // remaining bytes
|
||||
}
|
||||
|
||||
// LimitWriter returns a new LimitWriter that writes to w.
|
||||
//
|
||||
// parameters:
|
||||
// w: the writer to write to
|
||||
// limit: the maximum number of bytes to write
|
||||
func LimitWriter(w io.Writer, limit int64) *LimitedWriter {
|
||||
return &LimitedWriter{W: w, N: limit}
|
||||
}
|
||||
|
||||
// Write writes p to the underlying writer up to the limit.
|
||||
func (l *LimitedWriter) Write(p []byte) (int, error) {
|
||||
if l.N <= 0 {
|
||||
return 0, ErrLimitExceeded
|
||||
}
|
||||
if int64(len(p)) > l.N {
|
||||
p = p[:l.N]
|
||||
}
|
||||
n, err := l.W.Write(p)
|
||||
l.N -= int64(n)
|
||||
return n, err
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package io
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLimitWriter(t *testing.T) {
|
||||
limit := int64(10)
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
written int
|
||||
}{
|
||||
{"hello", "hello", 5},
|
||||
{" world", " world", 6},
|
||||
{"!", "!", 1},
|
||||
{"1234567891011", "1234567891", 10},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
var buf bytes.Buffer
|
||||
lw := LimitWriter(&buf, limit)
|
||||
n, err := lw.Write([]byte(tt.input))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if n != tt.written {
|
||||
t.Errorf("expected %d bytes written, got %d", tt.written, n)
|
||||
}
|
||||
if buf.String() != tt.expected {
|
||||
t.Errorf("expected buffer %q, got %q", tt.expected, buf.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimitWriterFailed(t *testing.T) {
|
||||
limit := int64(10)
|
||||
longString := "1234567891011"
|
||||
|
||||
var buf bytes.Buffer
|
||||
lw := LimitWriter(&buf, limit)
|
||||
_, err := lw.Write([]byte(longString))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
_, err = lw.Write([]byte(longString))
|
||||
expectedErr := errors.New("write limit exceeded")
|
||||
if err.Error() != expectedErr.Error() {
|
||||
t.Errorf("expected error %v, got %v", expectedErr, err)
|
||||
}
|
||||
}
|
|
@ -18,7 +18,8 @@ import (
|
|||
_ "embed"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
"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"
|
||||
)
|
||||
|
@ -125,7 +126,6 @@ type Repository struct {
|
|||
FetchSignatureBlobError error
|
||||
MissMatchDigest bool
|
||||
ExceededNumOfSignatures bool
|
||||
PushSignatureError error
|
||||
}
|
||||
|
||||
func NewRepository() Repository {
|
||||
|
@ -164,44 +164,40 @@ func (t Repository) FetchSignatureBlob(ctx context.Context, desc ocispec.Descrip
|
|||
}
|
||||
|
||||
func (t Repository) PushSignature(ctx context.Context, mediaType string, blob []byte, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) {
|
||||
if t.PushSignatureError != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, t.PushSignatureError
|
||||
}
|
||||
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, nil
|
||||
}
|
||||
|
||||
type PluginMock struct {
|
||||
Metadata plugin.GetMetadataResponse
|
||||
Metadata proto.GetMetadataResponse
|
||||
ExecuteResponse interface{}
|
||||
ExecuteError error
|
||||
}
|
||||
|
||||
func (p *PluginMock) GetMetadata(ctx context.Context, req *plugin.GetMetadataRequest) (*plugin.GetMetadataResponse, error) {
|
||||
func (p *PluginMock) GetMetadata(ctx context.Context, req *proto.GetMetadataRequest) (*proto.GetMetadataResponse, error) {
|
||||
return &p.Metadata, nil
|
||||
}
|
||||
|
||||
func (p *PluginMock) VerifySignature(ctx context.Context, req *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) {
|
||||
if resp, ok := p.ExecuteResponse.(*plugin.VerifySignatureResponse); ok {
|
||||
func (p *PluginMock) VerifySignature(ctx context.Context, req *proto.VerifySignatureRequest) (*proto.VerifySignatureResponse, error) {
|
||||
if resp, ok := p.ExecuteResponse.(*proto.VerifySignatureResponse); ok {
|
||||
return resp, nil
|
||||
}
|
||||
return nil, p.ExecuteError
|
||||
}
|
||||
|
||||
func (p *PluginMock) DescribeKey(ctx context.Context, req *plugin.DescribeKeyRequest) (*plugin.DescribeKeyResponse, error) {
|
||||
func (p *PluginMock) DescribeKey(ctx context.Context, req *proto.DescribeKeyRequest) (*proto.DescribeKeyResponse, error) {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (p *PluginMock) GenerateSignature(ctx context.Context, req *plugin.GenerateSignatureRequest) (*plugin.GenerateSignatureResponse, error) {
|
||||
func (p *PluginMock) GenerateSignature(ctx context.Context, req *proto.GenerateSignatureRequest) (*proto.GenerateSignatureResponse, error) {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (p *PluginMock) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) {
|
||||
func (p *PluginMock) GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error) {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
type PluginManager struct {
|
||||
PluginCapabilities []plugin.Capability
|
||||
PluginCapabilities []proto.Capability
|
||||
GetPluginError error
|
||||
PluginRunnerLoadError error
|
||||
PluginRunnerExecuteResponse interface{}
|
||||
|
@ -210,7 +206,7 @@ type PluginManager struct {
|
|||
|
||||
func (pm PluginManager) Get(ctx context.Context, name string) (plugin.Plugin, error) {
|
||||
return &PluginMock{
|
||||
Metadata: plugin.GetMetadataResponse{
|
||||
Metadata: proto.GetMetadataResponse{
|
||||
Name: "plugin-name",
|
||||
Description: "for mocking in unit tests",
|
||||
Version: "1.0.0",
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ocilayout
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"oras.land/oras-go/v2"
|
||||
"oras.land/oras-go/v2/content/oci"
|
||||
)
|
||||
|
||||
// Copy creates a temporary OCI layout for testing
|
||||
// and returns the path to the layout.
|
||||
func Copy(sourcePath, destPath, tag string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
srcStore, err := oci.NewFromFS(ctx, os.DirFS(sourcePath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// create a dest store for store the generated oci layout.
|
||||
destStore, err := oci.New(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// copy data
|
||||
_, err = oras.ExtendedCopy(ctx, srcStore, tag, destStore, "", oras.DefaultExtendedCopyOptions)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,64 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package ocilayout
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCopy(t *testing.T) {
|
||||
t.Run("empty oci layout", func(t *testing.T) {
|
||||
err := Copy("", "", "v2")
|
||||
if err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid target path permission", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
tempDir := t.TempDir()
|
||||
// change the permission of the tempDir to make it invalid
|
||||
if err := os.Chmod(tempDir, 0); err != nil {
|
||||
t.Fatalf("failed to change the permission of the tempDir: %v", err)
|
||||
}
|
||||
err := Copy("../../testdata/oci-layout", tempDir, "v2")
|
||||
if err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
|
||||
if err := os.Chmod(tempDir, 0755); err != nil {
|
||||
t.Fatalf("failed to change the permission of the tempDir: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("copy failed", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
err := Copy("../../testdata/oci-layout", tempDir, "v3")
|
||||
if err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("copy success", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
err := Copy("../../testdata/oci-layout", tempDir, "v2")
|
||||
if err != nil {
|
||||
t.Errorf("expected nil, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pkix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func FuzzParseDistinguishedName(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, name string) {
|
||||
_, _ = ParseDistinguishedName(name)
|
||||
})
|
||||
}
|
|
@ -26,10 +26,11 @@ func ParseDistinguishedName(name string) (map[string]string, error) {
|
|||
return nil, fmt.Errorf("unsupported distinguished name (DN) %q: notation does not support x509.subject identities containing \"=#\"", name)
|
||||
}
|
||||
|
||||
mandatoryFields := []string{"C", "ST", "O"}
|
||||
attrKeyValue := make(map[string]string)
|
||||
dn, err := ldapv3.ParseDN(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing distinguished name (DN) %q failed with err: %v. A valid DN must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name, err)
|
||||
return nil, fmt.Errorf("parsing distinguished name (DN) %q failed with err: %v. A valid DN must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name, err)
|
||||
}
|
||||
|
||||
for _, rdn := range dn.RDNs {
|
||||
|
@ -38,10 +39,6 @@ func ParseDistinguishedName(name string) (map[string]string, error) {
|
|||
return nil, fmt.Errorf("distinguished name (DN) %q has multi-valued RDN attributes, remove multi-valued RDN attributes as they are not supported", name)
|
||||
}
|
||||
for _, attribute := range rdn.Attributes {
|
||||
// stateOrProvince name 'S' is an alias for 'ST'
|
||||
if attribute.Type == "S" {
|
||||
attribute.Type = "ST"
|
||||
}
|
||||
if attrKeyValue[attribute.Type] == "" {
|
||||
attrKeyValue[attribute.Type] = attribute.Value
|
||||
} else {
|
||||
|
@ -51,13 +48,11 @@ func ParseDistinguishedName(name string) (map[string]string, error) {
|
|||
}
|
||||
|
||||
// Verify mandatory fields are present
|
||||
mandatoryFields := []string{"C", "ST", "O"}
|
||||
for _, field := range mandatoryFields {
|
||||
if attrKeyValue[field] == "" {
|
||||
return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum", name, field)
|
||||
return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum", name, field)
|
||||
}
|
||||
}
|
||||
|
||||
// No errors
|
||||
return attrKeyValue, nil
|
||||
}
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pkix
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseDistinguishedName(t *testing.T) {
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid DN",
|
||||
input: "C=US,ST=California,O=Notary Project",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid DN with State alias",
|
||||
input: "C=US,S=California,O=Notary Project",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid DN",
|
||||
input: "C=US,ST=California",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid DN without State",
|
||||
input: "C=US,O=Notary Project",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid DN without State",
|
||||
input: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "duplicate RDN attribute",
|
||||
input: "C=US,ST=California,O=Notary Project,S=California",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unsupported DN =#",
|
||||
input: "C=US,ST=California,O=Notary Project=#",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "multi-valued RDN attributes",
|
||||
input: "OU=Sales+CN=J. Smith,DC=example,DC=net",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseDistinguishedName(tt.input)
|
||||
if tt.wantErr != (err != nil) {
|
||||
t.Errorf("ParseDistinguishedName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSubsetDN(t *testing.T) {
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
dn1 map[string]string
|
||||
dn2 map[string]string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "subset DN",
|
||||
dn1: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project",
|
||||
},
|
||||
dn2: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project",
|
||||
"L": "Los Angeles",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not subset DN",
|
||||
dn1: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project",
|
||||
},
|
||||
dn2: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project 2",
|
||||
"L": "Los Angeles",
|
||||
"CN": "Notary",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not subset DN 2",
|
||||
dn1: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project",
|
||||
"CN": "Notary",
|
||||
},
|
||||
dn2: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project",
|
||||
"L": "Los Angeles",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsSubsetDN(tt.dn1, tt.dn2); got != tt.want {
|
||||
t.Errorf("IsSubsetDN() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package semver provides functions related to semanic version.
|
||||
// This package is based on "golang.org/x/mod/semver"
|
||||
package semver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// semVerRegEx is taken from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||
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-]+)*))?$`)
|
||||
|
||||
// IsValid returns true if version is a valid semantic version
|
||||
func IsValid(version string) bool {
|
||||
return semVerRegEx.MatchString(version)
|
||||
}
|
||||
|
||||
// ComparePluginVersion validates and compares two plugin semantic versions.
|
||||
//
|
||||
// The result will be 0 if v == w, -1 if v < w, or +1 if v > w.
|
||||
func ComparePluginVersion(v, w string) (int, error) {
|
||||
// sanity check
|
||||
if !IsValid(v) {
|
||||
return 0, fmt.Errorf("%s is not a valid semantic version", v)
|
||||
}
|
||||
if !IsValid(w) {
|
||||
return 0, fmt.Errorf("%s is not a valid semantic version", w)
|
||||
}
|
||||
|
||||
// golang.org/x/mod/semver requires semantic version strings must begin
|
||||
// with a leading "v". Adding prefix "v" to the inputs.
|
||||
// Reference: https://pkg.go.dev/golang.org/x/mod/semver#pkg-overview
|
||||
return semver.Compare("v"+v, "v"+w), nil
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package semver
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestComparePluginVersion(t *testing.T) {
|
||||
t.Run("compare with lower version", func(t *testing.T) {
|
||||
comp, err := ComparePluginVersion("1.0.0", "1.0.1")
|
||||
if err != nil || comp >= 0 {
|
||||
t.Fatal("expected nil err and negative comp")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("compare with equal version", func(t *testing.T) {
|
||||
comp, err := ComparePluginVersion("1.0.1", "1.0.1")
|
||||
if err != nil || comp != 0 {
|
||||
t.Fatal("expected nil err and comp equal to 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("failed due to invalid semantic version", func(t *testing.T) {
|
||||
expectedErrMsg := "v1.0.0 is not a valid semantic version"
|
||||
_, err := ComparePluginVersion("v1.0.0", "1.0.1")
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected err %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package log provides logging functionality to notation.
|
||||
// Users who want to enable logging option in notation should implement the
|
||||
// log.Logger interface and include it in context by calling log.WithLogger.
|
||||
// 3rd party loggers that implement log.Logger: github.com/uber-go/zap.SugaredLogger
|
||||
// and github.com/sirupsen/logrus.Logger.
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWithLoggerAndGetLogger(t *testing.T) {
|
||||
tl := &discardLogger{}
|
||||
ctx := WithLogger(context.Background(), tl)
|
||||
|
||||
if got := GetLogger(ctx); got != tl {
|
||||
t.Errorf("GetLogger() = %v, want %v", got, tl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLoggerWithNoLogger(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
if got := GetLogger(ctx); got != Discard {
|
||||
t.Errorf("GetLogger() = %v, want Discard", got)
|
||||
}
|
||||
}
|
341
notation.go
341
notation.go
|
@ -12,46 +12,36 @@
|
|||
// limitations under the License.
|
||||
|
||||
// Package notation provides signer and verifier for notation Sign
|
||||
// and Verification. It supports both OCI artifact and arbitrary blob.
|
||||
// and Verification.
|
||||
package notation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
orasRegistry "oras.land/oras-go/v2/registry"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/revocation"
|
||||
"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/notaryproject/tspclient-go"
|
||||
"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")
|
||||
|
||||
var reservedAnnotationPrefixes = [...]string{"io.cncf.notary"}
|
||||
|
||||
// SignerSignOptions contains parameters for [Signer] and [BlobSigner].
|
||||
// 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
|
||||
|
||||
|
@ -64,60 +54,17 @@ type SignerSignOptions struct {
|
|||
|
||||
// SigningAgent sets the signing agent name
|
||||
SigningAgent string
|
||||
|
||||
// Timestamper denotes the timestamper for RFC 3161 timestamping
|
||||
Timestamper tspclient.Timestamper
|
||||
|
||||
// TSARootCAs is the cert pool holding caller's TSA trust anchor
|
||||
TSARootCAs *x509.CertPool
|
||||
|
||||
// TSARevocationValidator is used for validating revocation status of
|
||||
// timestamping certificate chain with context during signing.
|
||||
// When present, only used when timestamping is performed.
|
||||
TSARevocationValidator revocation.Validator
|
||||
}
|
||||
|
||||
// Signer is a generic interface for signing an OCI artifact.
|
||||
// Signer is a generic interface for signing an artifact.
|
||||
// The interface allows signing with local or remote keys,
|
||||
// and packing in various signature formats.
|
||||
type Signer interface {
|
||||
// Sign signs the OCI artifact described by its descriptor,
|
||||
// Sign signs the 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
|
||||
|
||||
// UserMetadata contains key-value pairs that are added to the signature
|
||||
// payload
|
||||
UserMetadata map[string]string
|
||||
}
|
||||
|
||||
// BlobDescriptorGenerator creates descriptor using the digest Algorithm.
|
||||
// Below is the example of minimal descriptor, it must contain mediatype,
|
||||
// digest and size of the artifact.
|
||||
//
|
||||
// {
|
||||
// "mediaType": "application/octet-stream",
|
||||
// "digest": "sha256:2f3a23b6373afb134ddcd864be8e037e34a662d090d33ee849471ff73c873345",
|
||||
// "size": 1024
|
||||
// }
|
||||
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 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
|
||||
|
@ -125,7 +72,7 @@ type signerAnnotation interface {
|
|||
PluginAnnotations() map[string]string
|
||||
}
|
||||
|
||||
// SignOptions contains parameters for [notation.Sign].
|
||||
// SignOptions contains parameters for notation.Sign.
|
||||
type SignOptions struct {
|
||||
SignerSignOptions
|
||||
|
||||
|
@ -138,32 +85,21 @@ type SignOptions struct {
|
|||
UserMetadata map[string]string
|
||||
}
|
||||
|
||||
// Sign signs the OCI artifact and push the signature to the Repository.
|
||||
// The descriptor of the sign content is returned upon successful signing.
|
||||
//
|
||||
// Deprecated: use [SignOCI] instead.
|
||||
// Sign signs the artifact and push the signature to the Repository.
|
||||
// The descriptor of the sign content is returned upon sucessful signing.
|
||||
func Sign(ctx context.Context, signer Signer, repo registry.Repository, signOpts SignOptions) (ocispec.Descriptor, error) {
|
||||
artifactMenifestDesc, _, err := SignOCI(ctx, signer, repo, signOpts)
|
||||
return artifactMenifestDesc, err
|
||||
}
|
||||
|
||||
// SignOCI signs the OCI artifact and push the signature to the Repository.
|
||||
//
|
||||
// Both artifact and signature manifest descriptors are returned upon successful
|
||||
// signing.
|
||||
//
|
||||
// Note: If the error type is [remote.ReferrersError] and
|
||||
// referrerError.IsReferrersIndexDelete() returns true, the signature is
|
||||
// successfully pushed to the repository, but the referrers index deletion
|
||||
// failed. In this case, the artifact and signature manifest descriptors are
|
||||
// returned with the error.
|
||||
func SignOCI(ctx context.Context, signer Signer, repo registry.Repository, signOpts SignOptions) (artifactManifestDesc, sigManifestDesc ocispec.Descriptor, err error) {
|
||||
// sanity check
|
||||
if err := validateSignArguments(signer, signOpts.SignerSignOptions); err != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
|
||||
if signer == nil {
|
||||
return ocispec.Descriptor{}, errors.New("signer cannot be nil")
|
||||
}
|
||||
if repo == nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, errors.New("repo cannot be 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)
|
||||
|
@ -172,114 +108,73 @@ func SignOCI(ctx context.Context, signer Signer, repo registry.Repository, signO
|
|||
// artifactRef is a valid full reference
|
||||
artifactRef = ref.Reference
|
||||
}
|
||||
artifactManifestDesc, err = repo.Resolve(ctx, artifactRef)
|
||||
targetDesc, err := repo.Resolve(ctx, artifactRef)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, fmt.Errorf("failed to resolve reference: %w", err)
|
||||
return ocispec.Descriptor{}, fmt.Errorf("failed to resolve reference: %w", err)
|
||||
}
|
||||
|
||||
// artifactRef is a tag or a digest, if it's a digest it has to match
|
||||
// the resolved digest
|
||||
if artifactRef != artifactManifestDesc.Digest.String() {
|
||||
if artifactRef != targetDesc.Digest.String() {
|
||||
if _, err := digest.Parse(artifactRef); err == nil {
|
||||
// artifactRef is a digest, but does not match the resolved digest
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, fmt.Errorf("user input digest %s does not match the resolved digest %s", artifactRef, artifactManifestDesc.Digest.String())
|
||||
return ocispec.Descriptor{}, fmt.Errorf("user input digest %s does not match the resolved digest %s", artifactRef, targetDesc.Digest.String())
|
||||
}
|
||||
|
||||
// artifactRef is a tag
|
||||
logger.Warnf("Always sign the artifact using digest(`@sha256:...`) rather than a tag(`:%s`) because tags are mutable and a tag reference can point to a different artifact than the one signed", artifactRef)
|
||||
logger.Infof("Resolved artifact tag `%s` to digest `%v` before signing", artifactRef, artifactManifestDesc.Digest)
|
||||
logger.Infof("Resolved artifact tag `%s` to digest `%s` before signing", artifactRef, targetDesc.Digest.String())
|
||||
}
|
||||
descToSign, err := addUserMetadataToDescriptor(ctx, artifactManifestDesc, signOpts.UserMetadata)
|
||||
descToSign, err := addUserMetadataToDescriptor(ctx, targetDesc, signOpts.UserMetadata)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
sig, signerInfo, err := signer.Sign(ctx, descToSign, signOpts.SignerSignOptions)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
var pluginAnnotations map[string]string
|
||||
if signerAnts, ok := signer.(signerAnnotation); ok {
|
||||
pluginAnnotations = signerAnts.PluginAnnotations()
|
||||
}
|
||||
|
||||
logger.Debug("Generating annotation")
|
||||
annotations, err := generateAnnotations(signerInfo, pluginAnnotations)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
logger.Debugf("Generated annotations: %+v", annotations)
|
||||
logger.Debugf("Pushing signature of artifact descriptor: %+v, signature media type: %v", artifactManifestDesc, signOpts.SignatureMediaType)
|
||||
_, sigManifestDesc, err = repo.PushSignature(ctx, signOpts.SignatureMediaType, sig, artifactManifestDesc, annotations)
|
||||
logger.Debugf("Pushing signature of artifact descriptor: %+v, signature media type: %v", targetDesc, signOpts.SignatureMediaType)
|
||||
_, _, err = repo.PushSignature(ctx, signOpts.SignatureMediaType, sig, targetDesc, annotations)
|
||||
if err != nil {
|
||||
var referrerError *remote.ReferrersError
|
||||
if errors.As(err, &referrerError) && referrerError.IsReferrersIndexDelete() {
|
||||
// return the descriptors for referrersIndexDelete error as
|
||||
// the signature is successfully pushed to the repository
|
||||
return artifactManifestDesc, sigManifestDesc, err
|
||||
}
|
||||
logger.Error("Failed to push the signature")
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, ErrorPushSignatureFailed{Msg: err.Error()}
|
||||
}
|
||||
return artifactManifestDesc, sigManifestDesc, nil
|
||||
}
|
||||
|
||||
// SignBlob signs the arbitrary data from blobReader and returns
|
||||
// the signature and SignerInfo.
|
||||
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 := validateContentMediaType(signBlobOpts.ContentMediaType); err != nil {
|
||||
return nil, nil, err
|
||||
return ocispec.Descriptor{}, ErrorPushSignatureFailed{Msg: err.Error()}
|
||||
}
|
||||
|
||||
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 err := validateSigMediaType(signOpts.SignatureMediaType); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return targetDesc, nil
|
||||
}
|
||||
|
||||
func addUserMetadataToDescriptor(ctx context.Context, desc ocispec.Descriptor, userMetadata map[string]string) (ocispec.Descriptor, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
|
||||
if desc.Annotations == nil && len(userMetadata) > 0 {
|
||||
desc.Annotations = map[string]string{}
|
||||
}
|
||||
|
||||
for k, v := range userMetadata {
|
||||
logger.Debugf("Adding metadata %v=%v to annotations", k, v)
|
||||
|
||||
for _, reservedPrefix := range reservedAnnotationPrefixes {
|
||||
if strings.HasPrefix(k, reservedPrefix) {
|
||||
return desc, fmt.Errorf("error adding user metadata: metadata key %v has reserved prefix %v", k, reservedPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := desc.Annotations[k]; ok {
|
||||
return desc, fmt.Errorf("error adding user metadata: metadata key %v is already present in the target artifact", k)
|
||||
}
|
||||
|
||||
desc.Annotations[k] = v
|
||||
}
|
||||
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
|
@ -298,7 +193,7 @@ type ValidationResult struct {
|
|||
Error error
|
||||
}
|
||||
|
||||
// VerificationOutcome encapsulates a signature envelope blob, its content,
|
||||
// VerificationOutcome encapsulates a signature blob's descriptor, its content,
|
||||
// the verification level and results for each verification type that was
|
||||
// performed.
|
||||
type VerificationOutcome struct {
|
||||
|
@ -321,7 +216,6 @@ type VerificationOutcome struct {
|
|||
Error error
|
||||
}
|
||||
|
||||
// UserMetadata returns the user metadata from the signature envelope.
|
||||
func (outcome *VerificationOutcome) UserMetadata() (map[string]string, error) {
|
||||
if outcome.EnvelopeContent == nil {
|
||||
return nil, errors.New("unable to find envelope content for verification outcome")
|
||||
|
@ -332,21 +226,22 @@ func (outcome *VerificationOutcome) UserMetadata() (map[string]string, error) {
|
|||
if err != nil {
|
||||
return nil, errors.New("failed to unmarshal the payload content in the signature blob to envelope.Payload")
|
||||
}
|
||||
|
||||
if payload.TargetArtifact.Annotations == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
return payload.TargetArtifact.Annotations, nil
|
||||
}
|
||||
|
||||
// VerifierVerifyOptions contains parameters for [Verifier.Verify] used for
|
||||
// verifying OCI artifact.
|
||||
// VerifierVerifyOptions contains parameters for Verifier.Verify.
|
||||
type VerifierVerifyOptions struct {
|
||||
// ArtifactReference is the reference of the artifact that is being
|
||||
// ArtifactReference is the reference of the artifact that is been
|
||||
// verified against to. It must be a full reference.
|
||||
ArtifactReference string
|
||||
|
||||
// SignatureMediaType is the envelope type of the signature.
|
||||
// Currently only `application/jose+json` and `application/cose` are
|
||||
// Currently both `application/jose+json` and `application/cose` are
|
||||
// supported.
|
||||
SignatureMediaType string
|
||||
|
||||
|
@ -358,51 +253,24 @@ type VerifierVerifyOptions struct {
|
|||
UserMetadata map[string]string
|
||||
}
|
||||
|
||||
// Verifier is a generic interface for verifying an OCI artifact.
|
||||
// Verifier is a generic interface for verifying an artifact.
|
||||
type Verifier interface {
|
||||
// Verify verifies the `signature` associated with the target OCI artifact
|
||||
// with manifest descriptor `desc`, and returns the outcome upon
|
||||
// Verify verifies the signature blob `signature` against the target OCI
|
||||
// artifact with manifest descriptor `desc`, and returns the outcome upon
|
||||
// successful verification.
|
||||
// If nil signature is present and the verification level is not 'skip',
|
||||
// an error will be returned.
|
||||
Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifierVerifyOptions) (*VerificationOutcome, error)
|
||||
}
|
||||
|
||||
// BlobVerifierVerifyOptions contains parameters for [BlobVerifier.Verify].
|
||||
type BlobVerifierVerifyOptions struct {
|
||||
// SignatureMediaType is the envelope type of the signature.
|
||||
// Currently only `application/jose+json` and `application/cose` are
|
||||
// supported.
|
||||
SignatureMediaType string
|
||||
|
||||
// PluginConfig is a map of plugin configs.
|
||||
PluginConfig map[string]string
|
||||
|
||||
// UserMetadata contains key-value pairs that must be present in the
|
||||
// signature.
|
||||
UserMetadata map[string]string
|
||||
|
||||
// TrustPolicyName is the name of trust policy picked by caller.
|
||||
// If empty, the global trust policy will be applied.
|
||||
TrustPolicyName string
|
||||
}
|
||||
|
||||
// BlobVerifier is a generic interface for verifying a blob.
|
||||
type BlobVerifier interface {
|
||||
// VerifyBlob verifies the `signature` against the target blob using the
|
||||
// descriptor returned by descGenFunc parameter and
|
||||
// returns the outcome upon successful verification.
|
||||
VerifyBlob(ctx context.Context, descGenFunc BlobDescriptorGenerator, signature []byte, opts BlobVerifierVerifyOptions) (*VerificationOutcome, error)
|
||||
}
|
||||
|
||||
type verifySkipper interface {
|
||||
// SkipVerify validates whether the verification level is skip.
|
||||
SkipVerify(ctx context.Context, opts VerifierVerifyOptions) (bool, *trustpolicy.VerificationLevel, error)
|
||||
}
|
||||
|
||||
// VerifyOptions contains parameters for [notation.Verify].
|
||||
// VerifyOptions contains parameters for notation.Verify.
|
||||
type VerifyOptions struct {
|
||||
// ArtifactReference is the reference of the artifact that is being
|
||||
// ArtifactReference is the reference of the artifact that is been
|
||||
// verified against to.
|
||||
ArtifactReference string
|
||||
|
||||
|
@ -419,51 +287,8 @@ type VerifyOptions struct {
|
|||
UserMetadata map[string]string
|
||||
}
|
||||
|
||||
// VerifyBlobOptions contains parameters for [notation.VerifyBlob].
|
||||
type VerifyBlobOptions struct {
|
||||
BlobVerifierVerifyOptions
|
||||
|
||||
// ContentMediaType is the media-type type of the content being verified.
|
||||
ContentMediaType string
|
||||
}
|
||||
|
||||
// VerifyBlob performs signature verification for a blob using notation supported
|
||||
// verification types (like integrity, authenticity, etc.) and returns the
|
||||
// successful signature verification outcome. The blob is read using blobReader,
|
||||
// and upon successful verification, it returns the descriptor of the blob.
|
||||
// For more details on signature verification, see
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification
|
||||
func VerifyBlob(ctx context.Context, blobVerifier BlobVerifier, blobReader io.Reader, signature []byte, verifyBlobOpts VerifyBlobOptions) (ocispec.Descriptor, *VerificationOutcome, error) {
|
||||
if blobVerifier == nil {
|
||||
return ocispec.Descriptor{}, nil, errors.New("blobVerifier cannot be nil")
|
||||
}
|
||||
if blobReader == nil {
|
||||
return ocispec.Descriptor{}, nil, errors.New("blobReader cannot be nil")
|
||||
}
|
||||
if len(signature) == 0 {
|
||||
return ocispec.Descriptor{}, nil, errors.New("signature cannot be nil or empty")
|
||||
}
|
||||
if err := validateContentMediaType(verifyBlobOpts.ContentMediaType); err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
if err := validateSigMediaType(verifyBlobOpts.SignatureMediaType); err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
getDescFunc := getDescriptorFunc(ctx, blobReader, verifyBlobOpts.ContentMediaType, verifyBlobOpts.UserMetadata)
|
||||
vo, err := blobVerifier.VerifyBlob(ctx, getDescFunc, signature, verifyBlobOpts.BlobVerifierVerifyOptions)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
|
||||
var desc ocispec.Descriptor
|
||||
if err = json.Unmarshal(vo.EnvelopeContent.Payload.Content, &desc); err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
return desc, vo, nil
|
||||
}
|
||||
|
||||
// Verify performs signature verification on each of the notation supported
|
||||
// verification types (like integrity, authenticity, etc.) and returns the
|
||||
// verification types (like integrity, authenticity, etc.) and return the
|
||||
// successful signature verification outcome.
|
||||
// For more details on signature verification, see
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification
|
||||
|
@ -487,6 +312,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
|
|||
PluginConfig: verifyOpts.PluginConfig,
|
||||
UserMetadata: verifyOpts.UserMetadata,
|
||||
}
|
||||
|
||||
if skipChecker, ok := verifier.(verifySkipper); ok {
|
||||
logger.Info("Checking whether signature verification should be skipped or not")
|
||||
skip, verificationLevel, err := skipChecker.SkipVerify(ctx, opts)
|
||||
|
@ -494,10 +320,10 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
|
|||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
if skip {
|
||||
logger.Infoln("Signature verification skipped for", verifyOpts.ArtifactReference)
|
||||
logger.Infoln("Verification skipped for", verifyOpts.ArtifactReference)
|
||||
return ocispec.Descriptor{}, []*VerificationOutcome{{VerificationLevel: verificationLevel}}, nil
|
||||
}
|
||||
logger.Info("Check over. The signature verification level is not set to 'skip' in the trust policy.")
|
||||
logger.Info("Check over. Trust policy is not configured to skip signature verification")
|
||||
}
|
||||
|
||||
// get artifact descriptor
|
||||
|
@ -515,18 +341,18 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
|
|||
}
|
||||
if ref.ValidateReferenceAsDigest() != nil {
|
||||
// artifactRef is not a digest reference
|
||||
logger.Infof("Resolved artifact tag `%s` to digest `%v` before verification", ref.Reference, artifactDescriptor.Digest)
|
||||
logger.Infof("Resolved artifact tag `%s` to digest `%s` before verification", ref.Reference, artifactDescriptor.Digest.String())
|
||||
logger.Warn("The resolved digest may not point to the same signed artifact, since tags are mutable")
|
||||
} else if ref.Reference != artifactDescriptor.Digest.String() {
|
||||
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("user input digest %s does not match the resolved digest %s", ref.Reference, artifactDescriptor.Digest.String())}
|
||||
}
|
||||
|
||||
var verificationSucceeded bool
|
||||
var verificationOutcomes []*VerificationOutcome
|
||||
var verificationFailedErrorArray = []error{ErrorVerificationFailed{}}
|
||||
errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("signature evaluation stopped. The configured limit of %d signatures to verify per artifact exceeded", verifyOpts.MaxSignatureAttempts)}
|
||||
numOfSignatureProcessed := 0
|
||||
|
||||
var verificationFailedErr error = ErrorVerificationFailed{}
|
||||
|
||||
// get signature manifests
|
||||
logger.Debug("Fetching signature manifests")
|
||||
err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error {
|
||||
|
@ -554,26 +380,29 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
|
|||
logger.Error("Got nil outcome. Expecting non-nil outcome on verification failure")
|
||||
return err
|
||||
}
|
||||
outcome.Error = fmt.Errorf("failed to verify signature with digest %v, %w", sigManifestDesc.Digest, outcome.Error)
|
||||
verificationFailedErrorArray = append(verificationFailedErrorArray, outcome.Error)
|
||||
|
||||
if _, ok := outcome.Error.(ErrorUserMetadataVerificationFailed); ok {
|
||||
verificationFailedErr = outcome.Error
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
// at this point, the signature is verified successfully
|
||||
verificationSucceeded = true
|
||||
|
||||
// on success, verificationOutcomes only contains the
|
||||
// succeeded outcome
|
||||
verificationOutcomes = []*VerificationOutcome{outcome}
|
||||
// at this point, the signature is verified successfully. Add
|
||||
// it to the verificationOutcomes.
|
||||
verificationOutcomes = append(verificationOutcomes, outcome)
|
||||
logger.Debugf("Signature verification succeeded for artifact %v with signature digest %v", artifactDescriptor.Digest, sigManifestDesc.Digest)
|
||||
|
||||
// early break on success
|
||||
return errDoneVerification
|
||||
}
|
||||
|
||||
if numOfSignatureProcessed >= verifyOpts.MaxSignatureAttempts {
|
||||
return errExceededMaxVerificationLimit
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil && !errors.Is(err, errDoneVerification) {
|
||||
if errors.Is(err, errExceededMaxVerificationLimit) {
|
||||
return ocispec.Descriptor{}, verificationOutcomes, err
|
||||
|
@ -587,9 +416,9 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
|
|||
}
|
||||
|
||||
// Verification Failed
|
||||
if !verificationSucceeded {
|
||||
if len(verificationOutcomes) == 0 {
|
||||
logger.Debugf("Signature verification failed for all the signatures associated with artifact %v", artifactDescriptor.Digest)
|
||||
return ocispec.Descriptor{}, verificationOutcomes, errors.Join(verificationFailedErrorArray...)
|
||||
return ocispec.Descriptor{}, verificationOutcomes, verificationFailedErr
|
||||
}
|
||||
|
||||
// Verification Succeeded
|
||||
|
@ -621,35 +450,3 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
func validateContentMediaType(contentMediaType string) error {
|
||||
if contentMediaType != "" {
|
||||
if _, _, err := mime.ParseMediaType(contentMediaType); err != nil {
|
||||
return fmt.Errorf("invalid content media-type %q: %v", contentMediaType, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSigMediaType(sigMediaType string) error {
|
||||
if !(sigMediaType == jws.MediaTypeEnvelope || sigMediaType == cose.MediaTypeEnvelope) {
|
||||
return fmt.Errorf("invalid signature media-type %q", sigMediaType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
562
notation_test.go
562
notation_test.go
|
@ -15,29 +15,20 @@ package notation
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"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/internal/mock"
|
||||
"github.com/notaryproject/notation-go/internal/mock/ocilayout"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -56,7 +47,6 @@ 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
|
||||
|
||||
|
@ -68,91 +58,11 @@ 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 {
|
||||
|
@ -160,71 +70,6 @@ func TestSignSuccessWithUserMetadata(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSignWithDanglingReferrersIndex(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
repo.PushSignatureError = &remote.ReferrersError{
|
||||
Op: "DeleteReferrersIndex",
|
||||
Err: errors.New("error"),
|
||||
}
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = mock.SampleArtifactUri
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("no error occurred, expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithNilRepo(t *testing.T) {
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = mock.SampleArtifactUri
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, nil, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("no error occurred, expected error: repo cannot be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignResolveFailed(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
repo.ResolveError = errors.New("resolve error")
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = mock.SampleArtifactUri
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("no error occurred, expected resolve error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignArtifactRefIsTag(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = "registry.acme-rockets.io/software/net-monitor:v1"
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithPushSignatureError(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
repo.PushSignatureError = errors.New("error")
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = mock.SampleArtifactUri
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("no error occurred, expected error: failed to delete dangling referrers index")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithInvalidExpiry(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
testCases := []struct {
|
||||
|
@ -258,14 +103,7 @@ func TestSignWithInvalidUserMetadata(t *testing.T) {
|
|||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(b *testing.T) {
|
||||
opts := SignOptions{
|
||||
UserMetadata: tc.metadata,
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: jws.MediaTypeEnvelope,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, SignOptions{UserMetadata: tc.metadata})
|
||||
if err == nil {
|
||||
b.Fatalf("Expected error but not found")
|
||||
}
|
||||
|
@ -273,41 +111,10 @@ func TestSignWithInvalidUserMetadata(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSignOptsMissingSignatureMediaType(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
opts := SignOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: "",
|
||||
},
|
||||
ArtifactReference: mock.SampleArtifactUri,
|
||||
}
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error but not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignOptsUnknownMediaType(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
opts := SignOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: "unknown",
|
||||
},
|
||||
ArtifactReference: mock.SampleArtifactUri,
|
||||
}
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error but not found")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRegistryResolveError(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
|
||||
errorMessage := "network error"
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
@ -323,9 +130,9 @@ func TestRegistryResolveError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVerifyEmptyReference(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
|
||||
errorMessage := "reference is missing digest or tag"
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
@ -339,11 +146,11 @@ func TestVerifyEmptyReference(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVerifyTagReferenceFailed(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
|
||||
errorMessage := "invalid reference: invalid repository \"UPPERCASE/test\""
|
||||
errorMessage := "invalid reference: invalid repository"
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
||||
// mock the repository
|
||||
|
@ -355,10 +162,10 @@ func TestVerifyTagReferenceFailed(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVerifyDigestNotMatchResolve(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
repo.MissMatchDigest = true
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
|
||||
errorMessage := fmt.Sprintf("user input digest %s does not match the resolved digest %s", mock.SampleDigest, mock.ZeroDigest)
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
@ -375,14 +182,11 @@ func TestSignDigestNotMatchResolve(t *testing.T) {
|
|||
repo := mock.NewRepository()
|
||||
repo.MissMatchDigest = true
|
||||
signOpts := SignOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: jws.MediaTypeEnvelope,
|
||||
},
|
||||
ArtifactReference: mock.SampleArtifactUri,
|
||||
}
|
||||
|
||||
errorMessage := fmt.Sprintf("user input digest %s does not match the resolved digest %s", mock.SampleDigest, mock.ZeroDigest)
|
||||
expectedErr := errors.New(errorMessage)
|
||||
expectedErr := fmt.Errorf(errorMessage)
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, signOpts)
|
||||
if err == nil || err.Error() != errorMessage {
|
||||
|
@ -391,9 +195,9 @@ func TestSignDigestNotMatchResolve(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSkippedSignatureVerification(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelSkip, false}
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelSkip}
|
||||
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, outcomes, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
@ -404,9 +208,9 @@ func TestSkippedSignatureVerification(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRegistryNoSignatureManifests(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
errorMessage := fmt.Sprintf("no signature is associated with %q, make sure the artifact was signed successfully", mock.SampleArtifactUri)
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
||||
|
@ -421,9 +225,9 @@ func TestRegistryNoSignatureManifests(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRegistryFetchSignatureBlobError(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
errorMessage := fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the Repository, error : network error", mock.SampleDigest, mock.SampleArtifactUri)
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
||||
|
@ -438,37 +242,23 @@ func TestRegistryFetchSignatureBlobError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVerifyValid(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifySkip(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, true}
|
||||
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, but got: %v", err)
|
||||
t.Fatalf("SignaureMediaTypeMismatch expected: %v got: %v", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxSignatureAttemptsMissing(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
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
|
||||
|
@ -481,11 +271,10 @@ func TestMaxSignatureAttemptsMissing(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestExceededMaxSignatureAttempts(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
repo.ExceededNumOfSignatures = true
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict, false}
|
||||
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict}
|
||||
expectedErr := ErrorVerificationFailed{Msg: fmt.Sprintf("signature evaluation stopped. The configured limit of %d signatures to verify per artifact exceeded", 1)}
|
||||
|
||||
// mock the repository
|
||||
|
@ -498,116 +287,30 @@ func TestExceededMaxSignatureAttempts(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVerifyFailed(t *testing.T) {
|
||||
t.Run("verification error", func(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict, false}
|
||||
expectedErr := ErrorVerificationFailed{}
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict}
|
||||
expectedErr := ErrorVerificationFailed{}
|
||||
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
// mock the repository
|
||||
opts := VerifyOptions{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)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verifier is nil", func(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
expectedErr := errors.New("verifier cannot be nil")
|
||||
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), nil, repo, opts)
|
||||
|
||||
if err == nil || err.Error() != expectedErr.Error() {
|
||||
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("repo is nil", func(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
expectedErr := errors.New("repo cannot be nil")
|
||||
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, nil, opts)
|
||||
|
||||
if err == nil || err.Error() != expectedErr.Error() {
|
||||
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVerifyBlobError(t *testing.T) {
|
||||
reader := strings.NewReader("some content")
|
||||
sig := []byte("signature")
|
||||
testCases := []struct {
|
||||
name string
|
||||
verifier BlobVerifier
|
||||
sig []byte
|
||||
rdr io.Reader
|
||||
ctMType string
|
||||
sigMType string
|
||||
errMsg string
|
||||
}{
|
||||
{"nilVerifier", nil, sig, reader, "video/mp4", jws.MediaTypeEnvelope, "blobVerifier cannot be nil"},
|
||||
{"verifierError", &dummyVerifier{FailVerify: true}, sig, reader, "video/mp4", jws.MediaTypeEnvelope, "failed verify"},
|
||||
{"nilSignature", &dummyVerifier{}, nil, reader, "video/mp4", jws.MediaTypeEnvelope, "signature cannot be nil or empty"},
|
||||
{"emptySignature", &dummyVerifier{}, []byte{}, reader, "video/mp4", jws.MediaTypeEnvelope, "signature cannot be nil or empty"},
|
||||
{"nilReader", &dummyVerifier{}, sig, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"},
|
||||
{"invalidContentType", &dummyVerifier{}, sig, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type \"video/mp4/zoping\": mime: unexpected content after media subtype"},
|
||||
{"invalidSigType", &dummyVerifier{}, sig, reader, "video/mp4", "hola!", "invalid signature media-type \"hola!\""},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
opts := VerifyBlobOptions{
|
||||
BlobVerifierVerifyOptions: BlobVerifierVerifyOptions{
|
||||
SignatureMediaType: tc.sigMType,
|
||||
UserMetadata: nil,
|
||||
TrustPolicyName: "",
|
||||
},
|
||||
ContentMediaType: tc.ctMType,
|
||||
}
|
||||
|
||||
_, _, err := VerifyBlob(context.Background(), tc.verifier, tc.rdr, tc.sig, 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())
|
||||
}
|
||||
})
|
||||
if err == nil || !errors.Is(err, expectedErr) {
|
||||
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyBlobValid(t *testing.T) {
|
||||
opts := VerifyBlobOptions{
|
||||
BlobVerifierVerifyOptions: BlobVerifierVerifyOptions{
|
||||
SignatureMediaType: jws.MediaTypeEnvelope,
|
||||
UserMetadata: nil,
|
||||
TrustPolicyName: "",
|
||||
},
|
||||
}
|
||||
|
||||
_, _, err := VerifyBlob(context.Background(), &dummyVerifier{}, strings.NewReader("some content"), []byte("signature"), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func dummyPolicyDocument() (policyDoc trustpolicy.OCIDocument) {
|
||||
policyDoc = trustpolicy.OCIDocument{
|
||||
func dummyPolicyDocument() (policyDoc trustpolicy.Document) {
|
||||
policyDoc = trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{dummyPolicyStatement()},
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{dummyPolicyStatement()},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func dummyPolicyStatement() (policyStatement trustpolicy.OCITrustPolicy) {
|
||||
policyStatement = trustpolicy.OCITrustPolicy{
|
||||
func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
|
||||
policyStatement = trustpolicy.TrustPolicy{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
|
@ -617,28 +320,9 @@ func dummyPolicyStatement() (policyStatement trustpolicy.OCITrustPolicy) {
|
|||
return
|
||||
}
|
||||
|
||||
type dummySigner struct {
|
||||
fail bool
|
||||
}
|
||||
|
||||
func (s *dummySigner) Sign(_ context.Context, _ ocispec.Descriptor, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
return []byte("ABC"), &signature.SignerInfo{
|
||||
SignedAttributes: signature.SignedAttributes{
|
||||
SigningTime: time.Now(),
|
||||
},
|
||||
}, 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
|
||||
}
|
||||
type dummySigner struct{}
|
||||
|
||||
func (s *dummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
return []byte("ABC"), &signature.SignerInfo{
|
||||
SignedAttributes: signature.SignedAttributes{
|
||||
SigningTime: time.Now(),
|
||||
|
@ -648,7 +332,7 @@ func (s *dummySigner) SignBlob(_ context.Context, descGenFunc BlobDescriptorGene
|
|||
|
||||
type verifyMetadataSigner struct{}
|
||||
|
||||
func (s *verifyMetadataSigner) Sign(_ context.Context, desc ocispec.Descriptor, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
func (s *verifyMetadataSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
for k, v := range expectedMetadata {
|
||||
if desc.Annotations[k] != v {
|
||||
return nil, nil, errors.New("expected metadata not present in descriptor")
|
||||
|
@ -662,14 +346,13 @@ func (s *verifyMetadataSigner) Sign(_ context.Context, desc ocispec.Descriptor,
|
|||
}
|
||||
|
||||
type dummyVerifier struct {
|
||||
TrustPolicyDoc *trustpolicy.OCIDocument
|
||||
TrustPolicyDoc *trustpolicy.Document
|
||||
PluginManager plugin.Manager
|
||||
FailVerify bool
|
||||
VerificationLevel trustpolicy.VerificationLevel
|
||||
SkipVerification bool
|
||||
}
|
||||
|
||||
func (v *dummyVerifier) Verify(_ context.Context, _ ocispec.Descriptor, _ []byte, _ VerifierVerifyOptions) (*VerificationOutcome, error) {
|
||||
func (v *dummyVerifier) Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifierVerifyOptions) (*VerificationOutcome, error) {
|
||||
outcome := &VerificationOutcome{
|
||||
VerificationResults: []*ValidationResult{},
|
||||
VerificationLevel: &v.VerificationLevel,
|
||||
|
@ -680,30 +363,8 @@ func (v *dummyVerifier) Verify(_ context.Context, _ ocispec.Descriptor, _ []byte
|
|||
return outcome, nil
|
||||
}
|
||||
|
||||
func (v *dummyVerifier) SkipVerify(_ context.Context, _ VerifierVerifyOptions) (bool, *trustpolicy.VerificationLevel, error) {
|
||||
if v.SkipVerification {
|
||||
return true, nil, nil
|
||||
}
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
func (v *dummyVerifier) VerifyBlob(_ context.Context, _ BlobDescriptorGenerator, _ []byte, _ BlobVerifierVerifyOptions) (*VerificationOutcome, error) {
|
||||
if v.FailVerify {
|
||||
return nil, errors.New("failed verify")
|
||||
}
|
||||
|
||||
return &VerificationOutcome{
|
||||
VerificationResults: []*ValidationResult{},
|
||||
VerificationLevel: &v.VerificationLevel,
|
||||
EnvelopeContent: &signature.EnvelopeContent{
|
||||
Payload: signature.Payload{
|
||||
Content: []byte("{}"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
ociLayoutPath = filepath.FromSlash("./internal/testdata/oci-layout")
|
||||
reference = "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
|
||||
artifactReference = "local/oci-layout@sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
|
||||
signaturePath = filepath.FromSlash("./internal/testdata/cose_signature.sig")
|
||||
|
@ -711,7 +372,7 @@ var (
|
|||
|
||||
type ociDummySigner struct{}
|
||||
|
||||
func (s *ociDummySigner) Sign(_ context.Context, _ ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
func (s *ociDummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
sigBlob, err := os.ReadFile(signaturePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -727,114 +388,37 @@ func (s *ociDummySigner) Sign(_ context.Context, _ ocispec.Descriptor, opts Sign
|
|||
return sigBlob, &content.SignerInfo, nil
|
||||
}
|
||||
|
||||
func TestLocalContent(t *testing.T) {
|
||||
// create a temp OCI layout
|
||||
ociLayoutTestDataPath, err := filepath.Abs(filepath.Join("internal", "testdata", "oci-layout"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get oci layout path: %v", err)
|
||||
}
|
||||
newOCILayoutPath := t.TempDir()
|
||||
if err := ocilayout.Copy(ociLayoutTestDataPath, newOCILayoutPath, "v2"); err != nil {
|
||||
t.Fatalf("failed to create temp oci layout: %v", err)
|
||||
}
|
||||
repo, err := registry.NewOCIRepository(newOCILayoutPath, registry.RepositoryOptions{})
|
||||
func TestSignLocalContent(t *testing.T) {
|
||||
repo, err := registry.NewOCIRepository(ociLayoutPath, registry.RepositoryOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("sign the local content", func(t *testing.T) {
|
||||
// sign the artifact
|
||||
signOpts := SignOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: cose.MediaTypeEnvelope,
|
||||
},
|
||||
ArtifactReference: reference,
|
||||
}
|
||||
_, err = Sign(context.Background(), &ociDummySigner{}, repo, signOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to Sign: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify local content", func(t *testing.T) {
|
||||
// verify the artifact
|
||||
verifyOpts := VerifyOptions{
|
||||
ArtifactReference: artifactReference,
|
||||
MaxSignatureAttempts: math.MaxInt64,
|
||||
}
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
// verify signatures inside the OCI layout folder
|
||||
_, _, err = Verify(context.Background(), &verifier, repo, verifyOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to verify local content: %v", err)
|
||||
}
|
||||
})
|
||||
signOpts := SignOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: cose.MediaTypeEnvelope,
|
||||
},
|
||||
ArtifactReference: reference,
|
||||
}
|
||||
_, err = Sign(context.Background(), &ociDummySigner{}, repo, signOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to Sign: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserMetadata(t *testing.T) {
|
||||
t.Run("EnvelopeContent is nil", func(t *testing.T) {
|
||||
outcome := &VerificationOutcome{}
|
||||
_, err := outcome.UserMetadata()
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, got nil")
|
||||
}
|
||||
if err.Error() != "unable to find envelope content for verification outcome" {
|
||||
t.Fatalf("expected error message 'unable to find envelope content for verification outcome', got '%s'", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EnvelopeContent is valid", func(t *testing.T) {
|
||||
payload := envelope.Payload{
|
||||
TargetArtifact: ocispec.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error marshaling payload: %v", err)
|
||||
}
|
||||
|
||||
outcome := &VerificationOutcome{
|
||||
EnvelopeContent: &signature.EnvelopeContent{
|
||||
Payload: signature.Payload{
|
||||
Content: payloadBytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
metadata, err := outcome.UserMetadata()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting user metadata: %v", err)
|
||||
}
|
||||
if len(metadata) != 1 || metadata["key"] != "value" {
|
||||
t.Fatalf("expected metadata map[key]=value, got %v", metadata)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Annotation is nil", func(t *testing.T) {
|
||||
payload := envelope.Payload{
|
||||
TargetArtifact: ocispec.Descriptor{},
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error marshaling payload: %v", err)
|
||||
}
|
||||
|
||||
outcome := &VerificationOutcome{
|
||||
EnvelopeContent: &signature.EnvelopeContent{
|
||||
Payload: signature.Payload{
|
||||
Content: payloadBytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
metadata, err := outcome.UserMetadata()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting user metadata: %v", err)
|
||||
}
|
||||
if len(metadata) != 0 {
|
||||
t.Fatalf("expected empty metadata, got %v", metadata)
|
||||
}
|
||||
})
|
||||
func TestVerifyLocalContent(t *testing.T) {
|
||||
repo, err := registry.NewOCIRepository(ociLayoutPath, registry.RepositoryOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create oci.Store as registry.Repository: %v", err)
|
||||
}
|
||||
verifyOpts := VerifyOptions{
|
||||
ArtifactReference: artifactReference,
|
||||
MaxSignatureAttempts: math.MaxInt64,
|
||||
}
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
// verify signatures inside the OCI layout folder
|
||||
_, _, err = Verify(context.Background(), &verifier, repo, verifyOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to verify local content: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plugin
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrNotCompliant is returned by plugin methods when the response is not
|
||||
// compliant.
|
||||
var ErrNotCompliant = errors.New("plugin not compliant")
|
||||
|
||||
// ErrNotRegularFile is returned when the plugin file is not an regular file.
|
||||
var ErrNotRegularFile = errors.New("plugin executable file is not a regular file")
|
||||
|
||||
// PluginDowngradeError is returned when installing a plugin with version
|
||||
// lower than the exisiting plugin version.
|
||||
type PluginDowngradeError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
// Error returns the error message.
|
||||
func (e PluginDowngradeError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return "installing plugin with version lower than the existing plugin version"
|
||||
}
|
||||
|
||||
// InstallEqualVersionError is returned when installing a plugin with version
|
||||
// equal to the exisiting plugin version.
|
||||
type InstallEqualVersionError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
// Error returns the error message.
|
||||
func (e InstallEqualVersionError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return "installing plugin with version equal to the existing plugin version"
|
||||
}
|
||||
|
||||
// PluginMalformedError is used when there is an issue with plugin and
|
||||
// should be fixed by plugin developers.
|
||||
type PluginMalformedError struct {
|
||||
Msg string
|
||||
InnerError error
|
||||
}
|
||||
|
||||
// Error returns the error message.
|
||||
func (e PluginMalformedError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return e.InnerError.Error()
|
||||
}
|
||||
|
||||
// Unwrap returns the inner error.
|
||||
func (e PluginMalformedError) Unwrap() error {
|
||||
return e.InnerError
|
||||
}
|
||||
|
||||
// PluginDirectoryWalkError is used when there is an issue with plugins directory
|
||||
// and should suggest user to check the permission of plugin directory.
|
||||
type PluginDirectoryWalkError error
|
||||
|
||||
// PluginExecutableFileError is used when there is an issue with plugin
|
||||
// executable file and should suggest user to check the existence, permission
|
||||
// and platform/arch compatibility of plugin.
|
||||
type PluginExecutableFileError struct {
|
||||
Msg string
|
||||
InnerError error
|
||||
}
|
||||
|
||||
// Error returns the error message.
|
||||
func (e PluginExecutableFileError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return e.InnerError.Error()
|
||||
}
|
||||
|
||||
// Unwrap returns the inner error.
|
||||
func (e PluginExecutableFileError) Unwrap() error {
|
||||
return e.InnerError
|
||||
}
|
|
@ -23,16 +23,16 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
)
|
||||
|
||||
var exampleMetadata = plugin.GetMetadataResponse{
|
||||
var exampleMetadata = proto.GetMetadataResponse{
|
||||
Name: "foo",
|
||||
Description: "friendly",
|
||||
Version: "1",
|
||||
URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"},
|
||||
Capabilities: []plugin.Capability{"cap"}}
|
||||
Capabilities: []proto.Capability{"cap"}}
|
||||
|
||||
func preparePlugin(t *testing.T) string {
|
||||
root := t.TempDir()
|
||||
|
@ -87,11 +87,11 @@ func TestIntegration(t *testing.T) {
|
|||
}
|
||||
|
||||
// validate and create
|
||||
pl, err := mgr.Get(context.Background(), "foo")
|
||||
plugin, err := mgr.Get(context.Background(), "foo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
metadata, err := pl.GetMetadata(context.Background(), &plugin.GetMetadataRequest{})
|
||||
metadata, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -16,26 +16,26 @@ package plugin
|
|||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-go/internal/file"
|
||||
"github.com/notaryproject/notation-go/internal/semver"
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
// ErrNotCompliant is returned by plugin methods when the response is not
|
||||
// compliant.
|
||||
var ErrNotCompliant = errors.New("plugin not compliant")
|
||||
|
||||
// ErrNotRegularFile is returned when the plugin file is not an regular file.
|
||||
var ErrNotRegularFile = errors.New("not regular file")
|
||||
|
||||
// Manager manages plugins installed on the system.
|
||||
type Manager interface {
|
||||
Get(ctx context.Context, name string) (plugin.Plugin, error)
|
||||
Get(ctx context.Context, name string) (Plugin, error)
|
||||
List(ctx context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
// CLIManager implements [Manager]
|
||||
// CLIManager implements Manager
|
||||
type CLIManager struct {
|
||||
pluginFS dir.SysFS
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ func NewCLIManager(pluginFS dir.SysFS) *CLIManager {
|
|||
// Get returns a plugin on the system by its name.
|
||||
//
|
||||
// If the plugin is not found, the error is of type os.ErrNotExist.
|
||||
func (m *CLIManager) Get(ctx context.Context, name string) (plugin.Plugin, error) {
|
||||
func (m *CLIManager) Get(ctx context.Context, name string) (Plugin, error) {
|
||||
pluginPath := path.Join(name, binName(name))
|
||||
path, err := m.pluginFS.SysPath(pluginPath)
|
||||
if err != nil {
|
||||
|
@ -62,11 +62,8 @@ func (m *CLIManager) Get(ctx context.Context, name string) (plugin.Plugin, error
|
|||
// List produces a list of the plugin names on the system.
|
||||
func (m *CLIManager) List(ctx context.Context) ([]string, error) {
|
||||
var plugins []string
|
||||
if err := fs.WalkDir(m.pluginFS, ".", func(dir string, d fs.DirEntry, err error) error {
|
||||
fs.WalkDir(m.pluginFS, ".", func(dir string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if dir == "." {
|
||||
|
@ -82,215 +79,6 @@ func (m *CLIManager) List(ctx context.Context) ([]string, error) {
|
|||
// add plugin name
|
||||
plugins = append(plugins, d.Name())
|
||||
return fs.SkipDir
|
||||
}); err != nil {
|
||||
return nil, PluginDirectoryWalkError(fmt.Errorf("failed to list plugin: %w", err))
|
||||
}
|
||||
})
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// CLIInstallOptions provides user customized options for plugin installation
|
||||
type CLIInstallOptions struct {
|
||||
// PluginPath can be path of:
|
||||
//
|
||||
// 1. A directory which contains plugin related files. Sub-directories are
|
||||
// ignored. It MUST contain one and only one valid plugin executable file
|
||||
// following spec: https://github.com/notaryproject/specifications/blob/v1.0.0/specs/plugin-extensibility.md#installation
|
||||
// It may contain extra lib files and LICENSE files.
|
||||
// On success, these files will be installed as well.
|
||||
//
|
||||
// 2. A single plugin executable file following the spec.
|
||||
PluginPath string
|
||||
|
||||
// Overwrite is a boolean flag. When set, always install the new plugin.
|
||||
Overwrite bool
|
||||
}
|
||||
|
||||
// Install installs a plugin to the system. It returns existing
|
||||
// plugin metadata, new plugin metadata, and error. It returns nil error
|
||||
// if and only if the installation succeeded.
|
||||
//
|
||||
// If plugin does not exist, directly install the new plugin.
|
||||
//
|
||||
// If plugin already exists:
|
||||
//
|
||||
// If overwrite is not set, then the new plugin
|
||||
// version MUST be higher than the existing plugin version.
|
||||
//
|
||||
// If overwrite is set, version check is skipped. If existing
|
||||
// plugin is malfunctioning, it will be overwritten.
|
||||
func (m *CLIManager) Install(ctx context.Context, installOpts CLIInstallOptions) (*plugin.GetMetadataResponse, *plugin.GetMetadataResponse, error) {
|
||||
// initialization
|
||||
logger := log.GetLogger(ctx)
|
||||
overwrite := installOpts.Overwrite
|
||||
if installOpts.PluginPath == "" {
|
||||
return nil, nil, errors.New("plugin source path cannot be empty")
|
||||
}
|
||||
logger.Debugf("Installing plugin from path %s", installOpts.PluginPath)
|
||||
var installFromNonDir bool
|
||||
pluginExecutableFile, pluginName, err := parsePluginFromDir(ctx, installOpts.PluginPath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, file.ErrNotDirectory) {
|
||||
return nil, nil, fmt.Errorf("failed to read plugin from input directory: %w", err)
|
||||
}
|
||||
// input is not a dir, check if it's a single plugin executable file
|
||||
installFromNonDir = true
|
||||
pluginExecutableFile = installOpts.PluginPath
|
||||
pluginExecutableFileName := filepath.Base(pluginExecutableFile)
|
||||
pluginName, err = parsePluginName(pluginExecutableFileName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read plugin name from input file %s: %w", pluginExecutableFileName, err)
|
||||
}
|
||||
isExec, err := isExecutableFile(pluginExecutableFile)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to check if input file %s is executable: %w", pluginExecutableFileName, err)
|
||||
}
|
||||
if !isExec {
|
||||
return nil, nil, fmt.Errorf("input file %s is not executable", pluginExecutableFileName)
|
||||
}
|
||||
}
|
||||
// validate and get new plugin metadata
|
||||
newPlugin, err := NewCLIPlugin(ctx, pluginName, pluginExecutableFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
newPluginMetadata, err := newPlugin.GetMetadata(ctx, &plugin.GetMetadataRequest{})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get metadata of new plugin: %w", err)
|
||||
}
|
||||
// check plugin existence and get existing plugin metadata
|
||||
var existingPluginMetadata *plugin.GetMetadataResponse
|
||||
existingPlugin, err := m.Get(ctx, pluginName)
|
||||
if err != nil {
|
||||
// fail only if overwrite is not set
|
||||
if !errors.Is(err, os.ErrNotExist) && !overwrite {
|
||||
return nil, nil, fmt.Errorf("failed to check plugin existence: %w", err)
|
||||
}
|
||||
} else { // plugin already exists
|
||||
existingPluginMetadata, err = existingPlugin.GetMetadata(ctx, &plugin.GetMetadataRequest{})
|
||||
if err != nil && !overwrite { // fail only if overwrite is not set
|
||||
return nil, nil, fmt.Errorf("failed to get metadata of existing plugin: %w", err)
|
||||
}
|
||||
// existing plugin is valid, and overwrite is not set, check version
|
||||
if !overwrite {
|
||||
comp, err := semver.ComparePluginVersion(newPluginMetadata.Version, existingPluginMetadata.Version)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to compare plugin versions: %w", err)
|
||||
}
|
||||
switch {
|
||||
case comp < 0:
|
||||
return nil, nil, PluginDowngradeError{Msg: fmt.Sprintf("failed to install plugin %s. The installing plugin version %s is lower than the existing plugin version %s", pluginName, newPluginMetadata.Version, existingPluginMetadata.Version)}
|
||||
case comp == 0:
|
||||
return nil, nil, InstallEqualVersionError{Msg: fmt.Sprintf("plugin %s with version %s already exists", pluginName, existingPluginMetadata.Version)}
|
||||
}
|
||||
}
|
||||
}
|
||||
// clean up before installation, this guarantees idempotent for install
|
||||
if err := m.Uninstall(ctx, pluginName); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil, fmt.Errorf("failed to clean up plugin %s before installation: %w", pluginName, err)
|
||||
}
|
||||
}
|
||||
// core process
|
||||
pluginDirPath, err := m.pluginFS.SysPath(pluginName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get the system path of plugin %s: %w", pluginName, err)
|
||||
}
|
||||
if installFromNonDir {
|
||||
if err := file.CopyToDir(pluginExecutableFile, pluginDirPath); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to copy plugin executable file from %s to %s: %w", pluginExecutableFile, pluginDirPath, err)
|
||||
}
|
||||
} else {
|
||||
if err := file.CopyDirToDir(installOpts.PluginPath, pluginDirPath); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to copy plugin files from %s to %s: %w", installOpts.PluginPath, pluginDirPath, err)
|
||||
}
|
||||
}
|
||||
return existingPluginMetadata, newPluginMetadata, nil
|
||||
}
|
||||
|
||||
// Uninstall uninstalls a plugin on the system by its name.
|
||||
// If the plugin dir does not exist, os.ErrNotExist is returned.
|
||||
func (m *CLIManager) Uninstall(ctx context.Context, name string) error {
|
||||
pluginDirPath, err := m.pluginFS.SysPath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(pluginDirPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.RemoveAll(pluginDirPath)
|
||||
}
|
||||
|
||||
// parsePluginFromDir checks if a dir is a valid plugin dir which contains
|
||||
// one and only one plugin executable file candidate.
|
||||
// The dir may contain extra lib files and LICENSE files.
|
||||
// Sub-directories are ignored.
|
||||
//
|
||||
// On success, the plugin executable file path, plugin name and
|
||||
// nil error are returned.
|
||||
func parsePluginFromDir(ctx context.Context, path string) (string, string, error) {
|
||||
// sanity check
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if !fi.Mode().IsDir() {
|
||||
return "", "", file.ErrNotDirectory
|
||||
}
|
||||
logger := log.GetLogger(ctx)
|
||||
// walk the path
|
||||
var pluginExecutableFile, pluginName, candidatePluginName string
|
||||
var foundPluginExecutableFile bool
|
||||
var filesWithValidNameFormat []string
|
||||
if err := filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// skip sub-directories
|
||||
if d.IsDir() && d.Name() != filepath.Base(path) {
|
||||
return fs.SkipDir
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// only take regular files
|
||||
if info.Mode().IsRegular() {
|
||||
if candidatePluginName, err = parsePluginName(d.Name()); err != nil {
|
||||
// file name does not follow the notation-{plugin-name} format,
|
||||
// continue
|
||||
return nil
|
||||
}
|
||||
filesWithValidNameFormat = append(filesWithValidNameFormat, p)
|
||||
isExec, err := isExecutableFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isExec {
|
||||
return nil
|
||||
}
|
||||
if foundPluginExecutableFile {
|
||||
return errors.New("found more than one plugin executable files")
|
||||
}
|
||||
foundPluginExecutableFile = true
|
||||
pluginExecutableFile = p
|
||||
pluginName = candidatePluginName
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if !foundPluginExecutableFile {
|
||||
// if no executable file was found, but there's one and only one
|
||||
// potential candidate, try install the candidate
|
||||
if len(filesWithValidNameFormat) == 1 {
|
||||
candidate := filesWithValidNameFormat[0]
|
||||
if err := setExecutable(candidate); err != nil {
|
||||
return "", "", fmt.Errorf("no plugin executable file was found: %w", err)
|
||||
}
|
||||
logger.Warnf("Found candidate plugin executable file %q without executable permission. Setting user executable bit and trying to install.", filepath.Base(candidate))
|
||||
return candidate, candidatePluginName, nil
|
||||
}
|
||||
return "", "", errors.New("no plugin executable file was found")
|
||||
}
|
||||
return pluginExecutableFile, pluginName, nil
|
||||
}
|
||||
|
|
|
@ -17,10 +17,7 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
|
@ -38,50 +35,8 @@ func (t testCommander) Output(ctx context.Context, path string, command proto.Co
|
|||
return t.stdout, t.stderr, t.err
|
||||
}
|
||||
|
||||
type testInstallCommander struct {
|
||||
existedPluginFilePath string
|
||||
existedPluginStdout []byte
|
||||
existedPluginStderr []byte
|
||||
existedPluginErr error
|
||||
newPluginFilePath string
|
||||
newPluginStdout []byte
|
||||
newPluginStderr []byte
|
||||
newPluginErr error
|
||||
err error
|
||||
}
|
||||
|
||||
func (t testInstallCommander) Output(ctx context.Context, path string, command proto.Command, req []byte) ([]byte, []byte, error) {
|
||||
if path == t.existedPluginFilePath {
|
||||
return t.existedPluginStdout, t.existedPluginStderr, t.existedPluginErr
|
||||
}
|
||||
if path == t.newPluginFilePath {
|
||||
return t.newPluginStdout, t.newPluginStderr, t.newPluginErr
|
||||
}
|
||||
return nil, nil, t.err
|
||||
}
|
||||
|
||||
var validMetadata = proto.GetMetadataResponse{
|
||||
Name: "foo", Description: "friendly", Version: "1.0.0", URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
|
||||
}
|
||||
|
||||
var validMetadataHigherVersion = proto.GetMetadataResponse{
|
||||
Name: "foo", Description: "friendly", Version: "1.1.0", URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
|
||||
}
|
||||
|
||||
var validMetadataLowerVersion = proto.GetMetadataResponse{
|
||||
Name: "foo", Description: "friendly", Version: "0.1.0", URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
|
||||
}
|
||||
|
||||
var validMetadataBar = proto.GetMetadataResponse{
|
||||
Name: "bar", Description: "friendly", Version: "1.0.0", URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
|
||||
}
|
||||
|
||||
var validMetadataBarExample = proto.GetMetadataResponse{
|
||||
Name: "bar.example.plugin", Description: "friendly", Version: "1.0.0", URL: "example.com",
|
||||
Name: "foo", Description: "friendly", Version: "1", URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
|
||||
}
|
||||
|
||||
|
@ -96,9 +51,6 @@ var invalidContractVersionMetadata = proto.GetMetadataResponse{
|
|||
}
|
||||
|
||||
func TestManager_Get(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
executor = testCommander{stdout: metadataJSON(validMetadata)}
|
||||
mgr := NewCLIManager(mockfs.NewSysFSWithRootMock(fstest.MapFS{}, "./testdata/plugins"))
|
||||
_, err := mgr.Get(context.Background(), "foo")
|
||||
|
@ -135,533 +87,6 @@ func TestManager_List(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestManager_Install(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
|
||||
newPluginFilePath := "testdata/foo/notation-foo"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mgr := NewCLIManager(mockfs.NewSysFSWithRootMock(fstest.MapFS{}, "testdata/plugins"))
|
||||
|
||||
t.Run("success install with higher version", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataHigherVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting error to be nil, but got %v", err)
|
||||
}
|
||||
if existingPluginMetadata.Version != validMetadata.Version {
|
||||
t.Fatalf("existing plugin version mismatch, existing plugin version: %s, but got: %s", validMetadata.Version, existingPluginMetadata.Version)
|
||||
}
|
||||
if newPluginMetadata.Version != validMetadataHigherVersion.Version {
|
||||
t.Fatalf("new plugin version mismatch, new plugin version: %s, but got: %s", validMetadataHigherVersion.Version, newPluginMetadata.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success install with lower version and overwrite", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataLowerVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
Overwrite: true,
|
||||
}
|
||||
if _, _, err := mgr.Install(context.Background(), installOpts); err != nil {
|
||||
t.Fatalf("expecting error to be nil, but got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success install without existing plugin", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-bar"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBar),
|
||||
}
|
||||
defer mgr.Uninstall(context.Background(), "bar")
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting error to be nil, but got %v", err)
|
||||
}
|
||||
if existingPluginMetadata != nil {
|
||||
t.Fatalf("expecting existingPluginMetadata to be nil, but got %v", existingPluginMetadata)
|
||||
}
|
||||
if newPluginMetadata.Version != validMetadataBar.Version {
|
||||
t.Fatalf("new plugin version mismatch, new plugin version: %s, but got: %s", validMetadataBar.Version, newPluginMetadata.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success install with file extension", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-bar.example.plugin"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBarExample),
|
||||
}
|
||||
defer mgr.Uninstall(context.Background(), "bar.example.plugin")
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting error to be nil, but got %v", err)
|
||||
}
|
||||
if existingPluginMetadata != nil {
|
||||
t.Fatalf("expecting existingPluginMetadata to be nil, but got %v", existingPluginMetadata)
|
||||
}
|
||||
if newPluginMetadata.Version != validMetadataBar.Version {
|
||||
t.Fatalf("new plugin version mismatch, new plugin version: %s, but got: %s", validMetadataBar.Version, newPluginMetadata.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to equal version", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadata),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "plugin foo with version 1.0.0 already exists"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to lower version", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataLowerVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to install plugin foo. The installing plugin version 0.1.0 is lower than the existing plugin version 1.0.0"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to wrong plugin executable file name format", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/bar"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBar),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to read plugin name from input file bar: invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got bar"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to plugin executable file name missing plugin name", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBar),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to read plugin name from input file notation-: invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got notation-"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to wrong plugin file permission", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-bar"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBar),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "input file notation-bar is not executable"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to new plugin executable file does not exist", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-bar"
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBar),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to read plugin from input directory: stat testdata/bar/notation-bar: no such file or directory"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to invalid new plugin metadata", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-bar"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(invalidMetadataName),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to get metadata of new plugin: plugin executable file name must be \"notation-foobar\" instead of \"notation-bar\""
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to invalid existing plugin metadata", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadataBar),
|
||||
newPluginStdout: metadataJSON(validMetadata),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to get metadata of existing plugin: plugin executable file name must be \"notation-bar\" instead of \"notation-foo\""
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success to install with overwrite and invalid existing plugin metadata", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadataBar),
|
||||
newPluginStdout: metadataJSON(validMetadata),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
Overwrite: true,
|
||||
}
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting error to be nil, but got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success to install from plugin dir", func(t *testing.T) {
|
||||
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
|
||||
newPluginFilePath := "testdata/foo/notation-foo"
|
||||
newPluginLibPath := "testdata/foo/notation-libfoo"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := createFileAndChmod(newPluginLibPath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataHigherVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginDir,
|
||||
}
|
||||
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting nil error, but got %v", err)
|
||||
}
|
||||
if existingPluginMetadata.Version != "1.0.0" {
|
||||
t.Fatalf("expecting existing plugin metadata to be 1.0.0, but got %s", existingPluginMetadata.Version)
|
||||
}
|
||||
if newPluginMetadata.Version != "1.1.0" {
|
||||
t.Fatalf("expecting new plugin metadata to be 1.1.0, but got %s", newPluginMetadata.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success to install from plugin dir with no executable file and one valid candidate file", func(t *testing.T) {
|
||||
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
|
||||
newPluginFilePath := "testdata/foo/notation-foo"
|
||||
newPluginLibPath := "testdata/foo/libfoo"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := createFileAndChmod(newPluginLibPath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataHigherVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginDir,
|
||||
}
|
||||
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting nil error, but got %v", err)
|
||||
}
|
||||
if existingPluginMetadata.Version != "1.0.0" {
|
||||
t.Fatalf("expecting existing plugin metadata to be 1.0.0, but got %s", existingPluginMetadata.Version)
|
||||
}
|
||||
if newPluginMetadata.Version != "1.1.0" {
|
||||
t.Fatalf("expecting new plugin metadata to be 1.1.0, but got %s", newPluginMetadata.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install from plugin dir due to more than one candidate plugin executable files", func(t *testing.T) {
|
||||
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
|
||||
newPluginFilePath := "testdata/foo/notation-foo1"
|
||||
newPluginFilePath2 := "testdata/foo/notation-foo2"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := createFileAndChmod(newPluginFilePath2, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataHigherVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginDir,
|
||||
}
|
||||
expectedErrorMsg := "failed to read plugin from input directory: no plugin executable file was found"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install from plugin dir due to more than one plugin executable files", func(t *testing.T) {
|
||||
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
|
||||
newPluginFilePath := "testdata/foo/notation-foo1"
|
||||
newPluginFilePath2 := "testdata/foo/notation-foo2"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := createFileAndChmod(newPluginFilePath2, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataHigherVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginDir,
|
||||
}
|
||||
expectedErrorMsg := "failed to read plugin from input directory: found more than one plugin executable files"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_Uninstall(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
executor = testCommander{stdout: metadataJSON(validMetadata)}
|
||||
mgr := NewCLIManager(mockfs.NewSysFSWithRootMock(fstest.MapFS{}, "./testdata/plugins"))
|
||||
if err := os.MkdirAll("./testdata/plugins/toUninstall", 0777); err != nil {
|
||||
t.Fatalf("failed to create toUninstall dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll("./testdata/plugins/toUninstall")
|
||||
pluginFile, err := os.Create("./testdata/plugins/toUninstall/toUninstall")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create toUninstall file: %v", err)
|
||||
}
|
||||
if err := pluginFile.Close(); err != nil {
|
||||
t.Fatalf("failed to close toUninstall file: %v", err)
|
||||
}
|
||||
// test uninstall valid plugin
|
||||
if err := mgr.Uninstall(context.Background(), "toUninstall"); err != nil {
|
||||
t.Fatalf("Manager.Uninstall() err %v, want nil", err)
|
||||
}
|
||||
// test uninstall non-exist plugin
|
||||
expectedErrorMsg := "stat testdata/plugins/non-exist: no such file or directory"
|
||||
if err := mgr.Uninstall(context.Background(), "non-exist"); err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("Manager.Uninstall() err %v, want %s", err, expectedErrorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePluginName(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
pluginName, err := parsePluginName("notation-my-plugin.exe")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil err, but got %v", err)
|
||||
}
|
||||
if pluginName != "my-plugin" {
|
||||
t.Fatalf("expected plugin name my-plugin, but got %s", pluginName)
|
||||
}
|
||||
|
||||
expectedErrorMsg := "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got notation-com.plugin"
|
||||
_, err = parsePluginName("notation-com.plugin")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
|
||||
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got my-plugin.exe"
|
||||
_, err = parsePluginName("my-plugin.exe")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
|
||||
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got notation-.exe"
|
||||
_, err = parsePluginName("notation-.exe")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
|
||||
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got my-plugin"
|
||||
_, err = parsePluginName("my-plugin")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
} else {
|
||||
pluginName, err := parsePluginName("notation-my-plugin")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil err, but got %v", err)
|
||||
}
|
||||
if pluginName != "my-plugin" {
|
||||
t.Fatalf("expected plugin name my-plugin, but got %s", pluginName)
|
||||
}
|
||||
|
||||
pluginName, err = parsePluginName("notation-com.example.plugin")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil err, but got %v", err)
|
||||
}
|
||||
if pluginName != "com.example.plugin" {
|
||||
t.Fatalf("expected plugin name com.example.plugin, but got %s", pluginName)
|
||||
}
|
||||
|
||||
expectedErrorMsg := "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got myPlugin"
|
||||
_, err = parsePluginName("myPlugin")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
|
||||
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got my-plugin"
|
||||
_, err = parsePluginName("my-plugin")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
|
||||
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got notation-"
|
||||
_, err = parsePluginName("notation-")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func metadataJSON(m proto.GetMetadataResponse) []byte {
|
||||
d, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
|
@ -669,14 +94,3 @@ func metadataJSON(m proto.GetMetadataResponse) []byte {
|
|||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func createFileAndChmod(path string, mode fs.FileMode) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Chmod(mode); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
|
|
@ -16,46 +16,8 @@
|
|||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
import "github.com/notaryproject/notation-go/plugin/proto"
|
||||
|
||||
func binName(name string) string {
|
||||
return plugin.BinaryPrefix + name
|
||||
}
|
||||
|
||||
// isExecutableFile checks if a file at filePath is user executable
|
||||
func isExecutableFile(filePath string) (bool, error) {
|
||||
fi, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
mode := fi.Mode()
|
||||
if !mode.IsRegular() {
|
||||
return false, ErrNotRegularFile
|
||||
}
|
||||
return mode.Perm()&0100 != 0, nil
|
||||
}
|
||||
|
||||
// parsePluginName checks if fileName is a valid plugin file name
|
||||
// and gets plugin name from it based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation
|
||||
func parsePluginName(fileName string) (string, error) {
|
||||
pluginName, found := strings.CutPrefix(fileName, plugin.BinaryPrefix)
|
||||
if !found || pluginName == "" {
|
||||
return "", fmt.Errorf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got %s", fileName)
|
||||
}
|
||||
return pluginName, nil
|
||||
}
|
||||
|
||||
// setExecutable sets file to be user executable
|
||||
func setExecutable(filePath string) error {
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(filePath, fileInfo.Mode()|os.FileMode(0100))
|
||||
return proto.Prefix + name
|
||||
}
|
||||
|
|
|
@ -13,48 +13,8 @@
|
|||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-go/internal/file"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
import "github.com/notaryproject/notation-go/plugin/proto"
|
||||
|
||||
func binName(name string) string {
|
||||
return plugin.BinaryPrefix + name + ".exe"
|
||||
}
|
||||
|
||||
// isExecutableFile checks if a file at filePath is executable
|
||||
func isExecutableFile(filePath string) (bool, error) {
|
||||
fi, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return false, ErrNotRegularFile
|
||||
}
|
||||
return strings.EqualFold(filepath.Ext(filepath.Base(filePath)), ".exe"), nil
|
||||
}
|
||||
|
||||
// parsePluginName checks if fileName is a valid plugin file name
|
||||
// and gets plugin name from it based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation
|
||||
func parsePluginName(fileName string) (string, error) {
|
||||
if !strings.EqualFold(filepath.Ext(fileName), ".exe") {
|
||||
return "", fmt.Errorf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got %s", fileName)
|
||||
}
|
||||
fname := file.TrimFileExtension(fileName)
|
||||
pluginName, found := strings.CutPrefix(fname, plugin.BinaryPrefix)
|
||||
if !found || pluginName == "" {
|
||||
return "", fmt.Errorf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got %s", fileName)
|
||||
}
|
||||
return pluginName, nil
|
||||
}
|
||||
|
||||
// setExecutable returns error on Windows. User needs to install the correct
|
||||
// plugin file.
|
||||
func setExecutable(filePath string) error {
|
||||
return fmt.Errorf(`plugin executable file must have file extension ".exe", but got %q`, filepath.Base(filePath))
|
||||
return proto.Prefix + name + ".exe"
|
||||
}
|
||||
|
|
165
plugin/plugin.go
165
plugin/plugin.go
|
@ -11,7 +11,7 @@
|
|||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package plugin provides the tooling to use the notation plugin.
|
||||
// Package plugin provides the toolings to use the notation plugin.
|
||||
//
|
||||
// includes a CLIManager and a CLIPlugin implementation.
|
||||
package plugin
|
||||
|
@ -25,58 +25,63 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-go/internal/io"
|
||||
"github.com/notaryproject/notation-go/internal/slices"
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
// maxPluginOutputSize is the maximum size of the plugin output.
|
||||
const maxPluginOutputSize = 64 * 1024 * 1024 // 64 MiB
|
||||
|
||||
var executor commander = &execCommander{} // for unit test
|
||||
|
||||
// GenericPlugin is the base requirement to be a plugin.
|
||||
//
|
||||
// Deprecated: GenericPlugin exists for historical compatibility and should not be used.
|
||||
// To access GenericPlugin, use the notation-plugin-framework-go's plugin.GenericPlugin type.
|
||||
type GenericPlugin = plugin.GenericPlugin
|
||||
// GenericPlugin is the base requirement to be an plugin.
|
||||
type GenericPlugin interface {
|
||||
// GetMetadata returns the metadata information of the plugin.
|
||||
GetMetadata(ctx context.Context, req *proto.GetMetadataRequest) (*proto.GetMetadataResponse, error)
|
||||
}
|
||||
|
||||
// SignPlugin defines the required methods to be a SignPlugin.
|
||||
//
|
||||
// Deprecated: SignPlugin exists for historical compatibility and should not be used.
|
||||
// To access SignPlugin, use the notation-plugin-framework-go's plugin.SignPlugin type.
|
||||
type SignPlugin = plugin.SignPlugin
|
||||
type SignPlugin interface {
|
||||
GenericPlugin
|
||||
|
||||
// DescribeKey returns the KeySpec of a key.
|
||||
DescribeKey(ctx context.Context, req *proto.DescribeKeyRequest) (*proto.DescribeKeyResponse, error)
|
||||
|
||||
// GenerateSignature generates the raw signature based on the request.
|
||||
GenerateSignature(ctx context.Context, req *proto.GenerateSignatureRequest) (*proto.GenerateSignatureResponse, error)
|
||||
|
||||
// GenerateEnvelope generates the Envelope with signature based on the
|
||||
// request.
|
||||
GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error)
|
||||
}
|
||||
|
||||
// VerifyPlugin defines the required method to be a VerifyPlugin.
|
||||
//
|
||||
// Deprecated: VerifyPlugin exists for historical compatibility and should not be used.
|
||||
// To access VerifyPlugin, use the notation-plugin-framework-go's plugin.VerifyPlugin type.
|
||||
type VerifyPlugin = plugin.VerifyPlugin
|
||||
type VerifyPlugin interface {
|
||||
GenericPlugin
|
||||
|
||||
// Plugin defines required methods to be a Plugin.
|
||||
//
|
||||
// Deprecated: Plugin exists for historical compatibility and should not be used.
|
||||
// To access Plugin, use the notation-plugin-framework-go's plugin.Plugin type.
|
||||
type Plugin = plugin.Plugin
|
||||
// VerifySignature validates the signature based on the request.
|
||||
VerifySignature(ctx context.Context, req *proto.VerifySignatureRequest) (*proto.VerifySignatureResponse, error)
|
||||
}
|
||||
|
||||
// CLIPlugin implements [Plugin] interface to CLI plugins.
|
||||
// Plugin defines required methods to be an Plugin.
|
||||
type Plugin interface {
|
||||
SignPlugin
|
||||
VerifyPlugin
|
||||
}
|
||||
|
||||
// CLIPlugin implements Plugin interface to CLI plugins.
|
||||
type CLIPlugin struct {
|
||||
name string
|
||||
path string
|
||||
}
|
||||
|
||||
// NewCLIPlugin returns a *CLIPlugin.
|
||||
// NewCLIPlugin validate the metadata of the plugin and return a *CLIPlugin.
|
||||
func NewCLIPlugin(ctx context.Context, name, path string) (*CLIPlugin, error) {
|
||||
// validate file existence
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
// Ignore any file which we cannot Stat
|
||||
// (e.g. due to permissions or anything else).
|
||||
return nil, fmt.Errorf("plugin executable file is either not found or inaccessible: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
// Ignore non-regular files.
|
||||
|
@ -84,28 +89,26 @@ func NewCLIPlugin(ctx context.Context, name, path string) (*CLIPlugin, error) {
|
|||
}
|
||||
|
||||
// generate plugin
|
||||
return &CLIPlugin{
|
||||
plugin := CLIPlugin{
|
||||
name: name,
|
||||
path: path,
|
||||
}, nil
|
||||
}
|
||||
return &plugin, nil
|
||||
}
|
||||
|
||||
// GetMetadata returns the metadata information of the plugin.
|
||||
func (p *CLIPlugin) GetMetadata(ctx context.Context, req *plugin.GetMetadataRequest) (*plugin.GetMetadataResponse, error) {
|
||||
var metadata plugin.GetMetadataResponse
|
||||
func (p *CLIPlugin) GetMetadata(ctx context.Context, req *proto.GetMetadataRequest) (*proto.GetMetadataResponse, error) {
|
||||
var metadata proto.GetMetadataResponse
|
||||
err := run(ctx, p.name, p.path, req, &metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// validate metadata
|
||||
if err = validate(&metadata); err != nil {
|
||||
return nil, &PluginMalformedError{
|
||||
Msg: fmt.Sprintf("metadata validation failed for plugin %s: %s", p.name, err),
|
||||
InnerError: err,
|
||||
}
|
||||
return nil, fmt.Errorf("invalid metadata: %w", err)
|
||||
}
|
||||
if metadata.Name != p.name {
|
||||
return nil, fmt.Errorf("plugin executable file name must be %q instead of %q", binName(metadata.Name), filepath.Base(p.path))
|
||||
return nil, fmt.Errorf("executable name must be %q instead of %q", binName(metadata.Name), filepath.Base(p.path))
|
||||
}
|
||||
return &metadata, nil
|
||||
}
|
||||
|
@ -113,12 +116,12 @@ func (p *CLIPlugin) GetMetadata(ctx context.Context, req *plugin.GetMetadataRequ
|
|||
// DescribeKey returns the KeySpec of a key.
|
||||
//
|
||||
// if ContractVersion is not set, it will be set by the function.
|
||||
func (p *CLIPlugin) DescribeKey(ctx context.Context, req *plugin.DescribeKeyRequest) (*plugin.DescribeKeyResponse, error) {
|
||||
func (p *CLIPlugin) DescribeKey(ctx context.Context, req *proto.DescribeKeyRequest) (*proto.DescribeKeyResponse, error) {
|
||||
if req.ContractVersion == "" {
|
||||
req.ContractVersion = plugin.ContractVersion
|
||||
req.ContractVersion = proto.ContractVersion
|
||||
}
|
||||
|
||||
var resp plugin.DescribeKeyResponse
|
||||
var resp proto.DescribeKeyResponse
|
||||
err := run(ctx, p.name, p.path, req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
@ -126,12 +129,12 @@ func (p *CLIPlugin) DescribeKey(ctx context.Context, req *plugin.DescribeKeyRequ
|
|||
// GenerateSignature generates the raw signature based on the request.
|
||||
//
|
||||
// if ContractVersion is not set, it will be set by the function.
|
||||
func (p *CLIPlugin) GenerateSignature(ctx context.Context, req *plugin.GenerateSignatureRequest) (*plugin.GenerateSignatureResponse, error) {
|
||||
func (p *CLIPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateSignatureRequest) (*proto.GenerateSignatureResponse, error) {
|
||||
if req.ContractVersion == "" {
|
||||
req.ContractVersion = plugin.ContractVersion
|
||||
req.ContractVersion = proto.ContractVersion
|
||||
}
|
||||
|
||||
var resp plugin.GenerateSignatureResponse
|
||||
var resp proto.GenerateSignatureResponse
|
||||
err := run(ctx, p.name, p.path, req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
@ -139,12 +142,12 @@ func (p *CLIPlugin) GenerateSignature(ctx context.Context, req *plugin.GenerateS
|
|||
// GenerateEnvelope generates the Envelope with signature based on the request.
|
||||
//
|
||||
// if ContractVersion is not set, it will be set by the function.
|
||||
func (p *CLIPlugin) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) {
|
||||
func (p *CLIPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error) {
|
||||
if req.ContractVersion == "" {
|
||||
req.ContractVersion = plugin.ContractVersion
|
||||
req.ContractVersion = proto.ContractVersion
|
||||
}
|
||||
|
||||
var resp plugin.GenerateEnvelopeResponse
|
||||
var resp proto.GenerateEnvelopeResponse
|
||||
err := run(ctx, p.name, p.path, req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
@ -152,96 +155,76 @@ func (p *CLIPlugin) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEn
|
|||
// VerifySignature validates the signature based on the request.
|
||||
//
|
||||
// if ContractVersion is not set, it will be set by the function.
|
||||
func (p *CLIPlugin) VerifySignature(ctx context.Context, req *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) {
|
||||
func (p *CLIPlugin) VerifySignature(ctx context.Context, req *proto.VerifySignatureRequest) (*proto.VerifySignatureResponse, error) {
|
||||
if req.ContractVersion == "" {
|
||||
req.ContractVersion = plugin.ContractVersion
|
||||
req.ContractVersion = proto.ContractVersion
|
||||
}
|
||||
|
||||
var resp plugin.VerifySignatureResponse
|
||||
var resp proto.VerifySignatureResponse
|
||||
err := run(ctx, p.name, p.path, req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func run(ctx context.Context, pluginName string, pluginPath string, req plugin.Request, resp interface{}) error {
|
||||
func run(ctx context.Context, pluginName string, pluginPath string, req proto.Request, resp interface{}) error {
|
||||
logger := log.GetLogger(ctx)
|
||||
|
||||
// serialize request
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to marshal request object: %+v", req)
|
||||
return fmt.Errorf("failed to marshal request object: %w", err)
|
||||
return fmt.Errorf("%s: failed to marshal request object: %w", pluginName, err)
|
||||
}
|
||||
|
||||
logger.Debugf("Plugin %s request: %s", req.Command(), string(data))
|
||||
// execute request
|
||||
stdout, stderr, err := executor.Output(ctx, pluginPath, req.Command(), data)
|
||||
if err != nil {
|
||||
logger.Errorf("plugin %s execution status: %v", req.Command(), err)
|
||||
|
||||
if len(stderr) == 0 {
|
||||
// if stderr is empty, it is possible that the plugin is not
|
||||
// running properly.
|
||||
logger.Errorf("failed to execute the %s command for plugin %s: %s", req.Command(), pluginName, err)
|
||||
return &PluginExecutableFileError{
|
||||
InnerError: err,
|
||||
}
|
||||
} else {
|
||||
var re proto.RequestError
|
||||
jsonErr := json.Unmarshal(stderr, &re)
|
||||
if jsonErr != nil {
|
||||
logger.Errorf("failed to execute the %s command for plugin %s: %s", req.Command(), pluginName, strings.TrimSuffix(string(stderr), "\n"))
|
||||
return &PluginMalformedError{
|
||||
InnerError: jsonErr,
|
||||
}
|
||||
}
|
||||
logger.Errorf("failed to execute the %s command for plugin %s: %s: %w", req.Command(), pluginName, re.Code, re)
|
||||
return re
|
||||
logger.Debugf("plugin %s execution status: %v", req.Command(), err)
|
||||
logger.Debugf("Plugin %s returned error: %s", req.Command(), string(stderr))
|
||||
var re proto.RequestError
|
||||
jsonErr := json.Unmarshal(stderr, &re)
|
||||
if jsonErr != nil {
|
||||
return proto.RequestError{
|
||||
Code: proto.ErrorCodeGeneric,
|
||||
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", err, string(stderr))}
|
||||
}
|
||||
return re
|
||||
}
|
||||
|
||||
logger.Debugf("Plugin %s response: %s", req.Command(), string(stdout))
|
||||
// deserialize response
|
||||
if err = json.Unmarshal(stdout, resp); err != nil {
|
||||
logger.Errorf("failed to unmarshal plugin %s response: %w", req.Command(), err)
|
||||
return &PluginMalformedError{
|
||||
Msg: fmt.Sprintf("failed to unmarshal the response of %s command for plugin %s", req.Command(), pluginName),
|
||||
InnerError: err,
|
||||
}
|
||||
err = json.Unmarshal(stdout, resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode json response: %w", ErrNotCompliant)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// commander is defined for mocking purposes.
|
||||
type commander interface {
|
||||
// Output runs the command, passing req to the stdin.
|
||||
// Output runs the command, passing req to the its stdin.
|
||||
// It only returns an error if the binary can't be executed.
|
||||
// Returns stdout if err is nil, stderr if err is not nil.
|
||||
Output(ctx context.Context, path string, command plugin.Command, req []byte) (stdout []byte, stderr []byte, err error)
|
||||
Output(ctx context.Context, path string, command proto.Command, req []byte) (stdout []byte, stderr []byte, err error)
|
||||
}
|
||||
|
||||
// execCommander implements the commander interface using exec.Command().
|
||||
type execCommander struct{}
|
||||
|
||||
func (c execCommander) Output(ctx context.Context, name string, command plugin.Command, req []byte) ([]byte, []byte, error) {
|
||||
func (c execCommander) Output(ctx context.Context, name string, command proto.Command, req []byte) ([]byte, []byte, error) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(ctx, name, string(command))
|
||||
cmd.Stdin = bytes.NewReader(req)
|
||||
// The limit writer will be handled by the caller in run() by comparing the
|
||||
// bytes written with the expected length of the bytes.
|
||||
cmd.Stderr = io.LimitWriter(&stderr, maxPluginOutputSize)
|
||||
cmd.Stdout = io.LimitWriter(&stdout, maxPluginOutputSize)
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdout = &stdout
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return nil, stderr.Bytes(), fmt.Errorf("'%s %s' command execution timeout: %w", name, string(command), err)
|
||||
}
|
||||
return nil, stderr.Bytes(), err
|
||||
}
|
||||
return stdout.Bytes(), nil, nil
|
||||
}
|
||||
|
||||
// validate checks if the metadata is correctly populated.
|
||||
func validate(metadata *plugin.GetMetadataResponse) error {
|
||||
func validate(metadata *proto.GetMetadataResponse) error {
|
||||
if metadata.Name == "" {
|
||||
return errors.New("empty name")
|
||||
}
|
||||
|
@ -260,10 +243,10 @@ func validate(metadata *plugin.GetMetadataResponse) error {
|
|||
if len(metadata.SupportedContractVersions) == 0 {
|
||||
return errors.New("supported contract versions not specified")
|
||||
}
|
||||
if !slices.Contains(metadata.SupportedContractVersions, plugin.ContractVersion) {
|
||||
if !slices.Contains(metadata.SupportedContractVersions, proto.ContractVersion) {
|
||||
return fmt.Errorf(
|
||||
"contract version %q is not in the list of the plugin supported versions %v",
|
||||
plugin.ContractVersion, metadata.SupportedContractVersions,
|
||||
proto.ContractVersion, metadata.SupportedContractVersions,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -17,13 +17,12 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
)
|
||||
|
@ -32,12 +31,14 @@ func TestGetMetadata(t *testing.T) {
|
|||
t.Run("plugin error is in invalid json format", func(t *testing.T) {
|
||||
exitErr := errors.New("unknown error")
|
||||
stderr := []byte("sad")
|
||||
expectedErrMsg := "invalid character 's' looking for beginning of value"
|
||||
plugin := CLIPlugin{name: "test-plugin"}
|
||||
wantErr := proto.RequestError{
|
||||
Code: proto.ErrorCodeGeneric,
|
||||
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", exitErr, string(stderr))}
|
||||
plugin := CLIPlugin{}
|
||||
executor = testCommander{stdout: nil, stderr: stderr, err: exitErr}
|
||||
_, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
|
||||
if err.Error() != expectedErrMsg {
|
||||
t.Fatalf("should error. got err = %v, want %v", err, expectedErrMsg)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("should error. got err = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -57,12 +58,14 @@ func TestGetMetadata(t *testing.T) {
|
|||
t.Run("plugin cause system error", func(t *testing.T) {
|
||||
exitErr := errors.New("system error")
|
||||
stderr := []byte("")
|
||||
expectedErrMsg := "system error"
|
||||
plugin := CLIPlugin{name: "test-plugin"}
|
||||
wantErr := proto.RequestError{
|
||||
Code: proto.ErrorCodeGeneric,
|
||||
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", exitErr, string(stderr))}
|
||||
plugin := CLIPlugin{}
|
||||
executor = testCommander{stdout: nil, stderr: stderr, err: exitErr}
|
||||
_, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
|
||||
if err.Error() != expectedErrMsg {
|
||||
t.Fatalf("should error. got err = %v, want %v", err, expectedErrMsg)
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("should error. got err = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -183,7 +186,7 @@ func TestValidateMetadata(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewCLIPlugin_Error(t *testing.T) {
|
||||
func TestNewCLIPlugin_PathError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Run("plugin directory exists without executable.", func(t *testing.T) {
|
||||
p, err := NewCLIPlugin(ctx, "emptyplugin", "./testdata/plugins/emptyplugin/notation-emptyplugin")
|
||||
|
@ -196,34 +199,14 @@ func TestNewCLIPlugin_Error(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("plugin is not a regular file", func(t *testing.T) {
|
||||
expectedErrMsg := "plugin executable file is not a regular file"
|
||||
p, err := NewCLIPlugin(ctx, "badplugin", "./testdata/plugins/badplugin/notation-badplugin")
|
||||
if err.Error() != expectedErrMsg {
|
||||
t.Errorf("NewCLIPlugin() error = %v, want %v", err, expectedErrMsg)
|
||||
if !errors.Is(err, ErrNotRegularFile) {
|
||||
t.Errorf("NewCLIPlugin() error = %v, want %v", err, ErrNotRegularFile)
|
||||
}
|
||||
if p != nil {
|
||||
t.Errorf("NewCLIPlugin() plugin = %v, want nil", p)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plugin timeout error", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
expectedErrMsg := "'sleep 2' command execution timeout: signal: killed"
|
||||
ctxWithTimout, cancel := context.WithTimeout(ctx, 10 * time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
var twoSeconds proto.Command
|
||||
twoSeconds = "2"
|
||||
_, _, err := execCommander{}.Output(ctxWithTimout, "sleep", twoSeconds, nil);
|
||||
if err == nil {
|
||||
t.Errorf("execCommander{}.Output() expected error = %v, got nil", expectedErrMsg)
|
||||
}
|
||||
if err.Error() != expectedErrMsg {
|
||||
t.Errorf("execCommander{}.Output() error = %v, want %v", err, expectedErrMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewCLIPlugin_ValidError(t *testing.T) {
|
||||
|
@ -235,23 +218,23 @@ func TestNewCLIPlugin_ValidError(t *testing.T) {
|
|||
t.Run("command no response", func(t *testing.T) {
|
||||
executor = testCommander{}
|
||||
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
|
||||
if _, ok := err.(*PluginMalformedError); !ok {
|
||||
t.Fatal("should return plugin validity error")
|
||||
if !strings.Contains(err.Error(), ErrNotCompliant.Error()) {
|
||||
t.Fatal("should fail the operation.")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid json", func(t *testing.T) {
|
||||
executor = testCommander{stdout: []byte("content")}
|
||||
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
|
||||
if _, ok := err.(*PluginMalformedError); !ok {
|
||||
t.Fatal("should return plugin validity error")
|
||||
if !strings.Contains(err.Error(), ErrNotCompliant.Error()) {
|
||||
t.Fatal("should fail the operation.")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid metadata name", func(t *testing.T) {
|
||||
executor = testCommander{stdout: metadataJSON(invalidMetadataName)}
|
||||
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
|
||||
if !strings.Contains(err.Error(), "executable file name must be") {
|
||||
if !strings.Contains(err.Error(), "executable name must be") {
|
||||
t.Fatal("should fail the operation.")
|
||||
}
|
||||
})
|
||||
|
@ -259,8 +242,8 @@ func TestNewCLIPlugin_ValidError(t *testing.T) {
|
|||
t.Run("invalid metadata content", func(t *testing.T) {
|
||||
executor = testCommander{stdout: metadataJSON(proto.GetMetadataResponse{Name: "foo"})}
|
||||
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
|
||||
if _, ok := err.(*PluginMalformedError); !ok {
|
||||
t.Fatal("should be plugin validity error.")
|
||||
if !strings.Contains(err.Error(), "invalid metadata") {
|
||||
t.Fatal("should fail the operation.")
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -18,74 +18,67 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
// KeySpec is type of the signing algorithm, including algorithm and size.
|
||||
//
|
||||
// Deprecated: KeySpec exists for historical compatibility and should not be used.
|
||||
// To access KeySpec, use the notation-plugin-framework-go's [plugin.KeySpec] type.
|
||||
type KeySpec = plugin.KeySpec
|
||||
type KeySpec string
|
||||
|
||||
// one of the following supported key spec names.
|
||||
//
|
||||
// Deprecated: KeySpec exists for historical compatibility and should not be used.
|
||||
// To access KeySpec, use the notation-plugin-framework-go's [plugin.KeySpec].
|
||||
//
|
||||
// [keys spec]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||
const (
|
||||
KeySpecRSA2048 = plugin.KeySpecRSA2048
|
||||
KeySpecRSA3072 = plugin.KeySpecRSA3072
|
||||
KeySpecRSA4096 = plugin.KeySpecRSA4096
|
||||
KeySpecEC256 = plugin.KeySpecEC256
|
||||
KeySpecEC384 = plugin.KeySpecEC384
|
||||
KeySpecEC521 = plugin.KeySpecEC521
|
||||
KeySpecRSA2048 KeySpec = "RSA-2048"
|
||||
KeySpecRSA3072 KeySpec = "RSA-3072"
|
||||
KeySpecRSA4096 KeySpec = "RSA-4096"
|
||||
KeySpecEC256 KeySpec = "EC-256"
|
||||
KeySpecEC384 KeySpec = "EC-384"
|
||||
KeySpecEC521 KeySpec = "EC-521"
|
||||
)
|
||||
|
||||
// EncodeKeySpec returns the name of a keySpec according to the spec.
|
||||
func EncodeKeySpec(k signature.KeySpec) (plugin.KeySpec, error) {
|
||||
func EncodeKeySpec(k signature.KeySpec) (KeySpec, error) {
|
||||
switch k.Type {
|
||||
case signature.KeyTypeEC:
|
||||
switch k.Size {
|
||||
case 256:
|
||||
return plugin.KeySpecEC256, nil
|
||||
return KeySpecEC256, nil
|
||||
case 384:
|
||||
return plugin.KeySpecEC384, nil
|
||||
return KeySpecEC384, nil
|
||||
case 521:
|
||||
return plugin.KeySpecEC521, nil
|
||||
return KeySpecEC521, nil
|
||||
}
|
||||
case signature.KeyTypeRSA:
|
||||
switch k.Size {
|
||||
case 2048:
|
||||
return plugin.KeySpecRSA2048, nil
|
||||
return KeySpecRSA2048, nil
|
||||
case 3072:
|
||||
return plugin.KeySpecRSA3072, nil
|
||||
return KeySpecRSA3072, nil
|
||||
case 4096:
|
||||
return plugin.KeySpecRSA4096, nil
|
||||
return KeySpecRSA4096, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("invalid KeySpec %q", k)
|
||||
}
|
||||
|
||||
// DecodeKeySpec parses keySpec name to a signature.keySpec type.
|
||||
func DecodeKeySpec(k plugin.KeySpec) (keySpec signature.KeySpec, err error) {
|
||||
func DecodeKeySpec(k KeySpec) (keySpec signature.KeySpec, err error) {
|
||||
switch k {
|
||||
case plugin.KeySpecRSA2048:
|
||||
case KeySpecRSA2048:
|
||||
keySpec.Size = 2048
|
||||
keySpec.Type = signature.KeyTypeRSA
|
||||
case plugin.KeySpecRSA3072:
|
||||
case KeySpecRSA3072:
|
||||
keySpec.Size = 3072
|
||||
keySpec.Type = signature.KeyTypeRSA
|
||||
case plugin.KeySpecRSA4096:
|
||||
case KeySpecRSA4096:
|
||||
keySpec.Size = 4096
|
||||
keySpec.Type = signature.KeyTypeRSA
|
||||
case plugin.KeySpecEC256:
|
||||
case KeySpecEC256:
|
||||
keySpec.Size = 256
|
||||
keySpec.Type = signature.KeyTypeEC
|
||||
case plugin.KeySpecEC384:
|
||||
case KeySpecEC384:
|
||||
keySpec.Size = 384
|
||||
keySpec.Type = signature.KeyTypeEC
|
||||
case plugin.KeySpecEC521:
|
||||
case KeySpecEC521:
|
||||
keySpec.Size = 521
|
||||
keySpec.Type = signature.KeyTypeEC
|
||||
default:
|
||||
|
@ -95,104 +88,92 @@ func DecodeKeySpec(k plugin.KeySpec) (keySpec signature.KeySpec, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// HashAlgorithm is the type of hash algorithm.
|
||||
//
|
||||
// Deprecated: HashAlgorithm exists for historical compatibility and should not be used.
|
||||
// To access HashAlgorithm, use the notation-plugin-framework-go's [plugin.HashAlgorithm] type.
|
||||
type HashAlgorithm = plugin.HashAlgorithm
|
||||
// HashAlgorithm is the type of a hash algorithm.
|
||||
type HashAlgorithm string
|
||||
|
||||
// one of the following supported hash algorithm names.
|
||||
//
|
||||
// Deprecated: HashAlgorithm exists for historical compatibility and should not be used.
|
||||
// To access HashAlgorithm, use the notation-plugin-framework-go's [plugin.HashAlgorithm] type.
|
||||
//
|
||||
// [hash algorithm]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||
const (
|
||||
HashAlgorithmSHA256 = plugin.HashAlgorithmSHA256
|
||||
HashAlgorithmSHA384 = plugin.HashAlgorithmSHA384
|
||||
HashAlgorithmSHA512 = plugin.HashAlgorithmSHA512
|
||||
HashAlgorithmSHA256 HashAlgorithm = "SHA-256"
|
||||
HashAlgorithmSHA384 HashAlgorithm = "SHA-384"
|
||||
HashAlgorithmSHA512 HashAlgorithm = "SHA-512"
|
||||
)
|
||||
|
||||
// HashAlgorithmFromKeySpec returns the name of hash function according to the spec.
|
||||
func HashAlgorithmFromKeySpec(k signature.KeySpec) (plugin.HashAlgorithm, error) {
|
||||
func HashAlgorithmFromKeySpec(k signature.KeySpec) (HashAlgorithm, error) {
|
||||
switch k.Type {
|
||||
case signature.KeyTypeEC:
|
||||
switch k.Size {
|
||||
case 256:
|
||||
return plugin.HashAlgorithmSHA256, nil
|
||||
return HashAlgorithmSHA256, nil
|
||||
case 384:
|
||||
return plugin.HashAlgorithmSHA384, nil
|
||||
return HashAlgorithmSHA384, nil
|
||||
case 521:
|
||||
return plugin.HashAlgorithmSHA512, nil
|
||||
return HashAlgorithmSHA512, nil
|
||||
}
|
||||
case signature.KeyTypeRSA:
|
||||
switch k.Size {
|
||||
case 2048:
|
||||
return plugin.HashAlgorithmSHA256, nil
|
||||
return HashAlgorithmSHA256, nil
|
||||
case 3072:
|
||||
return plugin.HashAlgorithmSHA384, nil
|
||||
return HashAlgorithmSHA384, nil
|
||||
case 4096:
|
||||
return plugin.HashAlgorithmSHA512, nil
|
||||
return HashAlgorithmSHA512, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("invalid KeySpec %q", k)
|
||||
}
|
||||
|
||||
// SignatureAlgorithm is the type of signature algorithm
|
||||
//
|
||||
// Deprecated: SignatureAlgorithm exists for historical compatibility and should not be used.
|
||||
// To access SignatureAlgorithm, use the notation-plugin-framework-go's [plugin.SignatureAlgorithm] type.
|
||||
type SignatureAlgorithm = plugin.SignatureAlgorithm
|
||||
type SignatureAlgorithm string
|
||||
|
||||
// one of the following supported [signing algorithm] names.
|
||||
// one of the following supported signing algorithm names.
|
||||
//
|
||||
// Deprecated: SignatureAlgorithm exists for historical compatibility and should not be used.
|
||||
// To access SignatureAlgorithm, use the notation-plugin-framework-go's [plugin.SignatureAlgorithm] type.
|
||||
//
|
||||
// [signing algorithm]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||
const (
|
||||
SignatureAlgorithmECDSA_SHA256 = plugin.SignatureAlgorithmECDSA_SHA256
|
||||
SignatureAlgorithmECDSA_SHA384 = plugin.SignatureAlgorithmECDSA_SHA384
|
||||
SignatureAlgorithmECDSA_SHA512 = plugin.SignatureAlgorithmECDSA_SHA512
|
||||
SignatureAlgorithmRSASSA_PSS_SHA256 = plugin.SignatureAlgorithmRSASSA_PSS_SHA256
|
||||
SignatureAlgorithmRSASSA_PSS_SHA384 = plugin.SignatureAlgorithmRSASSA_PSS_SHA384
|
||||
SignatureAlgorithmRSASSA_PSS_SHA512 = plugin.SignatureAlgorithmRSASSA_PSS_SHA512
|
||||
SignatureAlgorithmECDSA_SHA256 SignatureAlgorithm = "ECDSA-SHA-256"
|
||||
SignatureAlgorithmECDSA_SHA384 SignatureAlgorithm = "ECDSA-SHA-384"
|
||||
SignatureAlgorithmECDSA_SHA512 SignatureAlgorithm = "ECDSA-SHA-512"
|
||||
SignatureAlgorithmRSASSA_PSS_SHA256 SignatureAlgorithm = "RSASSA-PSS-SHA-256"
|
||||
SignatureAlgorithmRSASSA_PSS_SHA384 SignatureAlgorithm = "RSASSA-PSS-SHA-384"
|
||||
SignatureAlgorithmRSASSA_PSS_SHA512 SignatureAlgorithm = "RSASSA-PSS-SHA-512"
|
||||
)
|
||||
|
||||
// EncodeSigningAlgorithm returns the signing algorithm name of an algorithm
|
||||
// according to the spec.
|
||||
func EncodeSigningAlgorithm(alg signature.Algorithm) (plugin.SignatureAlgorithm, error) {
|
||||
func EncodeSigningAlgorithm(alg signature.Algorithm) (SignatureAlgorithm, error) {
|
||||
switch alg {
|
||||
case signature.AlgorithmES256:
|
||||
return plugin.SignatureAlgorithmECDSA_SHA256, nil
|
||||
return SignatureAlgorithmECDSA_SHA256, nil
|
||||
case signature.AlgorithmES384:
|
||||
return plugin.SignatureAlgorithmECDSA_SHA384, nil
|
||||
return SignatureAlgorithmECDSA_SHA384, nil
|
||||
case signature.AlgorithmES512:
|
||||
return plugin.SignatureAlgorithmECDSA_SHA512, nil
|
||||
return SignatureAlgorithmECDSA_SHA512, nil
|
||||
case signature.AlgorithmPS256:
|
||||
return plugin.SignatureAlgorithmRSASSA_PSS_SHA256, nil
|
||||
return SignatureAlgorithmRSASSA_PSS_SHA256, nil
|
||||
case signature.AlgorithmPS384:
|
||||
return plugin.SignatureAlgorithmRSASSA_PSS_SHA384, nil
|
||||
return SignatureAlgorithmRSASSA_PSS_SHA384, nil
|
||||
case signature.AlgorithmPS512:
|
||||
return plugin.SignatureAlgorithmRSASSA_PSS_SHA512, nil
|
||||
return SignatureAlgorithmRSASSA_PSS_SHA512, nil
|
||||
}
|
||||
return "", fmt.Errorf("invalid algorithm %q", alg)
|
||||
}
|
||||
|
||||
// DecodeSigningAlgorithm parses the signing algorithm name from a given string.
|
||||
func DecodeSigningAlgorithm(raw plugin.SignatureAlgorithm) (signature.Algorithm, error) {
|
||||
func DecodeSigningAlgorithm(raw SignatureAlgorithm) (signature.Algorithm, error) {
|
||||
switch raw {
|
||||
case plugin.SignatureAlgorithmECDSA_SHA256:
|
||||
case SignatureAlgorithmECDSA_SHA256:
|
||||
return signature.AlgorithmES256, nil
|
||||
case plugin.SignatureAlgorithmECDSA_SHA384:
|
||||
case SignatureAlgorithmECDSA_SHA384:
|
||||
return signature.AlgorithmES384, nil
|
||||
case plugin.SignatureAlgorithmECDSA_SHA512:
|
||||
case SignatureAlgorithmECDSA_SHA512:
|
||||
return signature.AlgorithmES512, nil
|
||||
case plugin.SignatureAlgorithmRSASSA_PSS_SHA256:
|
||||
case SignatureAlgorithmRSASSA_PSS_SHA256:
|
||||
return signature.AlgorithmPS256, nil
|
||||
case plugin.SignatureAlgorithmRSASSA_PSS_SHA384:
|
||||
case SignatureAlgorithmRSASSA_PSS_SHA384:
|
||||
return signature.AlgorithmPS384, nil
|
||||
case plugin.SignatureAlgorithmRSASSA_PSS_SHA512:
|
||||
case SignatureAlgorithmRSASSA_PSS_SHA512:
|
||||
return signature.AlgorithmPS512, nil
|
||||
}
|
||||
return 0, errors.New("unknown signing algorithm")
|
||||
|
|
|
@ -228,7 +228,7 @@ func TestDecodeKeySpec(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "Unsupported key spec",
|
||||
raw: "unsupported",
|
||||
raw: "unsuppored",
|
||||
expected: signature.KeySpec{},
|
||||
expectErr: true,
|
||||
},
|
||||
|
|
|
@ -17,62 +17,48 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
// Deprecated: ErrorCode exists for historical compatibility and should not be used.
|
||||
// To access ErrorCode, use the notation-plugin-framework-go's plugin.ErrorCode type.
|
||||
type ErrorCode = plugin.ErrorCode
|
||||
type ErrorCode string
|
||||
|
||||
const (
|
||||
// ErrorCodeValidation is used when any of the required request fields is empty ormalformed/invalid.
|
||||
//
|
||||
// Deprecated: ErrorCodeValidation exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeValidation, use the notation-plugin-framework-go's [plugin.ErrorCodeValidation].
|
||||
ErrorCodeValidation = plugin.ErrorCodeValidation
|
||||
// Any of the required request fields was empty,
|
||||
// or a value was malformed/invalid.
|
||||
ErrorCodeValidation ErrorCode = "VALIDATION_ERROR"
|
||||
|
||||
// ErrorCodeUnsupportedContractVersion is used when when the contract version used in the request is unsupported.
|
||||
//
|
||||
// Deprecated: ErrorCodeUnsupportedContractVersion exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeUnsupportedContractVersion, use the notation-plugin-framework-go's [plugin.ErrorCodeUnsupportedContractVersion].
|
||||
ErrorCodeUnsupportedContractVersion = plugin.ErrorCodeUnsupportedContractVersion
|
||||
// The contract version used in the request is unsupported.
|
||||
ErrorCodeUnsupportedContractVersion ErrorCode = "UNSUPPORTED_CONTRACT_VERSION"
|
||||
|
||||
// ErrorCodeAccessDenied is used when user doesn't have required permission to access the key.
|
||||
//
|
||||
// Deprecated: ErrorCodeAccessDenied exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeAccessDenied, use the notation-plugin-framework-go's [plugin.ErrorCodeAccessDenied].
|
||||
ErrorCodeAccessDenied = plugin.ErrorCodeAccessDenied
|
||||
// Authentication/authorization error to use given key.
|
||||
ErrorCodeAccessDenied ErrorCode = "ACCESS_DENIED"
|
||||
|
||||
// ErrorCodeTimeout is used when an operation to generate signature timed out and can be retried by Notation.
|
||||
//
|
||||
// Deprecated: ErrorCodeTimeout exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeTimeout, use the notation-plugin-framework-go's [plugin.ErrorCodeTimeout].
|
||||
ErrorCodeTimeout = plugin.ErrorCodeTimeout
|
||||
|
||||
// ErrorCodeThrottled is used when an operation to generate signature was throttles
|
||||
// The operation to generate signature timed out
|
||||
// and can be retried by Notation.
|
||||
//
|
||||
// Deprecated: ErrorCodeThrottled exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeThrottled, use the notation-plugin-framework-go's [plugin.ErrorCodeThrottled].
|
||||
ErrorCodeThrottled = plugin.ErrorCodeThrottled
|
||||
ErrorCodeTimeout ErrorCode = "TIMEOUT"
|
||||
|
||||
// ErrorCodeGeneric is used when an general error occurred that does not fall into any categories.
|
||||
//
|
||||
// Deprecated: ErrorCodeGeneric exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeGeneric, use the notation-plugin-framework-go's [plugin.ErrorCodeGeneric].
|
||||
ErrorCodeGeneric = plugin.ErrorCodeGeneric
|
||||
// The operation to generate signature was throttles
|
||||
// and can be retried by Notation.
|
||||
ErrorCodeThrottled ErrorCode = "THROTTLED"
|
||||
|
||||
// Any general error that does not fall into any categories.
|
||||
ErrorCodeGeneric ErrorCode = "ERROR"
|
||||
)
|
||||
|
||||
type jsonErr struct {
|
||||
Code ErrorCode `json:"errorCode"`
|
||||
Message string `json:"errorMessage,omitempty"`
|
||||
Metadata map[string]string `json:"errorMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// RequestError is the common error response for any request.
|
||||
type RequestError struct {
|
||||
Code plugin.ErrorCode
|
||||
Code ErrorCode
|
||||
Err error
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
func (e RequestError) Error() string {
|
||||
return fmt.Sprintf("%v", e.Err)
|
||||
return fmt.Sprintf("%s: %v", e.Code, e.Err)
|
||||
}
|
||||
|
||||
func (e RequestError) Unwrap() error {
|
||||
|
@ -97,19 +83,19 @@ func (e RequestError) MarshalJSON() ([]byte, error) {
|
|||
if e.Err != nil {
|
||||
msg = e.Err.Error()
|
||||
}
|
||||
return json.Marshal(plugin.Error{ErrCode: e.Code, Message: msg, Metadata: e.Metadata})
|
||||
return json.Marshal(jsonErr{e.Code, msg, e.Metadata})
|
||||
}
|
||||
|
||||
func (e *RequestError) UnmarshalJSON(data []byte) error {
|
||||
var tmp plugin.Error
|
||||
var tmp jsonErr
|
||||
err := json.Unmarshal(data, &tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tmp.ErrCode == "" && tmp.Message == "" && tmp.Metadata == nil {
|
||||
if tmp.Code == "" && tmp.Message == "" && tmp.Metadata == nil {
|
||||
return errors.New("incomplete json")
|
||||
}
|
||||
*e = RequestError{Code: tmp.ErrCode, Metadata: tmp.Metadata}
|
||||
*e = RequestError{Code: tmp.Code, Metadata: tmp.Metadata}
|
||||
if tmp.Message != "" {
|
||||
e.Err = errors.New(tmp.Message)
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import (
|
|||
|
||||
func TestRequestError_Error(t *testing.T) {
|
||||
err := RequestError{Code: ErrorCodeAccessDenied, Err: errors.New("an error")}
|
||||
want := "an error"
|
||||
want := string(ErrorCodeAccessDenied) + ": an error"
|
||||
if got := err.Error(); got != want {
|
||||
t.Errorf("RequestError.Error() = %v, want %v", got, want)
|
||||
}
|
||||
|
|
|
@ -13,16 +13,37 @@
|
|||
|
||||
package proto
|
||||
|
||||
import "github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
// GetMetadataRequest contains the parameters passed in a get-plugin-metadata
|
||||
// request.
|
||||
type GetMetadataRequest struct {
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
|
||||
// GetMetadataRequest contains the parameters passed in a get-plugin-metadata request.
|
||||
//
|
||||
// Deprecated: GetMetadataRequest exists for historical compatibility and should not be used.
|
||||
// To access GetMetadataRequest, use the notation-plugin-framework-go's [plugin.GetMetadataRequest] type.
|
||||
type GetMetadataRequest = plugin.GetMetadataRequest
|
||||
func (GetMetadataRequest) Command() Command {
|
||||
return CommandGetMetadata
|
||||
}
|
||||
|
||||
// GetMetadataResponse provided by the plugin.
|
||||
//
|
||||
// Deprecated: GetMetadataResponse exists for historical compatibility and should not be used.
|
||||
// To access GetMetadataResponse, use the notation-plugin-framework-go's [plugin.GetMetadataResponse] type.
|
||||
type GetMetadataResponse = plugin.GetMetadataResponse
|
||||
type GetMetadataResponse struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
SupportedContractVersions []string `json:"supportedContractVersions"`
|
||||
Capabilities []Capability `json:"capabilities"`
|
||||
}
|
||||
|
||||
// HasCapability return true if the metadata states that the
|
||||
// capability is supported.
|
||||
// Returns true if capability is empty.
|
||||
func (resp *GetMetadataResponse) HasCapability(capability Capability) bool {
|
||||
if capability == "" {
|
||||
return true
|
||||
}
|
||||
for _, c := range resp.Capabilities {
|
||||
if c == capability {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -15,106 +15,64 @@
|
|||
// and notation external plugin.
|
||||
package proto
|
||||
|
||||
import "github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
|
||||
// Prefix is the prefix required on all plugin binary names.
|
||||
//
|
||||
// Deprecated: Prefix exists for historical compatibility and should not be used.
|
||||
// To access Prefix, use the notation-plugin-framework-go's [plugin.BinaryPrefix] type.
|
||||
const Prefix = plugin.BinaryPrefix
|
||||
const Prefix = "notation-"
|
||||
|
||||
// ContractVersion is the <major>.<minor> version of the plugin contract.
|
||||
//
|
||||
// Deprecated: ContractVersion exists for historical compatibility and should not be used.
|
||||
// To access ContractVersion, use the notation-plugin-framework-go's [plugin.ContractVersion] type.
|
||||
const ContractVersion = plugin.ContractVersion
|
||||
const ContractVersion = "1.0"
|
||||
|
||||
// Command is a CLI command available in the plugin contract.
|
||||
//
|
||||
// Deprecated: Command exists for historical compatibility and should not be used.
|
||||
// To access Command, use the notation-plugin-framework-go's [plugin.Command] type.
|
||||
type Command = plugin.Command
|
||||
type Command string
|
||||
|
||||
// Request defines a plugin request, which is always associated to a command.
|
||||
//
|
||||
// Deprecated: Request exists for historical compatibility and should not be used.
|
||||
// To access Request, use the notation-plugin-framework-go's [plugin.Request] type.
|
||||
type Request = plugin.Request
|
||||
type Request interface {
|
||||
Command() Command
|
||||
}
|
||||
|
||||
const (
|
||||
// CommandGetMetadata is the name of the plugin command
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
//
|
||||
// Deprecated: CommandGetMetadata exists for historical compatibility and should not be used.
|
||||
// To access CommandGetMetadata, use the notation-plugin-framework-go's [plugin.CommandGetMetadata].
|
||||
CommandGetMetadata = plugin.CommandGetMetadata
|
||||
CommandGetMetadata Command = "get-plugin-metadata"
|
||||
|
||||
// CommandDescribeKey is the name of the plugin command
|
||||
// which must be supported by every plugin that has the
|
||||
// SIGNATURE_GENERATOR.RAW capability.
|
||||
//
|
||||
// Deprecated: CommandDescribeKey exists for historical compatibility and should not be used.
|
||||
// To access CommandDescribeKey, use the notation-plugin-framework-go's [plugin.CommandDescribeKey].
|
||||
CommandDescribeKey = plugin.CommandDescribeKey
|
||||
CommandDescribeKey Command = "describe-key"
|
||||
|
||||
// CommandGenerateSignature is the name of the plugin command
|
||||
// which must be supported by every plugin that has the
|
||||
// SIGNATURE_GENERATOR.RAW capability.
|
||||
//
|
||||
// Deprecated: CommandGenerateSignature exists for historical compatibility and should not be used.
|
||||
// To access CommandGenerateSignature, use the notation-plugin-framework-go's [plugin.CommandGenerateSignature].
|
||||
CommandGenerateSignature = plugin.CommandGenerateSignature
|
||||
CommandGenerateSignature Command = "generate-signature"
|
||||
|
||||
// CommandGenerateEnvelope is the name of the plugin command
|
||||
// which must be supported by every plugin that has the
|
||||
// SIGNATURE_GENERATOR.ENVELOPE capability.
|
||||
//
|
||||
// Deprecated: CommandGenerateEnvelope exists for historical compatibility and should not be used.
|
||||
// To access CommandGenerateEnvelope, use the notation-plugin-framework-go's [plugin.CommandGenerateEnvelope].
|
||||
CommandGenerateEnvelope = plugin.CommandGenerateEnvelope
|
||||
CommandGenerateEnvelope Command = "generate-envelope"
|
||||
|
||||
// CommandVerifySignature is the name of the plugin command
|
||||
// which must be supported by every plugin that has
|
||||
// any SIGNATURE_VERIFIER.* capability
|
||||
//
|
||||
// Deprecated: CommandVerifySignature exists for historical compatibility and should not be used.
|
||||
// To access CommandVerifySignature, use the notation-plugin-framework-go's [plugin.CommandVerifySignature].
|
||||
CommandVerifySignature = plugin.CommandVerifySignature
|
||||
CommandVerifySignature Command = "verify-signature"
|
||||
)
|
||||
|
||||
// Capability is a feature available in the plugin contract.
|
||||
//
|
||||
// Deprecated: Capability exists for historical compatibility and should not be used.
|
||||
// To access Capability, use the notation-plugin-framework-go's [plugin.Capability] type.
|
||||
type Capability = plugin.Capability
|
||||
type Capability string
|
||||
|
||||
const (
|
||||
// CapabilitySignatureGenerator is the name of the capability
|
||||
// for a plugin to support generating raw signatures.
|
||||
//
|
||||
// Deprecated: CapabilitySignatureGenerator exists for historical compatibility and should not be used.
|
||||
// To access CapabilitySignatureGenerator, use the notation-plugin-framework-go's [plugin.CapabilitySignatureGenerator].
|
||||
CapabilitySignatureGenerator = plugin.CapabilitySignatureGenerator
|
||||
CapabilitySignatureGenerator Capability = "SIGNATURE_GENERATOR.RAW"
|
||||
|
||||
// CapabilityEnvelopeGenerator is the name of the capability
|
||||
// for a plugin to support generating envelope signatures.
|
||||
//
|
||||
// Deprecated: CapabilityEnvelopeGenerator exists for historical compatibility and should not be used.
|
||||
// To access CapabilityEnvelopeGenerator, use the notation-plugin-framework-go's [plugin.CapabilityEnvelopeGenerator].
|
||||
CapabilityEnvelopeGenerator = plugin.CapabilityEnvelopeGenerator
|
||||
CapabilityEnvelopeGenerator Capability = "SIGNATURE_GENERATOR.ENVELOPE"
|
||||
|
||||
// CapabilityTrustedIdentityVerifier is the name of the
|
||||
// capability for a plugin to support verifying trusted identities.
|
||||
//
|
||||
// Deprecated: CapabilityTrustedIdentityVerifier exists for historical compatibility and should not be used.
|
||||
// To access CapabilityTrustedIdentityVerifier, use the notation-plugin-framework-go's [plugin.CapabilityTrustedIdentityVerifier].
|
||||
CapabilityTrustedIdentityVerifier = plugin.CapabilityTrustedIdentityVerifier
|
||||
CapabilityTrustedIdentityVerifier Capability = "SIGNATURE_VERIFIER.TRUSTED_IDENTITY"
|
||||
|
||||
// CapabilityRevocationCheckVerifier is the name of the
|
||||
// capability for a plugin to support verifying revocation checks.
|
||||
//
|
||||
// Deprecated: CapabilityRevocationCheckVerifier exists for historical compatibility and should not be used.
|
||||
// To access CapabilityRevocationCheckVerifier, use the notation-plugin-framework-go's [plugin.CapabilityRevocationCheckVerifier].
|
||||
CapabilityRevocationCheckVerifier = plugin.CapabilityRevocationCheckVerifier
|
||||
CapabilityRevocationCheckVerifier Capability = "SIGNATURE_VERIFIER.REVOCATION_CHECK"
|
||||
)
|
||||
|
|
|
@ -13,42 +13,72 @@
|
|||
|
||||
package proto
|
||||
|
||||
import "github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
|
||||
// DescribeKeyRequest contains the parameters passed in a describe-key request.
|
||||
//
|
||||
// Deprecated: DescribeKeyRequest exists for historical compatibility and should not be used.
|
||||
// To access DescribeKeyRequest, use the notation-plugin-framework-go's [plugin.DescribeKeyRequest] type.
|
||||
type DescribeKeyRequest = plugin.DescribeKeyRequest
|
||||
type DescribeKeyRequest struct {
|
||||
ContractVersion string `json:"contractVersion"`
|
||||
KeyID string `json:"keyId"`
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
|
||||
func (DescribeKeyRequest) Command() Command {
|
||||
return CommandDescribeKey
|
||||
}
|
||||
|
||||
// DescribeKeyResponse is the response of a describe-key request.
|
||||
//
|
||||
// Deprecated: DescribeKeyResponse exists for historical compatibility and should not be used.
|
||||
// To access DescribeKeyResponse, use the notation-plugin-framework-go's [plugin.DescribeKeyResponse] type.
|
||||
type DescribeKeyResponse = plugin.DescribeKeyResponse
|
||||
type DescribeKeyResponse struct {
|
||||
// The same key id as passed in the request.
|
||||
KeyID string `json:"keyId"`
|
||||
|
||||
// One of following supported key types:
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||
KeySpec KeySpec `json:"keySpec"`
|
||||
}
|
||||
|
||||
// GenerateSignatureRequest contains the parameters passed in a
|
||||
// generate-signature request.
|
||||
//
|
||||
// Deprecated: GenerateSignatureRequest exists for historical compatibility and should not be used.
|
||||
// To access GenerateSignatureRequest, use the notation-plugin-framework-go's [plugin.GenerateSignatureRequest] type.
|
||||
type GenerateSignatureRequest = plugin.GenerateSignatureRequest
|
||||
type GenerateSignatureRequest struct {
|
||||
ContractVersion string `json:"contractVersion"`
|
||||
KeyID string `json:"keyId"`
|
||||
KeySpec KeySpec `json:"keySpec"`
|
||||
Hash HashAlgorithm `json:"hashAlgorithm"`
|
||||
Payload []byte `json:"payload"`
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
|
||||
func (GenerateSignatureRequest) Command() Command {
|
||||
return CommandGenerateSignature
|
||||
}
|
||||
|
||||
// GenerateSignatureResponse is the response of a generate-signature request.
|
||||
//
|
||||
// Deprecated: GenerateSignatureResponse exists for historical compatibility and should not be used.
|
||||
// To access GenerateSignatureResponse, use the notation-plugin-framework-go's [plugin.GenerateSignatureResponse] type.
|
||||
type GenerateSignatureResponse = plugin.GenerateSignatureResponse
|
||||
type GenerateSignatureResponse struct {
|
||||
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.
|
||||
CertificateChain [][]byte `json:"certificateChain"`
|
||||
}
|
||||
|
||||
// GenerateEnvelopeRequest contains the parameters passed in a generate-envelope
|
||||
// request.
|
||||
//
|
||||
// Deprecated: GenerateEnvelopeRequest exists for historical compatibility and should not be used.
|
||||
// To access GenerateEnvelopeRequest, use the notation-plugin-framework-go's [plugin.GenerateEnvelopeRequest] type.
|
||||
type GenerateEnvelopeRequest = plugin.GenerateEnvelopeRequest
|
||||
type GenerateEnvelopeRequest struct {
|
||||
ContractVersion string `json:"contractVersion"`
|
||||
KeyID string `json:"keyId"`
|
||||
PayloadType string `json:"payloadType"`
|
||||
SignatureEnvelopeType string `json:"signatureEnvelopeType"`
|
||||
Payload []byte `json:"payload"`
|
||||
ExpiryDurationInSeconds uint64 `json:"expiryDurationInSeconds,omitempty"`
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
|
||||
func (GenerateEnvelopeRequest) Command() Command {
|
||||
return CommandGenerateEnvelope
|
||||
}
|
||||
|
||||
// GenerateEnvelopeResponse is the response of a generate-envelope request.
|
||||
//
|
||||
// Deprecated: GenerateEnvelopeResponse exists for historical compatibility and should not be used.
|
||||
// To access GenerateEnvelopeResponse, use the notation-plugin-framework-go's [plugin.GenerateEnvelopeResponse] type.
|
||||
type GenerateEnvelopeResponse = plugin.GenerateEnvelopeResponse
|
||||
type GenerateEnvelopeResponse struct {
|
||||
SignatureEnvelope []byte `json:"signatureEnvelope"`
|
||||
SignatureEnvelopeType string `json:"signatureEnvelopeType"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
|
|
|
@ -13,44 +13,52 @@
|
|||
|
||||
package proto
|
||||
|
||||
import (
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
import "time"
|
||||
|
||||
// VerifySignatureRequest contains the parameters passed in a verify-signature
|
||||
// request.
|
||||
//
|
||||
// Deprecated: VerifySignatureRequest exists for historical compatibility and should not be used.
|
||||
// To access VerifySignatureRequest, use the notation-plugin-framework-go'[s plugin.VerifySignatureRequest] type.
|
||||
type VerifySignatureRequest = plugin.VerifySignatureRequest
|
||||
type VerifySignatureRequest struct {
|
||||
ContractVersion string `json:"contractVersion"`
|
||||
Signature Signature `json:"signature"`
|
||||
TrustPolicy TrustPolicy `json:"trustPolicy"`
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
|
||||
func (VerifySignatureRequest) Command() Command {
|
||||
return CommandVerifySignature
|
||||
}
|
||||
|
||||
// Signature represents a signature pulled from the envelope
|
||||
//
|
||||
// Deprecated: Signature exists for historical compatibility and should not be used.
|
||||
// To access Signature, use the notation-plugin-framework-go's [plugin.Signature] type.
|
||||
type Signature = plugin.Signature
|
||||
type Signature struct {
|
||||
CriticalAttributes CriticalAttributes `json:"criticalAttributes"`
|
||||
UnprocessedAttributes []string `json:"unprocessedAttributes"`
|
||||
CertificateChain [][]byte `json:"certificateChain"`
|
||||
}
|
||||
|
||||
// CriticalAttributes contains all Notary Project defined critical
|
||||
// attributes and their values in the signature envelope
|
||||
//
|
||||
// Deprecated: CriticalAttributes exists for historical compatibility and should not be used.
|
||||
// To access CriticalAttributes, use the notation-plugin-framework-go's [plugin.CriticalAttributes] type.
|
||||
type CriticalAttributes = plugin.CriticalAttributes
|
||||
type CriticalAttributes struct {
|
||||
ContentType string `json:"contentType"`
|
||||
SigningScheme string `json:"signingScheme"`
|
||||
Expiry *time.Time `json:"expiry,omitempty"`
|
||||
AuthenticSigningTime *time.Time `json:"authenticSigningTime,omitempty"`
|
||||
ExtendedAttributes map[string]interface{} `json:"extendedAttributes,omitempty"`
|
||||
}
|
||||
|
||||
// TrustPolicy represents trusted identities that sign the artifacts
|
||||
//
|
||||
// Deprecated: TrustPolicy exists for historical compatibility and should not be used.
|
||||
// To access TrustPolicy, use the notation-plugin-framework-go's [plugin.TrustPolicy] type.
|
||||
type TrustPolicy = plugin.TrustPolicy
|
||||
type TrustPolicy struct {
|
||||
TrustedIdentities []string `json:"trustedIdentities"`
|
||||
SignatureVerification []Capability `json:"signatureVerification"`
|
||||
}
|
||||
|
||||
// VerifySignatureResponse is the response of a verify-signature request.
|
||||
//
|
||||
// Deprecated: VerifySignatureResponse exists for historical compatibility and should not be used.
|
||||
// To access VerifySignatureResponse, use the notation-plugin-framework-go's [plugin.VerifySignatureResponse] type.
|
||||
type VerifySignatureResponse = plugin.VerifySignatureResponse
|
||||
type VerifySignatureResponse struct {
|
||||
VerificationResults map[Capability]*VerificationResult `json:"verificationResults"`
|
||||
ProcessedAttributes []interface{} `json:"processedAttributes"`
|
||||
}
|
||||
|
||||
// VerificationResult is the result of a verification performed by the plugin.
|
||||
//
|
||||
// Deprecated: VerificationResult exists for historical compatibility and should not be used.
|
||||
// To access VerificationResult, use the notation-plugin-framework-go's [plugin.VerificationResult] type.
|
||||
type VerificationResult = plugin.VerificationResult
|
||||
// VerificationResult is the result of a verification performed by the plugin
|
||||
type VerificationResult struct {
|
||||
Success bool `json:"success"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
|
|
@ -14,5 +14,5 @@
|
|||
package registry
|
||||
|
||||
// ArtifactTypeNotation specifies the artifact type for a notation object.
|
||||
// spec: https://github.com/notaryproject/specifications/blob/v1.1.0/specs/signature-specification.md#signature
|
||||
// spec: https://github.com/notaryproject/notaryproject/blob/efc828223710f99ab9639d2d0f72d59036a8e80c/specs/signature-specification.md#storage
|
||||
const ArtifactTypeNotation = "application/vnd.cncf.notary.signature"
|
||||
|
|
|
@ -19,7 +19,6 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
"github.com/notaryproject/notation-go/registry/internal/artifactspec"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2"
|
||||
|
@ -33,17 +32,17 @@ const (
|
|||
maxManifestSizeLimit = 4 * 1024 * 1024 // 4 MiB
|
||||
)
|
||||
|
||||
// RepositoryOptions provides user options when creating a [Repository]
|
||||
// RepositoryOptions provides user options when creating a Repository
|
||||
// it is kept for future extensibility
|
||||
type RepositoryOptions struct{}
|
||||
|
||||
// repositoryClient implements [Repository]
|
||||
// repositoryClient implements Repository
|
||||
type repositoryClient struct {
|
||||
oras.GraphTarget
|
||||
RepositoryOptions
|
||||
}
|
||||
|
||||
// NewRepository returns a new [Repository].
|
||||
// NewRepository returns a new Repository.
|
||||
// Known implementations of oras.GraphTarget:
|
||||
// - [remote.Repository](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote#Repository)
|
||||
// - [oci.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/oci#Store)
|
||||
|
@ -53,7 +52,7 @@ func NewRepository(target oras.GraphTarget) Repository {
|
|||
}
|
||||
}
|
||||
|
||||
// NewRepositoryWithOptions returns a new [Repository] with user specified
|
||||
// NewRepositoryWithOptions returns a new Repository with user specified
|
||||
// options.
|
||||
func NewRepositoryWithOptions(target oras.GraphTarget, opts RepositoryOptions) Repository {
|
||||
return &repositoryClient{
|
||||
|
@ -62,7 +61,7 @@ func NewRepositoryWithOptions(target oras.GraphTarget, opts RepositoryOptions) R
|
|||
}
|
||||
}
|
||||
|
||||
// NewOCIRepository returns a new [Repository] with oci.Store as
|
||||
// NewOCIRepository returns a new Repository with oci.Store as
|
||||
// its oras.GraphTarget. `path` denotes directory path to the target OCI layout.
|
||||
func NewOCIRepository(path string, opts RepositoryOptions) (Repository, error) {
|
||||
fileInfo, err := os.Stat(path)
|
||||
|
@ -93,6 +92,7 @@ func (c *repositoryClient) ListSignatures(ctx context.Context, desc ocispec.Desc
|
|||
if repo, ok := c.GraphTarget.(registry.ReferrerLister); ok {
|
||||
return repo.Referrers(ctx, desc, ArtifactTypeNotation, fn)
|
||||
}
|
||||
|
||||
signatureManifests, err := signatureReferrers(ctx, c.GraphTarget, desc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get referrers during ListSignatures due to %w", err)
|
||||
|
@ -110,6 +110,7 @@ func (c *repositoryClient) FetchSignatureBlob(ctx context.Context, desc ocispec.
|
|||
if sigBlobDesc.Size > maxBlobSizeLimit {
|
||||
return nil, ocispec.Descriptor{}, fmt.Errorf("signature blob too large: %d bytes", sigBlobDesc.Size)
|
||||
}
|
||||
|
||||
var fetcher content.Fetcher = c.GraphTarget
|
||||
if repo, ok := c.GraphTarget.(registry.Repository); ok {
|
||||
fetcher = repo.Blobs()
|
||||
|
@ -162,7 +163,6 @@ func (c *repositoryClient) getSignatureBlobDesc(ctx context.Context, sigManifest
|
|||
|
||||
// get the signature blob descriptor from signature manifest
|
||||
var signatureBlobs []ocispec.Descriptor
|
||||
|
||||
// OCI image manifest
|
||||
if sigManifestDesc.MediaType == ocispec.MediaTypeImageManifest {
|
||||
var sigManifest ocispec.Manifest
|
||||
|
@ -177,27 +177,28 @@ func (c *repositoryClient) getSignatureBlobDesc(ctx context.Context, sigManifest
|
|||
}
|
||||
signatureBlobs = sigManifest.Blobs
|
||||
}
|
||||
|
||||
if len(signatureBlobs) != 1 {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("signature manifest requries exactly one signature envelope blob, got %d", len(signatureBlobs))
|
||||
}
|
||||
|
||||
return signatureBlobs[0], nil
|
||||
}
|
||||
|
||||
// uploadSignatureManifest uploads the signature manifest to the registry
|
||||
func (c *repositoryClient) uploadSignatureManifest(ctx context.Context, subject, blobDesc ocispec.Descriptor, annotations map[string]string) (ocispec.Descriptor, error) {
|
||||
opts := oras.PackManifestOptions{
|
||||
opts := oras.PackOptions{
|
||||
Subject: &subject,
|
||||
ManifestAnnotations: annotations,
|
||||
Layers: []ocispec.Descriptor{blobDesc},
|
||||
PackImageManifest: true,
|
||||
}
|
||||
return oras.PackManifest(ctx, c.GraphTarget, oras.PackManifestVersion1_1, ArtifactTypeNotation, opts)
|
||||
|
||||
return oras.Pack(ctx, c.GraphTarget, ArtifactTypeNotation, []ocispec.Descriptor{blobDesc}, opts)
|
||||
}
|
||||
|
||||
// signatureReferrers returns referrer nodes of desc in target filtered by
|
||||
// the "application/vnd.cncf.notary.signature" artifact type
|
||||
func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
|
||||
var results []ocispec.Descriptor
|
||||
predecessors, err := target.Predecessors(ctx, desc)
|
||||
if err != nil {
|
||||
|
@ -213,7 +214,6 @@ func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var artifact artifactspec.Artifact
|
||||
if err := json.Unmarshal(fetched, &artifact); err != nil {
|
||||
return nil, err
|
||||
|
@ -221,10 +221,6 @@ func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage
|
|||
if artifact.Subject == nil || !content.Equal(*artifact.Subject, desc) {
|
||||
continue
|
||||
}
|
||||
if artifact.ArtifactType != ArtifactTypeNotation {
|
||||
// not a valid Notary Project signature
|
||||
continue
|
||||
}
|
||||
node.ArtifactType = artifact.ArtifactType
|
||||
node.Annotations = artifact.Annotations
|
||||
case ocispec.MediaTypeImageManifest:
|
||||
|
@ -235,7 +231,6 @@ func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var image ocispec.Manifest
|
||||
if err := json.Unmarshal(fetched, &image); err != nil {
|
||||
return nil, err
|
||||
|
@ -243,39 +238,15 @@ func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage
|
|||
if image.Subject == nil || !content.Equal(*image.Subject, desc) {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if image is a valid Notary Project signature
|
||||
switch image.ArtifactType {
|
||||
case ArtifactTypeNotation:
|
||||
// 1. artifactType is "application/vnd.cncf.notary.signature",
|
||||
// and config.mediaType is "application/vnd.oci.empty.v1+json"
|
||||
if image.Config.MediaType == ocispec.MediaTypeEmptyJSON {
|
||||
node.ArtifactType = image.ArtifactType
|
||||
} else {
|
||||
// not a valid Notary Project signature
|
||||
logger.Warnf("not a valid Notary Project signature with artifactType %q, but config.mediaType is %q", image.ArtifactType, image.Config.MediaType)
|
||||
continue
|
||||
}
|
||||
case "":
|
||||
// 2. artifacteType does not exist,
|
||||
// and config.mediaType is "application/vnd.cncf.notary.signature"
|
||||
if image.Config.MediaType == ArtifactTypeNotation {
|
||||
node.ArtifactType = image.Config.MediaType
|
||||
} else {
|
||||
// not a valid Notary Project signature
|
||||
continue
|
||||
}
|
||||
default:
|
||||
// not a valid Notary Project signature
|
||||
continue
|
||||
}
|
||||
node.ArtifactType = image.Config.MediaType
|
||||
node.Annotations = image.Annotations
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// add the node to results
|
||||
results = append(results, node)
|
||||
// only keep nodes of "application/vnd.cncf.notary.signature"
|
||||
if node.ArtifactType == ArtifactTypeNotation {
|
||||
results = append(results, node)
|
||||
}
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ package registry
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -24,26 +23,21 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/notaryproject/notation-go/internal/envelope"
|
||||
"github.com/notaryproject/notation-go/internal/mock/ocilayout"
|
||||
"github.com/notaryproject/notation-go/internal/slices"
|
||||
"github.com/notaryproject/notation-go/registry/internal/artifactspec"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/content/memory"
|
||||
"oras.land/oras-go/v2/content/oci"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
)
|
||||
|
||||
const (
|
||||
zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
emptyConfigDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
|
||||
validDigest = "6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b"
|
||||
validDigest2 = "1834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f2"
|
||||
invalidDigest = "invaliddigest"
|
||||
|
@ -67,7 +61,7 @@ const (
|
|||
{
|
||||
"Manifests": [
|
||||
{
|
||||
"MediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"MediaType": "application/vnd.oci.artifact.manifest.v1+json",
|
||||
"Digest": "sha256:cf2a0974295fc17b8351ef52abae2f40212e20e0359ea980ec5597bb0315347b",
|
||||
"Size": 620,
|
||||
"ArtifactType": "application/vnd.cncf.notary.signature"
|
||||
|
@ -135,13 +129,8 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
|
|||
"Docker-Content-Digest": {validDigestWithAlgo2},
|
||||
},
|
||||
}, nil
|
||||
case "/v2/test/blobs/" + emptyConfigDigest:
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte{})),
|
||||
}, nil
|
||||
case "/v2/test/manifests/" + invalidDigest:
|
||||
return &http.Response{}, errors.New(errMsg)
|
||||
return &http.Response{}, fmt.Errorf(errMsg)
|
||||
case "v2/test/manifest/" + validDigest2:
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
|
@ -165,39 +154,21 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
|
|||
},
|
||||
}, nil
|
||||
default:
|
||||
return &http.Response{}, errors.New(msg)
|
||||
return &http.Response{}, fmt.Errorf(msg)
|
||||
}
|
||||
case "/v2/test/referrers/":
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{ocispec.MediaTypeImageIndex},
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(validPage))),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(validPage))),
|
||||
Request: &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/v2/test/referrers/"},
|
||||
},
|
||||
}, nil
|
||||
case "/v2/test/referrers/" + validDigestWithAlgo:
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{ocispec.MediaTypeImageIndex},
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(validPage))),
|
||||
Request: &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/v2/test/referrers/" + validDigestWithAlgo},
|
||||
},
|
||||
}, nil
|
||||
case "/v2/test/referrers/" + zeroDigest:
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{ocispec.MediaTypeImageIndex},
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(validPageImage))),
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(validPageImage))),
|
||||
Request: &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/v2/test/referrers/" + zeroDigest},
|
||||
|
@ -211,17 +182,15 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
|
|||
default:
|
||||
_, digest, found := strings.Cut(req.URL.Path, "/v2/test/manifests/")
|
||||
if found && !slices.Contains(validDigestWithAlgoSlice, digest) {
|
||||
resp := &http.Response{
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusCreated,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(msg))),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{joseTag},
|
||||
"Oci-Subject": []string{validDigestWithAlgo},
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {joseTag},
|
||||
},
|
||||
}
|
||||
return resp, nil
|
||||
}, nil
|
||||
}
|
||||
return &http.Response{}, errors.New(errMsg)
|
||||
return &http.Response{}, fmt.Errorf(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -332,24 +301,16 @@ func TestListSignatures(t *testing.T) {
|
|||
reference: validReference,
|
||||
remoteClient: mockRemoteClient{},
|
||||
plainHttp: false,
|
||||
artifactManifestDesc: ocispec.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: validDigestWithAlgo,
|
||||
Size: 481,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
args := tt.args
|
||||
ref, err := registry.ParseReference(args.reference)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ref, _ := registry.ParseReference(args.reference)
|
||||
client := newRepositoryClient(args.remoteClient, ref, args.plainHttp)
|
||||
|
||||
err = client.ListSignatures(args.ctx, args.artifactManifestDesc, func(signatureManifests []ocispec.Descriptor) error {
|
||||
err := client.ListSignatures(args.ctx, args.artifactManifestDesc, func(signatureManifests []ocispec.Descriptor) error {
|
||||
if len(signatureManifests) != 1 {
|
||||
return fmt.Errorf("length of signatureManifests expected 1, got %d", len(signatureManifests))
|
||||
}
|
||||
|
@ -400,11 +361,6 @@ func TestPushSignature(t *testing.T) {
|
|||
signature: signature,
|
||||
ctx: context.Background(),
|
||||
remoteClient: mockRemoteClient{},
|
||||
subjectManifest: ocispec.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: validDigestWithAlgo,
|
||||
Size: 481,
|
||||
},
|
||||
annotations: map[string]string{
|
||||
envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]",
|
||||
},
|
||||
|
@ -469,6 +425,7 @@ func newRepositoryClientWithImageManifest(client remote.Client, ref registry.Ref
|
|||
}
|
||||
|
||||
var (
|
||||
ociLayoutPath = filepath.FromSlash("../internal/testdata/oci-layout")
|
||||
reference = "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
|
||||
expectedTargetDesc = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
|
@ -481,8 +438,8 @@ var (
|
|||
}
|
||||
expectedSignatureManifestDesc = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: "sha256:64300ad03f1dcd18136787363f3069c9598623221cbe76e3233d35266b7973d6",
|
||||
Size: 793,
|
||||
Digest: "sha256:baeaea44f55c94499b7e082bd3c98ad5ec40fdf23ef89cdf4e5db6b83e4f18f5",
|
||||
Size: 728,
|
||||
}
|
||||
expectedSignatureBlobDesc = ocispec.Descriptor{
|
||||
MediaType: joseTag,
|
||||
|
@ -491,23 +448,11 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
func TestOciLayoutRepositoryPushAndFetch(t *testing.T) {
|
||||
// create a temp OCI layout
|
||||
ociLayoutTestdataPath, err := filepath.Abs(filepath.Join("..", "internal", "testdata", "oci-layout"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get oci layout path: %v", err)
|
||||
}
|
||||
|
||||
newOCILayoutPath := t.TempDir()
|
||||
if err := ocilayout.Copy(ociLayoutTestdataPath, newOCILayoutPath, "v2"); err != nil {
|
||||
t.Fatalf("failed to create temp oci layout: %v", err)
|
||||
}
|
||||
repo, err := NewOCIRepository(newOCILayoutPath, RepositoryOptions{})
|
||||
func TestOciLayoutRepositoryResolveAndPush(t *testing.T) {
|
||||
repo, err := NewOCIRepository(ociLayoutPath, RepositoryOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create oci.Store as registry.Repository: %v", err)
|
||||
}
|
||||
|
||||
// test resolve
|
||||
targetDesc, err := repo.Resolve(context.Background(), reference)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to resolve reference: %v", err)
|
||||
|
@ -515,515 +460,59 @@ func TestOciLayoutRepositoryPushAndFetch(t *testing.T) {
|
|||
if !content.Equal(targetDesc, expectedTargetDesc) {
|
||||
t.Fatalf("failed to resolve reference. expected descriptor: %v, but got: %v", expectedTargetDesc, targetDesc)
|
||||
}
|
||||
|
||||
t.Run("oci layout push", func(t *testing.T) {
|
||||
signature, err := os.ReadFile(signaturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read signature: %v", err)
|
||||
}
|
||||
_, signatureManifestDesc, err := repo.PushSignature(context.Background(), joseTag, signature, targetDesc, annotations)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to push signature: %v", err)
|
||||
}
|
||||
if !content.Equal(expectedSignatureManifestDesc, signatureManifestDesc) {
|
||||
t.Fatalf("expected desc: %v, got: %v", expectedSignatureManifestDesc, signatureManifestDesc)
|
||||
}
|
||||
expectedAnnotations := map[string]string{
|
||||
envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]",
|
||||
ocispec.AnnotationCreated: "2023-03-14T08:10:02Z",
|
||||
}
|
||||
if !reflect.DeepEqual(expectedAnnotations, signatureManifestDesc.Annotations) {
|
||||
t.Fatalf("expected annotations: %v, but got: %v", expectedAnnotations, signatureManifestDesc.Annotations)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("oci layout fetch", func(t *testing.T) {
|
||||
err = repo.ListSignatures(context.Background(), targetDesc, func(signatureManifests []ocispec.Descriptor) error {
|
||||
if len(signatureManifests) == 0 {
|
||||
return fmt.Errorf("expected to find signature in the OCI layout folder, but got none")
|
||||
}
|
||||
var found bool
|
||||
for _, sigManifestDesc := range signatureManifests {
|
||||
if !content.Equal(sigManifestDesc, expectedSignatureManifestDesc) {
|
||||
continue
|
||||
}
|
||||
_, sigDesc, err := repo.FetchSignatureBlob(context.Background(), sigManifestDesc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch blob: %w", err)
|
||||
}
|
||||
if !content.Equal(expectedSignatureBlobDesc, sigDesc) {
|
||||
return fmt.Errorf("expected to get signature blob desc: %v, got: %v", expectedSignatureBlobDesc, sigDesc)
|
||||
}
|
||||
found = true
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("expected to find the signature with manifest desc: %v, but failed", expectedSignatureManifestDesc)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
signature, err := os.ReadFile(signaturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read signature: %v", err)
|
||||
}
|
||||
_, signatureManifestDesc, err := repo.PushSignature(context.Background(), joseTag, signature, targetDesc, annotations)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to push signature: %v", err)
|
||||
}
|
||||
if !content.Equal(expectedSignatureManifestDesc, signatureManifestDesc) {
|
||||
t.Fatalf("expected desc: %v, got: %v", expectedSignatureManifestDesc, signatureManifestDesc)
|
||||
}
|
||||
expectedAnnotations := map[string]string{
|
||||
envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]",
|
||||
ocispec.AnnotationCreated: "2023-03-14T08:10:02Z",
|
||||
}
|
||||
if !reflect.DeepEqual(expectedAnnotations, signatureManifestDesc.Annotations) {
|
||||
t.Fatalf("expected annotations: %v, but got: %v", expectedAnnotations, signatureManifestDesc.Annotations)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewRepository(t *testing.T) {
|
||||
target, err := oci.New(t.TempDir())
|
||||
func TestOciLayoutRepositoryListAndFetchBlob(t *testing.T) {
|
||||
repo, err := NewOCIRepository(ociLayoutPath, RepositoryOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create oci.Store as registry.Repository: %v", err)
|
||||
}
|
||||
repo := NewRepository(target)
|
||||
if repo == nil {
|
||||
t.Fatalf("failed to create repository")
|
||||
}
|
||||
repoClient, ok := repo.(*repositoryClient)
|
||||
if !ok {
|
||||
t.Fatalf("failed to create repositoryClient")
|
||||
}
|
||||
if target != repoClient.GraphTarget {
|
||||
t.Fatalf("expected target: %v, got: %v", target, repoClient.GraphTarget)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOCIRepositoryFailed(t *testing.T) {
|
||||
t.Run("os stat failed", func(t *testing.T) {
|
||||
_, err := NewOCIRepository("invalid-path", RepositoryOptions{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with invalid path")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("path is regular file", func(t *testing.T) {
|
||||
// create a regular file in the temp dir
|
||||
filePath := filepath.Join(t.TempDir(), "file")
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create file: %v", err)
|
||||
}
|
||||
file.Close()
|
||||
|
||||
_, err = NewOCIRepository(filePath, RepositoryOptions{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with regular file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no permission to create new path", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
// create a directory in the temp dir
|
||||
dirPath := filepath.Join(t.TempDir(), "dir")
|
||||
err := os.Mkdir(dirPath, 0000)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create dir: %v", err)
|
||||
}
|
||||
|
||||
_, err = NewOCIRepository(dirPath, RepositoryOptions{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with no permission to create new path")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// testStorage implements content.ReadOnlyGraphStorage
|
||||
type testStorage struct {
|
||||
store *memory.Store
|
||||
FetchError error
|
||||
FetchContent []byte
|
||||
PredecessorsError error
|
||||
PredecessorsDesc []ocispec.Descriptor
|
||||
}
|
||||
|
||||
func (s *testStorage) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error {
|
||||
return s.store.Push(ctx, expected, reader)
|
||||
}
|
||||
|
||||
func (s *testStorage) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
|
||||
if s.FetchError != nil {
|
||||
return nil, s.FetchError
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(s.FetchContent)), nil
|
||||
}
|
||||
|
||||
func (s *testStorage) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
|
||||
return s.store.Exists(ctx, target)
|
||||
}
|
||||
|
||||
func (s *testStorage) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
if s.PredecessorsError != nil {
|
||||
return nil, s.PredecessorsError
|
||||
}
|
||||
return s.PredecessorsDesc, nil
|
||||
}
|
||||
|
||||
func TestSignatureReferrers(t *testing.T) {
|
||||
t.Run("get predecessors failed", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsError: fmt.Errorf("failed to get predecessors"),
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with getting predecessors")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("artifact manifest exceds max blob size", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: validDigestWithAlgo2,
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 4*1024*1024 + 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: validDigestWithAlgo2,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with artifact manifest exceds max blob size")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest exceds max blob size", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: validDigestWithAlgo2,
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 4*1024*1024 + 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: validDigestWithAlgo2,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with image manifest exceds max blob size")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("artifact manifest fetchAll failed", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: validDigestWithAlgo,
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 481,
|
||||
},
|
||||
},
|
||||
FetchError: fmt.Errorf("failed to fetch all"),
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: validDigestWithAlgo,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with fetchAll failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest fetchAll failed", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: validDigestWithAlgo,
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 481,
|
||||
},
|
||||
},
|
||||
FetchError: fmt.Errorf("failed to fetch all"),
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: validDigestWithAlgo,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with fetchAll failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("artifact manifest marshal failed", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 15,
|
||||
},
|
||||
},
|
||||
FetchContent: []byte("invalid content"),
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with marshal failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest marshal failed", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 15,
|
||||
},
|
||||
},
|
||||
FetchContent: []byte("invalid content"),
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with marshal failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no valid artifact manifest", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 2,
|
||||
},
|
||||
},
|
||||
FetchContent: []byte("{}"),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("artifact manifest with invalid artifactType", func(t *testing.T) {
|
||||
sigManifest := `{"artifactType":"invalid", "subject":{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:835c3386406350fbddf5ee376b358bd20c6c423d6becbec166f83c533e4df5d6",
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 198,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no valid image manifest", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
},
|
||||
},
|
||||
FetchContent: []byte("{}"),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with invalid mediaType", func(t *testing.T) {
|
||||
sigManifest := `{}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "invalid",
|
||||
Size: 2,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatal("expected length of descriptors to be 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with valid artifactType and config.MediaType", func(t *testing.T) {
|
||||
sigManifest := `{"artifactType":"application/vnd.cncf.notary.signature","config":{"mediaType":"application/vnd.oci.empty.v1+json"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:ad3ab7874c72d7bf5db0e55ce839b37ee71320bf7c18ac1a512600963f03c54d",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 283,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 1 {
|
||||
t.Fatal("expected length of descriptors to be 1")
|
||||
}
|
||||
if !content.Equal(sigManifestDesc, descriptors[0]) {
|
||||
t.Fatalf("expected %v, got: %v", sigManifestDesc, descriptors[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with valid artifactType but invalid config.MediaType", func(t *testing.T) {
|
||||
sigManifest := `{"artifactType":"application/vnd.cncf.notary.signature","config":{"mediaType":"invalid"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:becfe1975b40352d0c7bd1337707a4c471fdcfa1ac380f2875fe8076a3bc3581",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 257,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatal("expected length of descriptors to be 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with no artifactType and valid config.MediaType", func(t *testing.T) {
|
||||
sigManifest := `{"config":{"mediaType":"application/vnd.cncf.notary.signature"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:0e0be61f687ba634dd772f6d3048101f78f22fabda64cc9600671cee41ab2d47",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 232,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 1 {
|
||||
t.Fatal("expected length of descriptors to be 1")
|
||||
}
|
||||
if !content.Equal(sigManifestDesc, descriptors[0]) {
|
||||
t.Fatalf("expected %v, got: %v", sigManifestDesc, descriptors[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with no artifactType and invalid config.MediaType", func(t *testing.T) {
|
||||
sigManifest := `{"config":{"mediaType":"invalid"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:1580e4f590269bd40a33e902888429c9bbb250902f5a7eb50f04fbb8bd4dbab3",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 202,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatal("expected length of descriptors to be 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with invalid artifactType", func(t *testing.T) {
|
||||
sigManifest := `{"artifactType":"invalid","config":{"mediaType":"application/vnd.oci.empty.v1+json"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:d8c225cb4eca3e15fa2a44c9d302044e8c8683399939e26f417edb82f8b69cc3",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 253,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatal("expected length of descriptors to be 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUploadSignatureManifest(t *testing.T) {
|
||||
ref, err := registry.ParseReference(validReference)
|
||||
targetDesc, err := repo.Resolve(context.Background(), reference)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse reference")
|
||||
t.Fatalf("failed to resolve reference: %v", err)
|
||||
}
|
||||
client := newRepositoryClientWithImageManifest(mockRemoteClient{}, ref, false)
|
||||
manifest, err := client.uploadSignatureManifest(context.Background(),
|
||||
ocispec.Descriptor{}, ocispec.Descriptor{}, nil)
|
||||
err = repo.ListSignatures(context.Background(), targetDesc, func(signatureManifests []ocispec.Descriptor) error {
|
||||
if len(signatureManifests) == 0 {
|
||||
return fmt.Errorf("expected to find signature in the OCI layout folder, but got none")
|
||||
}
|
||||
var found bool
|
||||
for _, sigManifestDesc := range signatureManifests {
|
||||
if !content.Equal(sigManifestDesc, expectedSignatureManifestDesc) {
|
||||
continue
|
||||
}
|
||||
_, sigDesc, err := repo.FetchSignatureBlob(context.Background(), sigManifestDesc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch blob: %w", err)
|
||||
}
|
||||
if !content.Equal(expectedSignatureBlobDesc, sigDesc) {
|
||||
return fmt.Errorf("expected to get signature blob desc: %v, got: %v", expectedSignatureBlobDesc, sigDesc)
|
||||
}
|
||||
found = true
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("expected to find the signature with manifest desc: %v, but failed", expectedSignatureManifestDesc)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to upload signature manifest: %v", err)
|
||||
}
|
||||
if manifest.ArtifactType != ArtifactTypeNotation {
|
||||
t.Fatalf("expected artifact type: %s, got: %s", ArtifactTypeNotation, manifest.ArtifactType)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
188
signer/plugin.go
188
signer/plugin.go
|
@ -15,62 +15,43 @@ package signer
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"oras.land/oras-go/v2/content"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/internal/envelope"
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
"github.com/notaryproject/notation-go/plugin"
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
)
|
||||
|
||||
// PluginSigner signs artifacts and generates signatures.
|
||||
//
|
||||
// It implements [notation.Signer] and [notation.BlobSigner].
|
||||
type PluginSigner struct {
|
||||
// pluginSigner signs artifacts and generates signatures.
|
||||
// It implements notation.Signer
|
||||
type pluginSigner struct {
|
||||
plugin plugin.SignPlugin
|
||||
keyID string
|
||||
pluginConfig map[string]string
|
||||
manifestAnnotations map[string]string
|
||||
}
|
||||
|
||||
var algorithms = map[crypto.Hash]digest.Algorithm{
|
||||
crypto.SHA256: digest.SHA256,
|
||||
crypto.SHA384: digest.SHA384,
|
||||
crypto.SHA512: digest.SHA512,
|
||||
}
|
||||
|
||||
// NewFromPlugin creates a [PluginSigner] that signs artifacts and generates
|
||||
// NewFromPlugin 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.
|
||||
//
|
||||
// Deprecated: NewFromPlugin function exists for historical compatibility and
|
||||
// should not be used. To create [PluginSigner], use NewPluginSigner() function.
|
||||
func NewFromPlugin(plugin plugin.SignPlugin, keyID string, pluginConfig map[string]string) (notation.Signer, error) {
|
||||
return NewPluginSigner(plugin, keyID, pluginConfig)
|
||||
}
|
||||
|
||||
// NewPluginSigner creates a [PluginSigner] 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 NewPluginSigner(plugin plugin.SignPlugin, keyID string, pluginConfig map[string]string) (*PluginSigner, error) {
|
||||
if plugin == nil {
|
||||
return nil, errors.New("nil plugin")
|
||||
}
|
||||
if keyID == "" {
|
||||
return nil, errors.New("keyID not specified")
|
||||
}
|
||||
return &PluginSigner{
|
||||
|
||||
return &pluginSigner{
|
||||
plugin: plugin,
|
||||
keyID: keyID,
|
||||
pluginConfig: pluginConfig,
|
||||
|
@ -78,103 +59,66 @@ func NewPluginSigner(plugin plugin.SignPlugin, keyID string, pluginConfig map[st
|
|||
}
|
||||
|
||||
// PluginAnnotations returns signature manifest annotations returned from plugin
|
||||
func (s *PluginSigner) PluginAnnotations() map[string]string {
|
||||
func (s *pluginSigner) PluginAnnotations() map[string]string {
|
||||
return s.manifestAnnotations
|
||||
}
|
||||
|
||||
// Sign signs the artifact described by its descriptor and returns the
|
||||
// signature and SignerInfo.
|
||||
func (s *PluginSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
// marshalled envelope.
|
||||
func (s *pluginSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
mergedConfig := s.mergeConfig(opts.PluginConfig)
|
||||
logger.Debug("Invoking plugin's get-plugin-metadata command")
|
||||
metadata, err := s.plugin.GetMetadata(ctx, &plugin.GetMetadataRequest{PluginConfig: mergedConfig})
|
||||
req := &proto.GetMetadataRequest{
|
||||
PluginConfig: s.mergeConfig(opts.PluginConfig),
|
||||
}
|
||||
metadata, err := s.plugin.GetMetadata(ctx, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
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(plugin.CapabilitySignatureGenerator) {
|
||||
ks, err := s.getKeySpec(ctx, mergedConfig)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
|
||||
}
|
||||
sig, signerInfo, err := s.generateSignature(ctx, desc, opts, ks, metadata, mergedConfig)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
|
||||
}
|
||||
return sig, signerInfo, nil
|
||||
} else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) {
|
||||
sig, signerInfo, err := s.generateSignatureEnvelope(ctx, desc, opts)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
|
||||
}
|
||||
return sig, signerInfo, nil
|
||||
|
||||
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)
|
||||
if metadata.HasCapability(proto.CapabilitySignatureGenerator) {
|
||||
return s.generateSignature(ctx, desc, opts, metadata)
|
||||
} else if metadata.HasCapability(proto.CapabilityEnvelopeGenerator) {
|
||||
return s.generateSignatureEnvelope(ctx, desc, opts)
|
||||
}
|
||||
return nil, nil, fmt.Errorf("plugin does not have signing capabilities")
|
||||
}
|
||||
|
||||
// SignBlob signs the descriptor returned by genDesc, and returns the
|
||||
// signature and SignerInfo.
|
||||
func (s *PluginSigner) SignBlob(ctx context.Context, descGenFunc notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
mergedConfig := s.mergeConfig(opts.PluginConfig)
|
||||
logger.Debug("Invoking plugin's get-plugin-metadata command")
|
||||
metadata, err := s.plugin.GetMetadata(ctx, &plugin.GetMetadataRequest{PluginConfig: mergedConfig})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// only support blob signing with the signature generator capability because
|
||||
// the envelope generator capability is designed for OCI signing.
|
||||
// A new capability may be added in the future for blob signing.
|
||||
if !metadata.HasCapability(plugin.CapabilitySignatureGenerator) {
|
||||
return nil, nil, fmt.Errorf("the plugin %q lacks the signature generator capability required for blob signing", metadata.Name)
|
||||
}
|
||||
|
||||
logger.Debug("Invoking plugin's describe-key command")
|
||||
ks, err := s.getKeySpec(ctx, mergedConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// get descriptor to sign
|
||||
desc, err := getDescriptor(ks, descGenFunc)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
logger.Debugf("Using plugin %v with capabilities %v to sign blob using descriptor %+v", metadata.Name, metadata.Capabilities, desc)
|
||||
return s.generateSignature(ctx, desc, opts, ks, metadata, mergedConfig)
|
||||
}
|
||||
|
||||
func (s *PluginSigner) getKeySpec(ctx context.Context, config map[string]string) (signature.KeySpec, error) {
|
||||
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
|
||||
}
|
||||
if s.keyID != descKeyResp.KeyID {
|
||||
return signature.KeySpec{}, fmt.Errorf("keyID in describeKey response %q does not match request %q", descKeyResp.KeyID, s.keyID)
|
||||
}
|
||||
return proto.DecodeKeySpec(descKeyResp.KeySpec)
|
||||
}
|
||||
|
||||
func (s *PluginSigner) generateSignature(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions, ks signature.KeySpec, metadata *plugin.GetMetadataResponse, pluginConfig map[string]string) ([]byte, *signature.SignerInfo, error) {
|
||||
func (s *pluginSigner) generateSignature(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions, metadata *proto.GetMetadataResponse) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debug("Generating signature by plugin")
|
||||
genericSigner := GenericSigner{
|
||||
signer: &pluginPrimitiveSigner{
|
||||
config := s.mergeConfig(opts.PluginConfig)
|
||||
// Get key info.
|
||||
key, err := s.describeKey(ctx, config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Check keyID is honored.
|
||||
if s.keyID != key.KeyID {
|
||||
return nil, nil, fmt.Errorf("keyID in describeKey response %q does not match request %q", key.KeyID, s.keyID)
|
||||
}
|
||||
ks, err := proto.DecodeKeySpec(key.KeySpec)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
genericSigner := genericSigner{
|
||||
Signer: &pluginPrimitiveSigner{
|
||||
ctx: ctx,
|
||||
plugin: s.plugin,
|
||||
keyID: s.keyID,
|
||||
pluginConfig: pluginConfig,
|
||||
pluginConfig: config,
|
||||
keySpec: ks,
|
||||
},
|
||||
}
|
||||
|
||||
opts.SigningAgent = fmt.Sprintf("%s %s/%s", signingAgent, metadata.Name, metadata.Version)
|
||||
return genericSigner.Sign(ctx, desc, opts)
|
||||
}
|
||||
|
||||
func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debug("Generating signature envelope by plugin")
|
||||
payload := envelope.Payload{TargetArtifact: envelope.SanitizeTargetArtifact(desc)}
|
||||
|
@ -182,10 +126,8 @@ func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
|
|||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("envelope payload can't be marshalled: %w", err)
|
||||
}
|
||||
|
||||
// Execute plugin sign command.
|
||||
req := &plugin.GenerateEnvelopeRequest{
|
||||
ContractVersion: plugin.ContractVersion,
|
||||
req := &proto.GenerateEnvelopeRequest{
|
||||
KeyID: s.keyID,
|
||||
Payload: payloadBytes,
|
||||
SignatureEnvelopeType: opts.SignatureMediaType,
|
||||
|
@ -205,11 +147,13 @@ func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
|
|||
resp.SignatureEnvelopeType, req.SignatureEnvelopeType,
|
||||
)
|
||||
}
|
||||
|
||||
logger.Debug("Verifying signature envelope generated by the plugin")
|
||||
sigEnv, err := signature.ParseEnvelope(opts.SignatureMediaType, resp.SignatureEnvelope)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
envContent, err := sigEnv.Verify()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("generated signature failed verification: %w", err)
|
||||
|
@ -217,29 +161,31 @@ func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
|
|||
if err := envelope.ValidatePayloadContentType(&envContent.Payload); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
content := envContent.Payload.Content
|
||||
var signedPayload envelope.Payload
|
||||
if err = json.Unmarshal(content, &signedPayload); err != nil {
|
||||
return nil, nil, fmt.Errorf("signed envelope payload can't be unmarshalled: %w", err)
|
||||
}
|
||||
|
||||
if !isPayloadDescriptorValid(desc, signedPayload.TargetArtifact) {
|
||||
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 {
|
||||
return nil, nil, fmt.Errorf("during signing, following unknown attributes were added to subject descriptor: %+q", unknownAttributes)
|
||||
}
|
||||
|
||||
s.manifestAnnotations = resp.Annotations
|
||||
return resp.SignatureEnvelope, &envContent.SignerInfo, nil
|
||||
}
|
||||
|
||||
func (s *PluginSigner) mergeConfig(config map[string]string) map[string]string {
|
||||
func (s *pluginSigner) mergeConfig(config map[string]string) map[string]string {
|
||||
c := make(map[string]string, len(s.pluginConfig)+len(config))
|
||||
|
||||
// First clone s.PluginConfig.
|
||||
for k, v := range s.pluginConfig {
|
||||
c[k] = v
|
||||
}
|
||||
|
||||
// Then set or override entries from config.
|
||||
for k, v := range config {
|
||||
c[k] = v
|
||||
|
@ -247,16 +193,16 @@ func (s *PluginSigner) mergeConfig(config map[string]string) map[string]string {
|
|||
return c
|
||||
}
|
||||
|
||||
func (s *PluginSigner) describeKey(ctx context.Context, config map[string]string) (*plugin.DescribeKeyResponse, error) {
|
||||
req := &plugin.DescribeKeyRequest{
|
||||
ContractVersion: plugin.ContractVersion,
|
||||
KeyID: s.keyID,
|
||||
PluginConfig: config,
|
||||
func (s *pluginSigner) describeKey(ctx context.Context, config map[string]string) (*proto.DescribeKeyResponse, error) {
|
||||
req := &proto.DescribeKeyRequest{
|
||||
KeyID: s.keyID,
|
||||
PluginConfig: config,
|
||||
}
|
||||
resp, err := s.plugin.DescribeKey(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("describe-key command failed: %w", err)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
@ -266,7 +212,6 @@ func isDescriptorSubset(original, newDesc ocispec.Descriptor) bool {
|
|||
if !content.Equal(original, newDesc) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Plugins may append additional annotations but not replace/override
|
||||
// existing.
|
||||
for k, v := range original.Annotations {
|
||||
|
@ -284,7 +229,6 @@ func isPayloadDescriptorValid(originalDesc, newDesc ocispec.Descriptor) bool {
|
|||
|
||||
func areUnknownAttributesAdded(content []byte) []string {
|
||||
var targetArtifactMap map[string]interface{}
|
||||
|
||||
// Ignoring error because we already successfully unmarshalled before this
|
||||
// point
|
||||
_ = json.Unmarshal(content, &targetArtifactMap)
|
||||
|
@ -341,21 +285,23 @@ func (s *pluginPrimitiveSigner) Sign(payload []byte) ([]byte, []*x509.Certificat
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
keySpecHash, err := proto.HashAlgorithmFromKeySpec(s.keySpec)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req := &plugin.GenerateSignatureRequest{
|
||||
ContractVersion: plugin.ContractVersion,
|
||||
KeyID: s.keyID,
|
||||
KeySpec: keySpec,
|
||||
Hash: keySpecHash,
|
||||
Payload: payload,
|
||||
PluginConfig: s.pluginConfig,
|
||||
|
||||
req := &proto.GenerateSignatureRequest{
|
||||
KeyID: s.keyID,
|
||||
KeySpec: keySpec,
|
||||
Hash: keySpecHash,
|
||||
Payload: payload,
|
||||
PluginConfig: s.pluginConfig,
|
||||
}
|
||||
|
||||
resp, err := s.plugin.GenerateSignature(s.ctx, req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, fmt.Errorf("generate-signature command failed: %w", err)
|
||||
}
|
||||
|
||||
// Check keyID is honored.
|
||||
|
|
|
@ -32,7 +32,6 @@ 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"
|
||||
)
|
||||
|
||||
|
@ -71,16 +70,6 @@ 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,
|
||||
|
@ -126,7 +115,7 @@ func (p *mockPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateS
|
|||
return &proto.GenerateSignatureResponse{
|
||||
KeyID: req.KeyID,
|
||||
Signature: invalidSignatureEnvelope,
|
||||
SigningAlgorithm: sigAlg,
|
||||
SigningAlgorithm: string(sigAlg),
|
||||
CertificateChain: certChain,
|
||||
}, err
|
||||
}
|
||||
|
@ -147,7 +136,7 @@ func (p *mockPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateS
|
|||
|
||||
// GenerateEnvelope generates the Envelope with signature based on the request.
|
||||
func (p *mockPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error) {
|
||||
internalPluginSigner := PluginSigner{
|
||||
internalPluginSigner := pluginSigner{
|
||||
plugin: newMockPlugin(p.key, p.certs, p.keySpec),
|
||||
}
|
||||
|
||||
|
@ -216,46 +205,16 @@ func (p *mockPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEn
|
|||
return &proto.GenerateEnvelopeResponse{}, nil
|
||||
}
|
||||
|
||||
func TestPluginSignerImpl(t *testing.T) {
|
||||
p := &PluginSigner{}
|
||||
if _, ok := interface{}(p).(notation.Signer); !ok {
|
||||
t.Fatal("PluginSigner does not implement notation.Signer")
|
||||
}
|
||||
|
||||
if _, ok := interface{}(p).(notation.BlobSigner); !ok {
|
||||
t.Fatal("PluginSigner does not implement notation.BlobSigner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromPluginFailed(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
pl plugin.SignPlugin
|
||||
keyID string
|
||||
errMsg string
|
||||
}{
|
||||
"Invalid KeyID": {
|
||||
pl: &plugin.CLIPlugin{},
|
||||
keyID: "",
|
||||
errMsg: "keyID not specified",
|
||||
},
|
||||
"nilPlugin": {
|
||||
pl: nil,
|
||||
keyID: "someKeyId",
|
||||
errMsg: "nil plugin",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := NewFromPlugin(tc.pl, tc.keyID, make(map[string]string))
|
||||
if err == nil || err.Error() != tc.errMsg {
|
||||
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", tc.errMsg, err.Error())
|
||||
}
|
||||
})
|
||||
wantErr := "keyID not specified"
|
||||
_, err := NewFromPlugin(&plugin.CLIPlugin{}, "", make(map[string]string))
|
||||
if err == nil || err.Error() != wantErr {
|
||||
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", wantErr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigner_Sign_EnvelopeNotSupported(t *testing.T) {
|
||||
signer := PluginSigner{
|
||||
signer := pluginSigner{
|
||||
plugin: newMockPlugin(nil, nil, signature.KeySpec{Type: signature.KeyTypeRSA, Size: 2048}),
|
||||
}
|
||||
opts := notation.SignerSignOptions{SignatureMediaType: "unsupported"}
|
||||
|
@ -266,7 +225,7 @@ func TestSigner_Sign_DescribeKeyIDMismatch(t *testing.T) {
|
|||
respKeyId := ""
|
||||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
|
||||
signer := PluginSigner{
|
||||
signer := pluginSigner{
|
||||
plugin: newMockPlugin(nil, nil, signature.KeySpec{}),
|
||||
keyID: "1",
|
||||
}
|
||||
|
@ -279,7 +238,7 @@ func TestSigner_Sign_ExpiryInValid(t *testing.T) {
|
|||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
|
||||
ks, _ := signature.ExtractKeySpec(keyCertPairCollections[0].certs[0])
|
||||
signer := PluginSigner{
|
||||
signer := pluginSigner{
|
||||
plugin: newMockPlugin(keyCertPairCollections[0].key, keyCertPairCollections[0].certs, ks),
|
||||
}
|
||||
_, _, err := signer.Sign(context.Background(), ocispec.Descriptor{}, notation.SignerSignOptions{ExpiryDuration: -24 * time.Hour, SignatureMediaType: envelopeType})
|
||||
|
@ -296,7 +255,7 @@ func TestSigner_Sign_InvalidCertChain(t *testing.T) {
|
|||
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
|
||||
mockPlugin := newMockPlugin(defaultKeyCert.key, defaultKeyCert.certs, defaultKeySpec)
|
||||
mockPlugin.invalidCertChain = true
|
||||
signer := PluginSigner{
|
||||
signer := pluginSigner{
|
||||
plugin: mockPlugin,
|
||||
}
|
||||
testSignerError(t, signer, "x509: malformed certificate", notation.SignerSignOptions{SignatureMediaType: envelopeType})
|
||||
|
@ -310,7 +269,7 @@ func TestSigner_Sign_InvalidDescriptor(t *testing.T) {
|
|||
mockPlugin := newMockPlugin(defaultKeyCert.key, defaultKeyCert.certs, defaultKeySpec)
|
||||
mockPlugin.wantEnvelope = true
|
||||
mockPlugin.invalidDescriptor = true
|
||||
signer := PluginSigner{
|
||||
signer := pluginSigner{
|
||||
plugin: mockPlugin,
|
||||
}
|
||||
testSignerError(t, signer, "during signing, following unknown attributes were added to subject descriptor: [\"additional_field\"]", notation.SignerSignOptions{SignatureMediaType: envelopeType})
|
||||
|
@ -323,7 +282,7 @@ func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) {
|
|||
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
|
||||
mockPlugin := newMockPlugin(defaultKeyCert.key, defaultKeyCert.certs, defaultKeySpec)
|
||||
mockPlugin.invalidSig = true
|
||||
signer := PluginSigner{
|
||||
signer := pluginSigner{
|
||||
plugin: mockPlugin,
|
||||
}
|
||||
testSignerError(t, signer, "signature is invalid", notation.SignerSignOptions{SignatureMediaType: envelopeType})
|
||||
|
@ -336,48 +295,15 @@ func TestPluginSigner_Sign_Valid(t *testing.T) {
|
|||
for _, keyCert := range keyCertPairCollections {
|
||||
t.Run(fmt.Sprintf("external plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
|
||||
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
|
||||
pluginSigner := PluginSigner{
|
||||
pluginSigner := pluginSigner{
|
||||
plugin: newMockPlugin(keyCert.key, keyCert.certs, keySpec),
|
||||
}
|
||||
validSignOpts.SignatureMediaType = envelopeType
|
||||
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, &validMetadata)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginSigner_SignBlob_Valid(t *testing.T) {
|
||||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
for _, keyCert := range keyCertPairCollections {
|
||||
t.Run(fmt.Sprintf("external plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
|
||||
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
|
||||
pluginSigner := PluginSigner{
|
||||
plugin: newMockPlugin(keyCert.key, keyCert.certs, keySpec),
|
||||
}
|
||||
validSignOpts.SignatureMediaType = envelopeType
|
||||
data, signerInfo, err := pluginSigner.SignBlob(context.Background(), getDescriptorFunc(false), validSignOpts)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginSigner_SignBlob_Invalid(t *testing.T) {
|
||||
t.Run("blob signing with generate envelope plugin should fail", func(t *testing.T) {
|
||||
plugin := &mockPlugin{}
|
||||
plugin.wantEnvelope = true
|
||||
pluginSigner := PluginSigner{
|
||||
plugin: plugin,
|
||||
}
|
||||
_, _, err := pluginSigner.SignBlob(context.Background(), getDescriptorFunc(false), validSignOpts)
|
||||
expectedErrMsg := "the plugin \"testPlugin\" lacks the signature generator capability required for blob signing"
|
||||
if err == nil || !strings.Contains(err.Error(), expectedErrMsg) {
|
||||
t.Fatalf("expected error %q, got %v", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPluginSigner_SignEnvelope_RunFailed(t *testing.T) {
|
||||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
|
||||
|
@ -385,7 +311,7 @@ func TestPluginSigner_SignEnvelope_RunFailed(t *testing.T) {
|
|||
wantEnvelope: true,
|
||||
failEnvelope: true,
|
||||
}
|
||||
signer := PluginSigner{
|
||||
signer := pluginSigner{
|
||||
plugin: p,
|
||||
}
|
||||
testSignerError(t, signer, "failed GenerateEnvelope", notation.SignerSignOptions{SignatureMediaType: envelopeType})
|
||||
|
@ -400,12 +326,10 @@ func TestPluginSigner_SignEnvelope_Valid(t *testing.T) {
|
|||
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
|
||||
mockPlugin := newMockPlugin(keyCert.key, keyCert.certs, keySpec)
|
||||
mockPlugin.wantEnvelope = true
|
||||
pluginSigner := PluginSigner{
|
||||
pluginSigner := pluginSigner{
|
||||
plugin: mockPlugin,
|
||||
}
|
||||
validSignOpts.SignatureMediaType = envelopeType
|
||||
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, &validMetadata)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -417,7 +341,7 @@ func TestPluginSigner_SignWithAnnotations_Valid(t *testing.T) {
|
|||
t.Run(fmt.Sprintf("external plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
|
||||
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
|
||||
annts := map[string]string{"key": "value"}
|
||||
pluginSigner := PluginSigner{
|
||||
pluginSigner := pluginSigner{
|
||||
plugin: &mockPlugin{
|
||||
key: keyCert.key,
|
||||
certs: keyCert.certs,
|
||||
|
@ -426,9 +350,7 @@ func TestPluginSigner_SignWithAnnotations_Valid(t *testing.T) {
|
|||
wantEnvelope: true,
|
||||
},
|
||||
}
|
||||
validSignOpts.SignatureMediaType = envelopeType
|
||||
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, &validMetadata)
|
||||
if !reflect.DeepEqual(pluginSigner.PluginAnnotations(), annts) {
|
||||
fmt.Println(pluginSigner.PluginAnnotations())
|
||||
t.Errorf("mismatch in annotations returned from PluginAnnotations()")
|
||||
|
@ -438,7 +360,7 @@ func TestPluginSigner_SignWithAnnotations_Valid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func testSignerError(t *testing.T, signer PluginSigner, wantEr string, opts notation.SignerSignOptions) {
|
||||
func testSignerError(t *testing.T, signer pluginSigner, wantEr string, opts notation.SignerSignOptions) {
|
||||
t.Helper()
|
||||
_, _, err := signer.Sign(context.Background(), ocispec.Descriptor{}, opts)
|
||||
if err == nil || !strings.Contains(err.Error(), wantEr) {
|
||||
|
@ -446,7 +368,9 @@ func testSignerError(t *testing.T, signer PluginSigner, wantEr string, opts nota
|
|||
}
|
||||
}
|
||||
|
||||
func basicSignTest(t *testing.T, ps *PluginSigner, envelopeType string, data []byte, signerInfo *signature.SignerInfo, err error) {
|
||||
func basicSignTest(t *testing.T, pluginSigner *pluginSigner, envelopeType string, metadata *proto.GetMetadataResponse) {
|
||||
validSignOpts.SignatureMediaType = envelopeType
|
||||
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("Signer.Sign() error = %v, wantErr nil", err)
|
||||
}
|
||||
|
@ -475,12 +399,12 @@ func basicSignTest(t *testing.T, ps *PluginSigner, envelopeType string, data []b
|
|||
TargetArtifact: validSignDescriptor,
|
||||
}
|
||||
if !reflect.DeepEqual(expectedPayload, gotPayload) {
|
||||
t.Fatalf("Signer.Sign() descriptor subject changed, expect: %+v, got: %+v", expectedPayload, payload)
|
||||
t.Fatalf("Signer.Sign() descriptor subject changed, expect: %v, got: %v", expectedPayload, payload)
|
||||
}
|
||||
if signerInfo.SignedAttributes.SigningScheme != signature.SigningSchemeX509 {
|
||||
t.Fatalf("Signer.Sign() signing scheme changed, expect: %+v, got: %+v", signerInfo.SignedAttributes.SigningScheme, signature.SigningSchemeX509)
|
||||
t.Fatalf("Signer.Sign() signing scheme changed, expect: %v, got: %v", signerInfo.SignedAttributes.SigningScheme, signature.SigningSchemeX509)
|
||||
}
|
||||
mockPlugin := ps.plugin.(*mockPlugin)
|
||||
mockPlugin := pluginSigner.plugin.(*mockPlugin)
|
||||
if mockPlugin.keySpec.SignatureAlgorithm() != signerInfo.SignatureAlgorithm {
|
||||
t.Fatalf("Signer.Sign() signing algorithm changed")
|
||||
}
|
||||
|
@ -490,5 +414,5 @@ func basicSignTest(t *testing.T, ps *PluginSigner, envelopeType string, data []b
|
|||
if !reflect.DeepEqual(mockPlugin.certs, signerInfo.CertificateChain) {
|
||||
t.Fatalf(" Signer.Sign() cert chain changed")
|
||||
}
|
||||
basicVerification(t, data, envelopeType, mockPlugin.certs[len(mockPlugin.certs)-1], &validMetadata)
|
||||
basicVerification(t, data, envelopeType, mockPlugin.certs[len(mockPlugin.certs)-1], metadata)
|
||||
}
|
||||
|
|
|
@ -12,8 +12,8 @@
|
|||
// limitations under the License.
|
||||
|
||||
// Package signer provides notation signing functionality. It implements the
|
||||
// [notation.Signer] and [notation.BlobSigner] interfaces by providing
|
||||
// builtinSigner for local signing and [PluginSigner] for remote signing.
|
||||
// notation.Signer interface by providing builtinSigner for local signing and
|
||||
// pluginSigner for remote signing.
|
||||
package signer
|
||||
|
||||
import (
|
||||
|
@ -34,42 +34,26 @@ import (
|
|||
)
|
||||
|
||||
// signingAgent is the unprotected header field used by signature.
|
||||
const signingAgent = "notation-go/1.3.0+unreleased"
|
||||
const signingAgent = "Notation/1.0.0"
|
||||
|
||||
// GenericSigner implements [notation.Signer] and [notation.BlobSigner].
|
||||
// It embeds signature.Signer.
|
||||
type GenericSigner struct {
|
||||
signer signature.Signer
|
||||
// genericSigner implements notation.Signer and embeds signature.Signer
|
||||
type genericSigner struct {
|
||||
signature.Signer
|
||||
}
|
||||
|
||||
// New returns a [notation.Signer] given key and cert chain.
|
||||
//
|
||||
// Deprecated: New function exists for historical compatibility and
|
||||
// should not be used. To create [GenericSigner],
|
||||
// use NewGenericSigner() function.
|
||||
// New returns a builtinSigner given key and cert chain
|
||||
func New(key crypto.PrivateKey, certChain []*x509.Certificate) (notation.Signer, error) {
|
||||
return NewGenericSigner(key, certChain)
|
||||
}
|
||||
|
||||
// NewGenericSigner returns a builtinSigner given key and cert chain.
|
||||
func NewGenericSigner(key crypto.PrivateKey, certChain []*x509.Certificate) (*GenericSigner, error) {
|
||||
localSigner, err := signature.NewLocalSigner(certChain, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &GenericSigner{
|
||||
signer: localSigner,
|
||||
return &genericSigner{
|
||||
Signer: localSigner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewFromFiles returns a [notation.Signer] given key and certChain paths.
|
||||
// NewFromFiles returns a builtinSigner given key and certChain paths.
|
||||
func NewFromFiles(keyPath, certChainPath string) (notation.Signer, error) {
|
||||
return NewGenericSignerFromFiles(keyPath, certChainPath)
|
||||
}
|
||||
|
||||
// NewGenericSignerFromFiles returns a builtinSigner given key and certChain
|
||||
// paths.
|
||||
func NewGenericSignerFromFiles(keyPath, certChainPath string) (*GenericSigner, error) {
|
||||
if keyPath == "" {
|
||||
return nil, errors.New("key path not specified")
|
||||
}
|
||||
|
@ -96,12 +80,12 @@ func NewGenericSignerFromFiles(keyPath, certChainPath string) (*GenericSigner, e
|
|||
}
|
||||
|
||||
// create signer
|
||||
return NewGenericSigner(cert.PrivateKey, certs)
|
||||
return New(cert.PrivateKey, certs)
|
||||
}
|
||||
|
||||
// Sign signs the artifact described by its descriptor and returns the
|
||||
// signature and SignerInfo.
|
||||
func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
// marshalled envelope.
|
||||
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.
|
||||
|
@ -110,30 +94,22 @@ func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
|
|||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("envelope payload can't be marshalled: %w", err)
|
||||
}
|
||||
|
||||
var signingAgentId string
|
||||
if opts.SigningAgent != "" {
|
||||
signingAgentId = opts.SigningAgent
|
||||
} else {
|
||||
signingAgentId = signingAgent
|
||||
}
|
||||
if opts.Timestamper != nil && opts.TSARootCAs == nil {
|
||||
return nil, nil, errors.New("timestamping: got Timestamper but nil TSARootCAs")
|
||||
}
|
||||
if opts.TSARootCAs != nil && opts.Timestamper == nil {
|
||||
return nil, nil, errors.New("timestamping: got TSARootCAs but nil Timestamper")
|
||||
}
|
||||
signReq := &signature.SignRequest{
|
||||
Payload: signature.Payload{
|
||||
ContentType: envelope.MediaTypePayloadV1,
|
||||
Content: payloadBytes,
|
||||
},
|
||||
Signer: s.signer,
|
||||
SigningTime: time.Now(),
|
||||
SigningScheme: signature.SigningSchemeX509,
|
||||
SigningAgent: signingAgentId,
|
||||
Timestamper: opts.Timestamper,
|
||||
TSARootCAs: opts.TSARootCAs,
|
||||
TSARevocationValidator: opts.TSARevocationValidator,
|
||||
Signer: s.Signer,
|
||||
SigningTime: time.Now(),
|
||||
SigningScheme: signature.SigningSchemeX509,
|
||||
SigningAgent: signingAgentId,
|
||||
}
|
||||
|
||||
// Add expiry only if ExpiryDuration is not zero
|
||||
|
@ -147,25 +123,18 @@ func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
|
|||
logger.Debugf(" Expiry: %v", signReq.Expiry)
|
||||
logger.Debugf(" SigningScheme: %v", signReq.SigningScheme)
|
||||
logger.Debugf(" SigningAgent: %v", signReq.SigningAgent)
|
||||
if signReq.Timestamper != nil {
|
||||
logger.Debug("Enabled timestamping")
|
||||
if signReq.TSARevocationValidator != nil {
|
||||
logger.Debug("Enabled timestamping certificate chain revocation check")
|
||||
}
|
||||
}
|
||||
|
||||
// Add ctx to the SignRequest
|
||||
signReq = signReq.WithContext(ctx)
|
||||
|
||||
// perform signing
|
||||
sigEnv, err := signature.NewEnvelope(opts.SignatureMediaType)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sig, err := sigEnv.Sign(signReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
envContent, err := sigEnv.Verify()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("generated signature failed verification: %v", err)
|
||||
|
@ -173,29 +142,7 @@ func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
|
|||
if err := envelope.ValidatePayloadContentType(&envContent.Payload); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO: re-enable timestamping https://github.com/notaryproject/notation-go/issues/78
|
||||
return sig, &envContent.SignerInfo, nil
|
||||
}
|
||||
|
||||
// SignBlob signs the descriptor returned by genDesc, and returns the
|
||||
// signature and SignerInfo.
|
||||
func (s *GenericSigner) SignBlob(ctx context.Context, genDesc notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debugf("Generic blob signing for signature media type %s", opts.SignatureMediaType)
|
||||
ks, err := s.signer.KeySpec()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
desc, err := getDescriptor(ks, genDesc)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return s.Sign(ctx, desc, opts)
|
||||
}
|
||||
|
||||
func getDescriptor(ks signature.KeySpec, genDesc notation.BlobDescriptorGenerator) (ocispec.Descriptor, error) {
|
||||
digestAlg, ok := algorithms[ks.SignatureAlgorithm().Hash()]
|
||||
if !ok {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("unknown hashing algo %v", ks.SignatureAlgorithm().Hash())
|
||||
}
|
||||
return genDesc(digestAlg)
|
||||
}
|
||||
|
|
|
@ -30,23 +30,17 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/revocation"
|
||||
"github.com/notaryproject/notation-core-go/revocation/purpose"
|
||||
"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-core-go/testhelper"
|
||||
nx509 "github.com/notaryproject/notation-core-go/x509"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/internal/envelope"
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
"github.com/notaryproject/tspclient-go"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
const rfc3161URL = "http://timestamp.digicert.com"
|
||||
|
||||
type keyCertPair struct {
|
||||
keySpecName string
|
||||
key crypto.PrivateKey
|
||||
|
@ -55,8 +49,7 @@ type keyCertPair struct {
|
|||
|
||||
var keyCertPairCollections []*keyCertPair
|
||||
|
||||
// setUpKeyCertPairCollections setups all combinations of private key and
|
||||
// certificates.
|
||||
// setUpKeyCertPairCollections setups all combinations of private key and certificates.
|
||||
func setUpKeyCertPairCollections() []*keyCertPair {
|
||||
// rsa
|
||||
var keyCertPairs []*keyCertPair
|
||||
|
@ -124,7 +117,7 @@ func generateKeyBytes(key crypto.PrivateKey) (keyBytes []byte, err error) {
|
|||
return keyBytes, nil
|
||||
}
|
||||
|
||||
func prepareTestKeyCertFile(keyCert *keyCertPair, dir string) (string, string, error) {
|
||||
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 {
|
||||
|
@ -145,7 +138,7 @@ func prepareTestKeyCertFile(keyCert *keyCertPair, dir string) (string, string, e
|
|||
}
|
||||
|
||||
func testSignerFromFile(t *testing.T, keyCert *keyCertPair, envelopeType, dir string) {
|
||||
keyPath, certPath, err := prepareTestKeyCertFile(keyCert, dir)
|
||||
keyPath, certPath, err := prepareTestKeyCertFile(keyCert, envelopeType, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("prepareTestKeyCertFile() failed: %v", err)
|
||||
}
|
||||
|
@ -163,17 +156,6 @@ func testSignerFromFile(t *testing.T, keyCert *keyCertPair, envelopeType, dir st
|
|||
basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1], nil)
|
||||
}
|
||||
|
||||
func TestGenericSignerImpl(t *testing.T) {
|
||||
g := &GenericSigner{}
|
||||
if _, ok := interface{}(g).(notation.Signer); !ok {
|
||||
t.Fatal("GenericSigner does not implement notation.Signer")
|
||||
}
|
||||
|
||||
if _, ok := interface{}(g).(notation.BlobSigner); !ok {
|
||||
t.Fatal("GenericSigner does not implement notation.BlobSigner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromFiles(t *testing.T) {
|
||||
// sign with key
|
||||
dir := t.TempDir()
|
||||
|
@ -186,134 +168,12 @@ func TestNewFromFiles(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewFromFilesError(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
keyPath string
|
||||
certPath string
|
||||
errMsg string
|
||||
}{
|
||||
"empty key path": {
|
||||
keyPath: "",
|
||||
certPath: "someCert",
|
||||
errMsg: "key path not specified",
|
||||
},
|
||||
"empty cert path": {
|
||||
keyPath: "someKeyId",
|
||||
certPath: "",
|
||||
errMsg: "certificate path not specified",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := NewFromFiles(tc.keyPath, tc.certPath)
|
||||
if err == nil || err.Error() != tc.errMsg {
|
||||
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", tc.errMsg, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewError(t *testing.T) {
|
||||
wantErr := "\"certs\" param is invalid. Error: empty certs"
|
||||
_, err := New(nil, nil)
|
||||
if err == nil || err.Error() != wantErr {
|
||||
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", wantErr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
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, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithTimestamping(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, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// timestamping without timestamper
|
||||
envelopeType := signature.RegisteredEnvelopeTypes()[0]
|
||||
keyCert := keyCertPairCollections[0]
|
||||
s, err := New(keyCert.key, keyCert.certs)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner() error = %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
desc, sOpts := generateSigningContent()
|
||||
sOpts.SignatureMediaType = envelopeType
|
||||
sOpts.TSARootCAs = x509.NewCertPool()
|
||||
_, _, err = s.Sign(ctx, desc, sOpts)
|
||||
expectedErrMsg := "timestamping: got TSARootCAs but nil Timestamper"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
|
||||
// timestamping without TSARootCAs
|
||||
desc, sOpts = generateSigningContent()
|
||||
sOpts.SignatureMediaType = envelopeType
|
||||
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, err = s.Sign(ctx, desc, sOpts)
|
||||
expectedErrMsg = "timestamping: got Timestamper but nil TSARootCAs"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
|
||||
// timestamping with unknown authority
|
||||
desc, sOpts = generateSigningContent()
|
||||
sOpts.SignatureMediaType = envelopeType
|
||||
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sOpts.TSARootCAs = x509.NewCertPool()
|
||||
tsaRevocationValidator, err := revocation.NewWithOptions(revocation.Options{
|
||||
CertChainPurpose: purpose.Timestamping,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sOpts.TSARevocationValidator = tsaRevocationValidator
|
||||
_, _, err = s.Sign(ctx, desc, sOpts)
|
||||
expectedErrMsg = "timestamp: failed to verify signed token: cms verification failure: x509: certificate signed by unknown authority"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignBlobWithCertChain(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 := NewGenericSigner(keyCert.key, keyCert.certs)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner() error = %v", err)
|
||||
}
|
||||
|
||||
sOpts := notation.SignerSignOptions{
|
||||
SignatureMediaType: envelopeType,
|
||||
}
|
||||
sig, _, err := s.SignBlob(context.Background(), getDescriptorFunc(false), sOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign() error = %v", err)
|
||||
}
|
||||
|
||||
// basic verification
|
||||
basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1], nil)
|
||||
validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -349,7 +209,7 @@ 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, pk *ecdsa.PrivateKey) ([]byte, error) {
|
||||
func signECDSA(digest []byte, hash crypto.Hash, pk *ecdsa.PrivateKey) ([]byte, error) {
|
||||
r, s, err := ecdsa.Sign(rand.Reader, pk, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -369,7 +229,7 @@ func localSign(payload []byte, hash crypto.Hash, pk crypto.PrivateKey) ([]byte,
|
|||
case *rsa.PrivateKey:
|
||||
return signRSA(digest, hash, key)
|
||||
case *ecdsa.PrivateKey:
|
||||
return signECDSA(digest, key)
|
||||
return signECDSA(digest, hash, key)
|
||||
default:
|
||||
return nil, errors.New("signing private key not supported")
|
||||
}
|
||||
|
@ -434,7 +294,7 @@ func verifySigningAgent(t *testing.T, signingAgentId string, metadata *proto.Get
|
|||
}
|
||||
}
|
||||
|
||||
func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.PrivateKey, certs []*x509.Certificate, timestamp bool) {
|
||||
func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.PrivateKey, certs []*x509.Certificate) {
|
||||
s, err := New(key, certs)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner() error = %v", err)
|
||||
|
@ -443,19 +303,6 @@ func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.Private
|
|||
ctx := context.Background()
|
||||
desc, sOpts := generateSigningContent()
|
||||
sOpts.SignatureMediaType = envelopeType
|
||||
if timestamp {
|
||||
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCerts, err := nx509.ReadCertificateFile("./testdata/DigiCertTSARootSHA384.cer")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCAs := x509.NewCertPool()
|
||||
rootCAs.AddCert(rootCerts[0])
|
||||
sOpts.TSARootCAs = rootCAs
|
||||
}
|
||||
sig, _, err := s.Sign(ctx, desc, sOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign() error = %v", err)
|
||||
|
|
Binary file not shown.
|
@ -1,171 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package crl provides functionalities for crl revocation check.
|
||||
package crl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
corecrl "github.com/notaryproject/notation-core-go/revocation/crl"
|
||||
"github.com/notaryproject/notation-go/internal/file"
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
)
|
||||
|
||||
// FileCache implements corecrl.Cache.
|
||||
//
|
||||
// Key: url of the CRL.
|
||||
//
|
||||
// Value: corecrl.Bundle.
|
||||
//
|
||||
// This cache builds on top of the UNIX file system to leverage the file system's
|
||||
// atomic operations. The `rename` and `remove` operations will unlink the old
|
||||
// file but keep the inode and file descriptor for existing processes to access
|
||||
// the file. The old inode will be dereferenced when all processes close the old
|
||||
// file descriptor. Additionally, the operations are proven to be atomic on
|
||||
// UNIX-like platforms, so there is no need to handle file locking.
|
||||
//
|
||||
// NOTE: For Windows, the `open`, `rename` and `remove` operations need file
|
||||
// locking to ensure atomicity. The current implementation does not handle
|
||||
// file locking, so the concurrent write from multiple processes may be failed.
|
||||
// Please do not use this cache in a multi-process environment on Windows.
|
||||
type FileCache struct {
|
||||
// root is the root directory of the cache
|
||||
root string
|
||||
}
|
||||
|
||||
// fileCacheContent is the actual content saved in a FileCache
|
||||
type fileCacheContent struct {
|
||||
// BaseCRL is the ASN.1 encoded base CRL
|
||||
BaseCRL []byte `json:"baseCRL"`
|
||||
|
||||
// DeltaCRL is the ASN.1 encoded delta CRL
|
||||
DeltaCRL []byte `json:"deltaCRL,omitempty"`
|
||||
}
|
||||
|
||||
// NewFileCache creates a FileCache with root as the root directory
|
||||
//
|
||||
// An example for root is `dir.CacheFS().SysPath(dir.PathCRLCache)`
|
||||
func NewFileCache(root string) (*FileCache, error) {
|
||||
if err := os.MkdirAll(root, 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create crl file cache: %w", err)
|
||||
}
|
||||
return &FileCache{
|
||||
root: root,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get retrieves CRL bundle from c given url as key. If the key does not exist
|
||||
// or the content has expired, corecrl.ErrCacheMiss is returned.
|
||||
func (c *FileCache) Get(ctx context.Context, url string) (*corecrl.Bundle, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debugf("Retrieving crl bundle from file cache with key %q ...", url)
|
||||
|
||||
// get content from file cache
|
||||
contentBytes, err := os.ReadFile(filepath.Join(c.root, c.fileName(url)))
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
logger.Debugf("CRL file cache miss. Key %q does not exist", url)
|
||||
return nil, corecrl.ErrCacheMiss
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get crl bundle from file cache with key %q: %w", url, err)
|
||||
}
|
||||
|
||||
// decode content to crl Bundle
|
||||
var content fileCacheContent
|
||||
if err := json.Unmarshal(contentBytes, &content); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode file retrieved from file cache: %w", err)
|
||||
}
|
||||
var bundle corecrl.Bundle
|
||||
bundle.BaseCRL, err = x509.ParseRevocationList(content.BaseCRL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse base CRL of file retrieved from file cache: %w", err)
|
||||
}
|
||||
if content.DeltaCRL != nil {
|
||||
bundle.DeltaCRL, err = x509.ParseRevocationList(content.DeltaCRL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse delta CRL of file retrieved from file cache: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// check expiry
|
||||
if err := checkExpiry(ctx, bundle.BaseCRL.NextUpdate); err != nil {
|
||||
return nil, fmt.Errorf("check BaseCRL expiry failed: %w", err)
|
||||
}
|
||||
if bundle.DeltaCRL != nil {
|
||||
if err := checkExpiry(ctx, bundle.DeltaCRL.NextUpdate); err != nil {
|
||||
return nil, fmt.Errorf("check DeltaCRL expiry failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &bundle, nil
|
||||
}
|
||||
|
||||
// Set stores the CRL bundle in c with url as key.
|
||||
func (c *FileCache) Set(ctx context.Context, url string, bundle *corecrl.Bundle) error {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debugf("Storing crl bundle to file cache with key %q ...", url)
|
||||
|
||||
if bundle == nil {
|
||||
return errors.New("failed to store crl bundle in file cache: bundle cannot be nil")
|
||||
}
|
||||
if bundle.BaseCRL == nil {
|
||||
return errors.New("failed to store crl bundle in file cache: bundle BaseCRL cannot be nil")
|
||||
}
|
||||
|
||||
// actual content to be saved in the cache
|
||||
content := fileCacheContent{
|
||||
BaseCRL: bundle.BaseCRL.Raw,
|
||||
}
|
||||
if bundle.DeltaCRL != nil {
|
||||
content.DeltaCRL = bundle.DeltaCRL.Raw
|
||||
}
|
||||
contentBytes, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store crl bundle in file cache: %w", err)
|
||||
}
|
||||
if err := file.WriteFile(c.root, filepath.Join(c.root, c.fileName(url)), contentBytes); err != nil {
|
||||
return fmt.Errorf("failed to store crl bundle in file cache: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fileName returns the filename of the content stored in c
|
||||
func (c *FileCache) fileName(url string) string {
|
||||
hash := sha256.Sum256([]byte(url))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// checkExpiry returns nil when nextUpdate is bounded before current time
|
||||
func checkExpiry(ctx context.Context, nextUpdate time.Time) error {
|
||||
logger := log.GetLogger(ctx)
|
||||
|
||||
if nextUpdate.IsZero() {
|
||||
return errors.New("crl bundle retrieved from file cache does not contain valid NextUpdate")
|
||||
}
|
||||
if time.Now().After(nextUpdate) {
|
||||
logger.Debugf("CRL bundle retrieved from file cache has expired at %s", nextUpdate)
|
||||
return corecrl.ErrCacheMiss
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,395 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package crl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/big"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
corecrl "github.com/notaryproject/notation-core-go/revocation/crl"
|
||||
"github.com/notaryproject/notation-core-go/testhelper"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
t.Run("file cache implement Cache interface", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
var coreCache corecrl.Cache
|
||||
var err error
|
||||
coreCache, err = NewFileCache(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := coreCache.(*FileCache); !ok {
|
||||
t.Fatal("FileCache does not implement coreCache")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileCache(t *testing.T) {
|
||||
now := time.Now()
|
||||
certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true)
|
||||
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
|
||||
Number: big.NewInt(1),
|
||||
NextUpdate: now.Add(time.Hour),
|
||||
}, certChain[1].Cert, certChain[1].PrivateKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
baseCRL, err := x509.ParseRevocationList(crlBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
root := t.TempDir()
|
||||
cache, err := NewFileCache(root)
|
||||
t.Run("NewFileCache", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, but got %v", err)
|
||||
}
|
||||
if cache.root != root {
|
||||
t.Fatalf("expected root %v, but got %v", root, cache.root)
|
||||
}
|
||||
})
|
||||
|
||||
key := "http://example.com"
|
||||
t.Run("comformance", func(t *testing.T) {
|
||||
bundle := &corecrl.Bundle{BaseCRL: baseCRL}
|
||||
if err := cache.Set(ctx, key, bundle); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
retrievedBundle, err := cache.Get(ctx, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(retrievedBundle.BaseCRL, bundle.BaseCRL) {
|
||||
t.Fatalf("expected BaseCRL %+v, but got %+v", bundle.BaseCRL, retrievedBundle.BaseCRL)
|
||||
}
|
||||
|
||||
if bundle.DeltaCRL != nil {
|
||||
t.Fatalf("expected DeltaCRL to be nil, but got %+v", retrievedBundle.DeltaCRL)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("comformance with delta crl", func(t *testing.T) {
|
||||
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
|
||||
Number: big.NewInt(2),
|
||||
NextUpdate: now.Add(time.Hour),
|
||||
}, certChain[1].Cert, certChain[1].PrivateKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
deltaCRL, err := x509.ParseRevocationList(crlBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bundle := &corecrl.Bundle{BaseCRL: baseCRL, DeltaCRL: deltaCRL}
|
||||
if err := cache.Set(ctx, key, bundle); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
retrievedBundle, err := cache.Get(ctx, key)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(retrievedBundle.BaseCRL, bundle.BaseCRL) {
|
||||
t.Fatalf("expected BaseCRL %+v, but got %+v", bundle.BaseCRL, retrievedBundle.BaseCRL)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(retrievedBundle.DeltaCRL, bundle.DeltaCRL) {
|
||||
t.Fatalf("expected DeltaCRL %+v, but got %+v", bundle.DeltaCRL, retrievedBundle.DeltaCRL)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewFileCacheFailed(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Run("without permission to create cache directory", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
|
||||
if err := os.Chmod(tempDir, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := filepath.Join(tempDir, "test")
|
||||
_, err := NewFileCache(root)
|
||||
if !strings.Contains(err.Error(), "permission denied") {
|
||||
t.Fatalf("expected permission denied error, but got %v", err)
|
||||
}
|
||||
// restore permission
|
||||
if err := os.Chmod(tempDir, 0755); err != nil {
|
||||
t.Fatalf("failed to change permission: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFailed(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
cache, err := NewFileCache(tempDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("key does not exist", func(t *testing.T) {
|
||||
_, err := cache.Get(context.Background(), "nonExistKey")
|
||||
if !errors.Is(err, corecrl.ErrCacheMiss) {
|
||||
t.Fatalf("expected ErrCacheMiss, but got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
invalidFile := filepath.Join(tempDir, cache.fileName("invalid"))
|
||||
if err := os.WriteFile(invalidFile, []byte("invalid"), 0644); err != nil {
|
||||
t.Fatalf("failed to write file: %v", err)
|
||||
}
|
||||
|
||||
t.Run("no permission to read file", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
|
||||
if err := os.Chmod(invalidFile, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err := cache.Get(context.Background(), "invalid")
|
||||
if err == nil || !strings.Contains(err.Error(), "permission denied") {
|
||||
t.Fatalf("expected permission denied error, but got %v", err)
|
||||
}
|
||||
// restore permission
|
||||
if err := os.Chmod(invalidFile, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid content", func(t *testing.T) {
|
||||
_, err := cache.Get(context.Background(), "invalid")
|
||||
expectedErrMsg := "failed to decode file retrieved from file cache: invalid character 'i' looking for beginning of value"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
now := time.Now()
|
||||
certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true)
|
||||
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
|
||||
Number: big.NewInt(1),
|
||||
}, certChain[1].Cert, certChain[1].PrivateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create base CRL: %v", err)
|
||||
}
|
||||
baseCRL, err := x509.ParseRevocationList(crlBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse base CRL: %v", err)
|
||||
}
|
||||
|
||||
t.Run("empty RawBaseCRL of content", func(t *testing.T) {
|
||||
content := fileCacheContent{
|
||||
BaseCRL: []byte{},
|
||||
}
|
||||
b, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invalidBundleFile := filepath.Join(tempDir, cache.fileName("invalidBundle"))
|
||||
if err := os.WriteFile(invalidBundleFile, b, 0644); err != nil {
|
||||
t.Fatalf("failed to write file: %v", err)
|
||||
}
|
||||
_, err = cache.Get(context.Background(), "invalidBundle")
|
||||
expectedErrMsg := "failed to parse base CRL of file retrieved from file cache: x509: malformed crl"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid RawBaseCRL of content", func(t *testing.T) {
|
||||
content := fileCacheContent{
|
||||
BaseCRL: []byte("invalid"),
|
||||
}
|
||||
b, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invalidBundleFile := filepath.Join(tempDir, cache.fileName("invalidBundle"))
|
||||
if err := os.WriteFile(invalidBundleFile, b, 0644); err != nil {
|
||||
t.Fatalf("failed to write file: %v", err)
|
||||
}
|
||||
_, err = cache.Get(context.Background(), "invalidBundle")
|
||||
expectedErrMsg := "failed to parse base CRL of file retrieved from file cache: x509: malformed crl"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid RawDeltaCRL of content", func(t *testing.T) {
|
||||
content := fileCacheContent{
|
||||
BaseCRL: baseCRL.Raw,
|
||||
DeltaCRL: []byte("invalid"),
|
||||
}
|
||||
b, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
invalidBundleFile := filepath.Join(tempDir, cache.fileName("invalidBundle"))
|
||||
if err := os.WriteFile(invalidBundleFile, b, 0644); err != nil {
|
||||
t.Fatalf("failed to write file: %v", err)
|
||||
}
|
||||
_, err = cache.Get(context.Background(), "invalidBundle")
|
||||
expectedErrMsg := "failed to parse delta CRL of file retrieved from file cache: x509: malformed crl"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("bundle with invalid NextUpdate", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
expiredBundle := &corecrl.Bundle{BaseCRL: baseCRL}
|
||||
if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = cache.Get(ctx, "expiredKey")
|
||||
expectedErrMsg := "check BaseCRL expiry failed: crl bundle retrieved from file cache does not contain valid NextUpdate"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
crlBytes, err = x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
|
||||
Number: big.NewInt(1),
|
||||
NextUpdate: now.Add(-time.Hour),
|
||||
}, certChain[1].Cert, certChain[1].PrivateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create base CRL: %v", err)
|
||||
}
|
||||
expiredBaseCRL, err := x509.ParseRevocationList(crlBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse base CRL: %v", err)
|
||||
}
|
||||
t.Run("base crl in cache has expired", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
expiredBundle := &corecrl.Bundle{BaseCRL: expiredBaseCRL}
|
||||
if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = cache.Get(ctx, "expiredKey")
|
||||
if !errors.Is(err, corecrl.ErrCacheMiss) {
|
||||
t.Fatalf("expected ErrCacheMiss, but got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delta crl in cache has expired", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
|
||||
Number: big.NewInt(1),
|
||||
NextUpdate: now.Add(time.Hour),
|
||||
}, certChain[1].Cert, certChain[1].PrivateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create base CRL: %v", err)
|
||||
}
|
||||
baseCRL, err := x509.ParseRevocationList(crlBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse base CRL: %v", err)
|
||||
}
|
||||
crlBytes, err = x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
|
||||
Number: big.NewInt(1),
|
||||
NextUpdate: now.Add(-time.Hour),
|
||||
}, certChain[1].Cert, certChain[1].PrivateKey)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create base CRL: %v", err)
|
||||
}
|
||||
expiredDeltaCRL, err := x509.ParseRevocationList(crlBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse base CRL: %v", err)
|
||||
}
|
||||
expiredBundle := &corecrl.Bundle{BaseCRL: baseCRL, DeltaCRL: expiredDeltaCRL}
|
||||
if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = cache.Get(ctx, "expiredKey")
|
||||
if !errors.Is(err, corecrl.ErrCacheMiss) {
|
||||
t.Fatalf("expected ErrCacheMiss, but got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetFailed(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
cache, err := NewFileCache(tempDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true)
|
||||
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
|
||||
Number: big.NewInt(1),
|
||||
NextUpdate: now.Add(time.Hour),
|
||||
}, certChain[1].Cert, certChain[1].PrivateKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
baseCRL, err := x509.ParseRevocationList(crlBytes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
key := "testKey"
|
||||
|
||||
t.Run("nil bundle", func(t *testing.T) {
|
||||
err := cache.Set(ctx, key, nil)
|
||||
expectedErrMsg := "failed to store crl bundle in file cache: bundle cannot be nil"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil bundle BaseCRL", func(t *testing.T) {
|
||||
bundle := &corecrl.Bundle{}
|
||||
err := cache.Set(ctx, key, bundle)
|
||||
expectedErrMsg := "failed to store crl bundle in file cache: bundle BaseCRL cannot be nil"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("failed to write into cache due to permission denied", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
|
||||
if err := os.Chmod(tempDir, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bundle := &corecrl.Bundle{BaseCRL: baseCRL}
|
||||
err := cache.Set(ctx, key, bundle)
|
||||
if err == nil || !strings.Contains(err.Error(), "permission denied") {
|
||||
t.Fatalf("expected permission denied error, but got %v", err)
|
||||
}
|
||||
// restore permission
|
||||
if err := os.Chmod(tempDir, 0755); err != nil {
|
||||
t.Fatalf("failed to change permission: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -18,12 +18,12 @@ import (
|
|||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-go"
|
||||
set "github.com/notaryproject/notation-go/internal/container"
|
||||
notationsemver "github.com/notaryproject/notation-go/internal/semver"
|
||||
"github.com/notaryproject/notation-go/internal/slices"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
|
@ -47,7 +47,10 @@ var VerificationPluginHeaders = []string{
|
|||
|
||||
var errExtendedAttributeNotExist = errors.New("extended attribute not exist")
|
||||
|
||||
func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, policyName string, trustStores []string, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) {
|
||||
// semVerRegEx is takenfrom https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||
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-]+)*))?$`)
|
||||
|
||||
func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, policy *trustpolicy.TrustPolicy, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) {
|
||||
var typeToLoad truststore.Type
|
||||
switch scheme {
|
||||
case signature.SigningSchemeX509:
|
||||
|
@ -55,15 +58,39 @@ func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, po
|
|||
case signature.SigningSchemeX509SigningAuthority:
|
||||
typeToLoad = truststore.TypeSigningAuthority
|
||||
default:
|
||||
return nil, truststore.TrustStoreError{Msg: fmt.Sprintf("error while loading the trust store, unrecognized signing scheme %q", scheme)}
|
||||
return nil, fmt.Errorf("unrecognized signing scheme %q", scheme)
|
||||
}
|
||||
return loadX509TrustStoresWithType(ctx, typeToLoad, policyName, trustStores, x509TrustStore)
|
||||
|
||||
processedStoreSet := set.New[string]()
|
||||
var certificates []*x509.Certificate
|
||||
for _, trustStore := range policy.TrustStores {
|
||||
if processedStoreSet.Contains(trustStore) {
|
||||
// we loaded this trust store already
|
||||
continue
|
||||
}
|
||||
|
||||
storeType, name, found := strings.Cut(trustStore, ":")
|
||||
if !found {
|
||||
return nil, fmt.Errorf("trust policy statement %q is missing separator in trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", policy.Name, trustStore)
|
||||
}
|
||||
if typeToLoad != truststore.Type(storeType) {
|
||||
continue
|
||||
}
|
||||
|
||||
certs, err := x509TrustStore.GetCertificates(ctx, typeToLoad, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certificates = append(certificates, certs...)
|
||||
processedStoreSet.Add(trustStore)
|
||||
}
|
||||
return certificates, nil
|
||||
}
|
||||
|
||||
// isCriticalFailure checks whether a [notation.ValidationResult] fails the
|
||||
// entire signature verification workflow.
|
||||
// isCriticalFailure checks whether a VerificationResult fails the entire
|
||||
// signature verification workflow.
|
||||
// signature verification workflow is considered failed if there is a
|
||||
// ValidationResult with "Enforced" as the action but the result was
|
||||
// VerificationResult with "Enforced" as the action but the result was
|
||||
// unsuccessful.
|
||||
func isCriticalFailure(result *notation.ValidationResult) bool {
|
||||
return result.Action == trustpolicy.ActionEnforce && result.Error != nil
|
||||
|
@ -125,61 +152,12 @@ func getVerificationPluginMinVersion(signerInfo *signature.SignerInfo) (string,
|
|||
if strings.TrimSpace(version) == "" {
|
||||
return "", fmt.Errorf("%v from extended attribute is an empty string", HeaderVerificationPluginMinVersion)
|
||||
}
|
||||
if !notationsemver.IsValid(version) {
|
||||
if !isVersionSemverValid(version) {
|
||||
return "", fmt.Errorf("%v from extended attribute is not a valid SemVer", HeaderVerificationPluginMinVersion)
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func loadX509TSATrustStores(ctx context.Context, scheme signature.SigningScheme, policyName string, trustStores []string, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) {
|
||||
var typeToLoad truststore.Type
|
||||
switch scheme {
|
||||
case signature.SigningSchemeX509:
|
||||
typeToLoad = truststore.TypeTSA
|
||||
default:
|
||||
return nil, truststore.TrustStoreError{Msg: fmt.Sprintf("error while loading the TSA trust store, signing scheme must be notary.x509, but got %s", scheme)}
|
||||
}
|
||||
return loadX509TrustStoresWithType(ctx, typeToLoad, policyName, trustStores, x509TrustStore)
|
||||
}
|
||||
|
||||
func loadX509TrustStoresWithType(ctx context.Context, trustStoreType truststore.Type, policyName string, trustStores []string, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) {
|
||||
processedStoreSet := set.New[string]()
|
||||
var certificates []*x509.Certificate
|
||||
for _, trustStore := range trustStores {
|
||||
if processedStoreSet.Contains(trustStore) {
|
||||
// we loaded this trust store already
|
||||
continue
|
||||
}
|
||||
|
||||
storeType, name, found := strings.Cut(trustStore, ":")
|
||||
if !found {
|
||||
return nil, truststore.TrustStoreError{Msg: fmt.Sprintf("error while loading the trust store, trust policy statement %q is missing separator in trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", policyName, trustStore)}
|
||||
}
|
||||
if trustStoreType != truststore.Type(storeType) {
|
||||
continue
|
||||
}
|
||||
|
||||
certs, err := x509TrustStore.GetCertificates(ctx, trustStoreType, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certificates = append(certificates, certs...)
|
||||
processedStoreSet.Add(trustStore)
|
||||
}
|
||||
return certificates, nil
|
||||
}
|
||||
|
||||
// isTSATrustStoreInPolicy checks if tsa trust store is configured in
|
||||
// trust policy
|
||||
func isTSATrustStoreInPolicy(policyName string, trustStores []string) (bool, error) {
|
||||
for _, trustStore := range trustStores {
|
||||
storeType, _, found := strings.Cut(trustStore, ":")
|
||||
if !found {
|
||||
return false, truststore.TrustStoreError{Msg: fmt.Sprintf("invalid trust policy statement: %q is missing separator in trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", policyName, trustStore)}
|
||||
}
|
||||
if truststore.Type(storeType) == truststore.TypeTSA {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
func isVersionSemverValid(version string) bool {
|
||||
return semVerRegEx.MatchString(version)
|
||||
}
|
||||
|
|
|
@ -28,6 +28,25 @@ import (
|
|||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
)
|
||||
|
||||
func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
|
||||
policyStatement = trustpolicy.TrustPolicy{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func dummyPolicyDocument() (policyDoc trustpolicy.Document) {
|
||||
policyDoc = trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{dummyPolicyStatement()},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestGetArtifactDigestFromUri(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
|
@ -60,18 +79,21 @@ func TestLoadX509TrustStore(t *testing.T) {
|
|||
// load "ca" and "signingAuthority" trust store
|
||||
caStore := "ca:valid-trust-store"
|
||||
signingAuthorityStore := "signingAuthority:valid-trust-store"
|
||||
dummyPolicy := dummyOCIPolicyDocument().TrustPolicies[0]
|
||||
dummyPolicy := dummyPolicyStatement()
|
||||
dummyPolicy.TrustStores = []string{caStore, signingAuthorityStore}
|
||||
dir.UserConfigDir = "testdata"
|
||||
x509truststore := truststore.NewX509TrustStore(dir.ConfigFS())
|
||||
_, err := loadX509TrustStores(context.Background(), signature.SigningSchemeX509, dummyPolicy.Name, dummyPolicy.TrustStores, x509truststore)
|
||||
caCerts, err := loadX509TrustStores(context.Background(), signature.SigningSchemeX509, &dummyPolicy, x509truststore)
|
||||
if err != nil {
|
||||
t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err)
|
||||
}
|
||||
_, err = loadX509TrustStores(context.Background(), signature.SigningSchemeX509SigningAuthority, dummyPolicy.Name, dummyPolicy.TrustStores, x509truststore)
|
||||
saCerts, err := loadX509TrustStores(context.Background(), signature.SigningSchemeX509SigningAuthority, &dummyPolicy, x509truststore)
|
||||
if err != nil {
|
||||
t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err)
|
||||
}
|
||||
if len(caCerts) != 4 || len(saCerts) != 3 {
|
||||
t.Fatalf("ca store should have 4 certs and signingAuthority store should have 3 certs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCriticalFailure(t *testing.T) {
|
||||
|
@ -95,34 +117,6 @@ func TestIsCriticalFailure(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestLoadX509TSATrustStores(t *testing.T) {
|
||||
policyDoc := trustpolicy.Document{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{
|
||||
{
|
||||
Name: "testTSA",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"tsa:test-timestamp"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
dir.UserConfigDir = "testdata"
|
||||
x509truststore := truststore.NewX509TrustStore(dir.ConfigFS())
|
||||
policyStatement := policyDoc.TrustPolicies[0]
|
||||
_, err := loadX509TSATrustStores(context.Background(), signature.SigningSchemeX509, policyStatement.Name, policyStatement.TrustStores, x509truststore)
|
||||
if err != nil {
|
||||
t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err)
|
||||
}
|
||||
|
||||
_, err = loadX509TSATrustStores(context.Background(), signature.SigningSchemeX509SigningAuthority, policyStatement.Name, policyStatement.TrustStores, x509truststore)
|
||||
expectedErrMsg := "error while loading the TSA trust store, signing scheme must be notary.x509, but got notary.x509.signingAuthority"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
}
|
||||
|
||||
func getArtifactDigestFromReference(artifactReference string) (string, error) {
|
||||
invalidUriErr := fmt.Errorf("artifact URI %q could not be parsed, make sure it is the fully qualified OCI artifact URI without the scheme/protocol. e.g domain.com:80/my/repository@sha256:digest", artifactReference)
|
||||
i := strings.LastIndex(artifactReference, "@")
|
||||
|
@ -137,32 +131,3 @@ func getArtifactDigestFromReference(artifactReference string) (string, error) {
|
|||
|
||||
return artifactReference[i+1:], nil
|
||||
}
|
||||
|
||||
func dummyOCIPolicyDocument() (policyDoc trustpolicy.OCIDocument) {
|
||||
return trustpolicy.OCIDocument{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func dummyBlobPolicyDocument() (policyDoc trustpolicy.BlobDocument) {
|
||||
return trustpolicy.BlobDocument{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.BlobTrustPolicy{
|
||||
{
|
||||
Name: "blob-test-statement-name",
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
|
||||
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
|
@ -1 +0,0 @@
|
|||
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6YzA2NjllZjM0Y2RjMTQzMzJjMGYxYWIwYzJjMDFhY2I5MWQ5NjAxNGIxNzJmMWE3NmYzYTM5ZTYzZDFmMGJkYSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo1Mjh9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDI0LTA2LTE4VDE3OjA4OjM1KzA4OjAwIn0","header":{"x5c":["MIIDPjCCAiagAwIBAgIBeTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEPMA0GA1UEAxMGYWxwaW5lMB4XDTIzMDUwOTA0NTUxMloXDTMzMDUxMDA0NTUxMlowTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxDzANBgNVBAMTBmFscGluZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5hpq1229GGLjMK6i9KZhuUO+SV7rUFnWIDiIPO5yWxYDkl+bGroeAvJYu6MVCMQ6FMRXD9jhnG6R+sAHwY7gVgcJ1OXak87PkLp/Ii1Cr7XkkySZeD+Br1vSQzfxs3pFG+iBCeVVkeZdsg+xqwnAlqAILXwIbTGRyJP1Xiu9nwOeuX1YmxPl2m29Pt1EtfVCL9COsVKt5LgOVyWP/9ISWevOBqSCU9bk35HFo9VTeUf6+ffhSMjv0Y9uwkFFOKXpcV8Sa3ArqyBmgQlUfGg1iwYlqiDE0fTYxiB3gLgETAlmTm50J+WB9LoDrnrQpbXFLoegm+JV+uSD8J8H7DL2sCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAt0Nvna1c4pPn8kzoN5VvmFmeIgdO/BJpmdhdg0WIQ9aeN/xPXXaVjPp1Mk7edXHAvBwQr0Gyzqyy7g/h0gdnAFG7f6blrRNzbrRBCq6cNqX8iwgK/9+2OYKxk1QWj8Gx0cvu1DN1aXjPPGgQ2j3tHjJvJv32J/zuZa8gU40RPPSLaBlc5ZjpFmyi29sKlTeeZ+F/Ssic51qXXw2CsYGGWK5yQ3xSCxbw6bb2G/s/YI7/KlWg9BktBJHzRu04ZNR77W7/dyJ3Lj17PlW1XKmMOFHsQivagXeRCbmYZ43fX4ugFRFKL7KE0EgmGOWpJ0xv+6ig93sqHzQ/0uv1YgFov"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"ToCyclYJtk-Gtb13j1sWW7FQ7iZA9Vq6u_x6nJD3pRkBXhtatvSBsaZ_mqFHKrJWEY3UOBzi2SYobCQYww0cVwbzeDetPhjBhmH-bW-N_pbjGntgB2K1owvJnlycUoOfC2RQ1eDa4mC7Dj1mKzA5Tb-qnNbrT75pvQKZjTY1RZaN6p_xKBJA-AAiQrgHEvlf4m8ZbvqtZ0x4_uiGwfWoNCqPtrZK71mEpPSjfOT3mN5FkZqY0L3jSKRtFRLd1rb0UA2RB-E0CshsNb-hJgTX4SIzUlgcVT10SJnKw0yy_QqrxhMlejOUiV8HHKgbsZqQg1kwFjP5QwzWr5HB6vbRzg"}
|
|
@ -1,20 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDPjCCAiagAwIBAgIBeTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
|
||||
MA0GA1UEAxMGYWxwaW5lMB4XDTIzMDUwOTA0NTUxMloXDTMzMDUxMDA0NTUxMlow
|
||||
TjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8w
|
||||
DQYDVQQKEwZOb3RhcnkxDzANBgNVBAMTBmFscGluZTCCASIwDQYJKoZIhvcNAQEB
|
||||
BQADggEPADCCAQoCggEBAK5hpq1229GGLjMK6i9KZhuUO+SV7rUFnWIDiIPO5yWx
|
||||
YDkl+bGroeAvJYu6MVCMQ6FMRXD9jhnG6R+sAHwY7gVgcJ1OXak87PkLp/Ii1Cr7
|
||||
XkkySZeD+Br1vSQzfxs3pFG+iBCeVVkeZdsg+xqwnAlqAILXwIbTGRyJP1Xiu9nw
|
||||
OeuX1YmxPl2m29Pt1EtfVCL9COsVKt5LgOVyWP/9ISWevOBqSCU9bk35HFo9VTeU
|
||||
f6+ffhSMjv0Y9uwkFFOKXpcV8Sa3ArqyBmgQlUfGg1iwYlqiDE0fTYxiB3gLgETA
|
||||
lmTm50J+WB9LoDrnrQpbXFLoegm+JV+uSD8J8H7DL2sCAwEAAaMnMCUwDgYDVR0P
|
||||
AQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IB
|
||||
AQAt0Nvna1c4pPn8kzoN5VvmFmeIgdO/BJpmdhdg0WIQ9aeN/xPXXaVjPp1Mk7ed
|
||||
XHAvBwQr0Gyzqyy7g/h0gdnAFG7f6blrRNzbrRBCq6cNqX8iwgK/9+2OYKxk1QWj
|
||||
8Gx0cvu1DN1aXjPPGgQ2j3tHjJvJv32J/zuZa8gU40RPPSLaBlc5ZjpFmyi29sKl
|
||||
TeeZ+F/Ssic51qXXw2CsYGGWK5yQ3xSCxbw6bb2G/s/YI7/KlWg9BktBJHzRu04Z
|
||||
NR77W7/dyJ3Lj17PlW1XKmMOFHsQivagXeRCbmYZ43fX4ugFRFKL7KE0EgmGOWpJ
|
||||
0xv+6ig93sqHzQ/0uv1YgFov
|
||||
-----END CERTIFICATE-----
|
|
@ -1,20 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDRTCCAi2gAwIBAgICAKYwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMx
|
||||
CzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3Rhcnkx
|
||||
EDAOBgNVBAMTB3Rlc3RUU0EwIhgPMjA5OTA5MTgxMTU0MzRaGA8yMTAwMDkxODEx
|
||||
NTQzNFowTzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0
|
||||
dGxlMQ8wDQYDVQQKEwZOb3RhcnkxEDAOBgNVBAMTB3Rlc3RUU0EwggEiMA0GCSqG
|
||||
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDI7xKl3GyBZregnHgxUw7rb3yO5jSo31Pa
|
||||
+EhxghQ0/rRKc/1DtfMQURjDYDdjqRmEXq8rVyEAuaBXSKqBMq9bazP7Ot8N/B0O
|
||||
gRCgXwizn//Ha5XfpHqV9lUud4oztdxapejfT6UQSIVqtgWEbZkr4N74G5NV13Ll
|
||||
ITtWmHpTLo2LfE7jAXTaoCjo/U/eVFFc6X7jyXwaAVyNC2Pi45d/GOaFx/MGHnK6
|
||||
zbN8PeIh5KqInp0UNcHZLBbduxWQhdISULR/x6pVocqExv6zLmRbn5I65wrYL/8g
|
||||
pQPTeZv4S2COpB+25Xy8oyaM6tPa96Pi1NIXtChWO8+muXj1Z4VfAgMBAAGjJzAl
|
||||
MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0B
|
||||
AQsFAAOCAQEAXFaaITvi3skq+czzmbyebtrAa8I9iEbjmWSPjoaUir2NYOLWsyQ7
|
||||
+gkBlMcw5+anP+BC98VBgNVjuQ5oXwdu57xouW7jk/dI5uuKLOFxFdCG7FwW3ycD
|
||||
6GGgj+/2LthxNOxc7CnnMjUuSw2FKJKesiuHQJpdPjgw9cKs+fZF5tr6ZhX4yAUF
|
||||
qouZJ7Hc5JSj3zyEpIbFapVpSAK8O1/mct4KDtt1SmyYn34o55ggyLurrlZ9ctQW
|
||||
HT8xyjc6+b4lEKbilA+xjTt+/BLIs/v/8CVIUzz6OzTCwBraj3kayM7CdGKSysoc
|
||||
nJZ/yUcHVw1hLs1+JIMj75i0T6s+GtuT4A==
|
||||
-----END CERTIFICATE-----
|
Binary file not shown.
|
@ -1,20 +0,0 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEb
|
||||
MBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMz
|
||||
MDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQH
|
||||
EwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3
|
||||
b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7h
|
||||
sGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYE
|
||||
s4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3
|
||||
w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwK
|
||||
GvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9
|
||||
X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMH
|
||||
PsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsG
|
||||
AQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrW
|
||||
LYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDA
|
||||
e2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvj
|
||||
FE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/i
|
||||
oe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qo
|
||||
zfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo
|
||||
-----END CERTIFICATE-----
|
Binary file not shown.
Binary file not shown.
|
@ -1,425 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package verifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"net/http"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/revocation"
|
||||
"github.com/notaryproject/notation-core-go/revocation/purpose"
|
||||
"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"
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
)
|
||||
|
||||
func TestAuthenticTimestamp(t *testing.T) {
|
||||
dir.UserConfigDir = "testdata"
|
||||
trustStore := truststore.NewX509TrustStore(dir.ConfigFS())
|
||||
dummyTrustPolicy := &trustpolicy.TrustPolicy{
|
||||
Name: "test-timestamp",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
VerifyTimestamp: trustpolicy.OptionAlways,
|
||||
},
|
||||
TrustStores: []string{"ca:valid-trust-store", "tsa:test-timestamp"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
}
|
||||
revocationTimestampingValidator, err := revocation.NewWithOptions(revocation.Options{
|
||||
OCSPHTTPClient: &http.Client{Timeout: 2 * time.Second},
|
||||
CertChainPurpose: purpose.Timestamping,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get revocation timestamp client: %v", err)
|
||||
}
|
||||
// valid JWS signature envelope with timestamp countersignature
|
||||
jwsEnvContent, err := parseEnvContent("testdata/timestamp/sigEnv/jwsWithTimestamp.sig", jws.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
|
||||
// valid COSE signature envelope with timestamp countersignature
|
||||
coseEnvContent, err := parseEnvContent("testdata/timestamp/sigEnv/coseWithTimestamp.sig", cose.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
|
||||
t.Run("verify Authentic Timestamp with jws format", func(t *testing.T) {
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: jwsEnvContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
if err := authenticTimestampResult.Error; err != nil {
|
||||
t.Fatalf("expected nil error, but got %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp with cose format", func(t *testing.T) {
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: coseEnvContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
if err := authenticTimestampResult.Error; err != nil {
|
||||
t.Fatalf("expected nil error, but got %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp jws with expired codeSigning cert", func(t *testing.T) {
|
||||
jwsEnvContent, err := parseEnvContent("testdata/timestamp/sigEnv/jwsExpiredWithTimestamp.sig", jws.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: jwsEnvContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
if err := authenticTimestampResult.Error; err != nil {
|
||||
t.Fatalf("expected nil error, but got %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp cose with expired codeSigning cert", func(t *testing.T) {
|
||||
coseEnvContent, err := parseEnvContent("testdata/timestamp/sigEnv/coseExpiredWithTimestamp.sig", cose.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: coseEnvContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
if err := authenticTimestampResult.Error; err != nil {
|
||||
t.Fatalf("expected nil error, but got %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp with afterCertExpiry set", func(t *testing.T) {
|
||||
dummyTrustPolicy := &trustpolicy.TrustPolicy{
|
||||
Name: "test-timestamp",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
VerifyTimestamp: trustpolicy.OptionAfterCertExpiry,
|
||||
},
|
||||
TrustStores: []string{"ca:valid-trust-store", "tsa:test-timestamp"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: coseEnvContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
if err := authenticTimestampResult.Error; err != nil {
|
||||
t.Fatalf("expected nil error, but got %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to invalid trust policy", func(t *testing.T) {
|
||||
dummyTrustPolicy := &trustpolicy.TrustPolicy{
|
||||
Name: "test-timestamp",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
VerifyTimestamp: trustpolicy.OptionAlways,
|
||||
},
|
||||
TrustStores: []string{"ca:valid-trust-store", "tsa"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: jwsEnvContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "failed to check tsa trust store configuration in turst policy with error: invalid trust policy statement: \"test-timestamp\" is missing separator in trust store value \"tsa\". The required format is <TrustStoreType>:<TrustStoreName>"
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to missing tsa in trust policy and expired codeSigning cert", func(t *testing.T) {
|
||||
dummyTrustPolicy := &trustpolicy.TrustPolicy{
|
||||
Name: "test-timestamp",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
VerifyTimestamp: trustpolicy.OptionAlways,
|
||||
},
|
||||
TrustStores: []string{"ca:valid-trust-store"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
}
|
||||
coseEnvContent, err := parseEnvContent("testdata/timestamp/sigEnv/coseExpiredWithTimestamp.sig", cose.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: coseEnvContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "verification time is after certificate \"CN=testTSA,O=Notary,L=Seattle,ST=WA,C=US\" validity period, it was expired at \"Tue, 18 Jun 2024 07:30:31 +0000\""
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to missing timestamp countersignature", func(t *testing.T) {
|
||||
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/withoutTimestamp.sig", jws.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: envContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "no timestamp countersignature was found in the signature envelope"
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to invalid timestamp countersignature content type", func(t *testing.T) {
|
||||
signedToken, err := os.ReadFile("testdata/timestamp/countersignature/TimeStampTokenWithInvalideContentType.p7s")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signedToken: %v", err)
|
||||
}
|
||||
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/withoutTimestamp.sig", jws.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
envContent.SignerInfo.UnsignedAttributes.TimestampSignature = signedToken
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: envContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "failed to parse timestamp countersignature with error: unexpected content type: 1.2.840.113549.1.7.1. Expected to be id-ct-TSTInfo (1.2.840.113549.1.9.16.1.4)"
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to invalid TSTInfo", func(t *testing.T) {
|
||||
signedToken, err := os.ReadFile("testdata/timestamp/countersignature/TimeStampTokenWithInvalidTSTInfo.p7s")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signedToken: %v", err)
|
||||
}
|
||||
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/withoutTimestamp.sig", jws.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
envContent.SignerInfo.UnsignedAttributes.TimestampSignature = signedToken
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: envContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "failed to get the timestamp TSTInfo with error: cannot unmarshal TSTInfo from timestamp token: asn1: structure error: tags don't match (23 vs {class:0 tag:16 length:3 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:24 set:false omitEmpty:false} Time @89"
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to failed to validate TSTInfo", func(t *testing.T) {
|
||||
signedToken, err := os.ReadFile("testdata/timestamp/countersignature/TimeStampToken.p7s")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signedToken: %v", err)
|
||||
}
|
||||
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/withoutTimestamp.sig", jws.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
envContent.SignerInfo.UnsignedAttributes.TimestampSignature = signedToken
|
||||
envContent.SignerInfo.Signature = []byte("mismatch")
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: envContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "failed to get timestamp from timestamp countersignature with error: invalid TSTInfo: mismatched message"
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to failed to verify timestamp countersignature", func(t *testing.T) {
|
||||
signedToken, err := os.ReadFile("testdata/timestamp/countersignature/TimeStampTokenWithoutCertificate.p7s")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signedToken: %v", err)
|
||||
}
|
||||
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/withoutTimestamp.sig", jws.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
envContent.SignerInfo.UnsignedAttributes.TimestampSignature = signedToken
|
||||
envContent.SignerInfo.Signature = []byte("notation")
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: envContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "failed to verify the timestamp countersignature with error: failed to verify signed token: signing certificate not found in the timestamp token"
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to trust store does not exist", func(t *testing.T) {
|
||||
dummyTrustPolicy := &trustpolicy.TrustPolicy{
|
||||
Name: "test-timestamp",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
VerifyTimestamp: trustpolicy.OptionAlways,
|
||||
},
|
||||
TrustStores: []string{"ca:valid-trust-store", "tsa:does-not-exist"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: coseEnvContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "failed to load tsa trust store with error: the trust store \"does-not-exist\" of type \"tsa\" does not exist"
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to empty trust store", func(t *testing.T) {
|
||||
dummyTrustPolicy := &trustpolicy.TrustPolicy{
|
||||
Name: "test-timestamp",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
VerifyTimestamp: trustpolicy.OptionAlways,
|
||||
},
|
||||
TrustStores: []string{"ca:valid-trust-store", "tsa:test-empty"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: coseEnvContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, dummyTrustStore{}, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "no trusted TSA certificate found in trust store"
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to tsa not trust", func(t *testing.T) {
|
||||
dummyTrustPolicy := &trustpolicy.TrustPolicy{
|
||||
Name: "test-timestamp",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
VerifyTimestamp: trustpolicy.OptionAlways,
|
||||
},
|
||||
TrustStores: []string{"ca:valid-trust-store", "tsa:test-mismatch"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: coseEnvContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "failed to verify the timestamp countersignature with error: failed to verify signed token: cms verification failure: x509: certificate signed by unknown authority"
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to timestamp before signing cert not before", func(t *testing.T) {
|
||||
dummyTrustPolicy := &trustpolicy.TrustPolicy{
|
||||
Name: "test-timestamp",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
VerifyTimestamp: trustpolicy.OptionAlways,
|
||||
},
|
||||
TrustStores: []string{"ca:valid-trust-store", "tsa:test-timestamp"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
}
|
||||
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/timestampBeforeNotBefore.sig", jws.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: envContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "timestamp can be before certificate \"CN=testTSA,O=Notary,L=Seattle,ST=WA,C=US\" validity period, it will be valid from \"Fri, 18 Sep 2099 11:54:34 +0000\""
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify Authentic Timestamp failed due to timestamp after signing cert not after", func(t *testing.T) {
|
||||
dummyTrustPolicy := &trustpolicy.TrustPolicy{
|
||||
Name: "test-timestamp",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
VerifyTimestamp: trustpolicy.OptionAlways,
|
||||
},
|
||||
TrustStores: []string{"ca:valid-trust-store", "tsa:test-timestamp"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
}
|
||||
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/timestampAfterNotAfter.sig", cose.MediaTypeEnvelope)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signature envelope content: %v", err)
|
||||
}
|
||||
outcome := ¬ation.VerificationOutcome{
|
||||
EnvelopeContent: envContent,
|
||||
VerificationLevel: trustpolicy.LevelStrict,
|
||||
}
|
||||
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
|
||||
expectedErrMsg := "timestamp can be after certificate \"CN=testTSA,O=Notary,L=Seattle,ST=WA,C=US\" validity period, it was expired at \"Tue, 18 Sep 2001 11:54:34 +0000\""
|
||||
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func parseEnvContent(filepath, format string) (*signature.EnvelopeContent, error) {
|
||||
sigEnvBytes, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sigEnv, err := signature.ParseEnvelope(format, sigEnvBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sigEnv.Content()
|
||||
}
|
||||
|
||||
type dummyTrustStore struct{}
|
||||
|
||||
func (ts dummyTrustStore) GetCertificates(ctx context.Context, storeType truststore.Type, namedStore string) ([]*x509.Certificate, error) {
|
||||
return nil, nil
|
||||
}
|
|
@ -1,148 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package trustpolicy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
set "github.com/notaryproject/notation-go/internal/container"
|
||||
"github.com/notaryproject/notation-go/internal/slices"
|
||||
)
|
||||
|
||||
// BlobDocument represents a trustpolicy.blob.json document for arbitrary blobs
|
||||
type BlobDocument struct {
|
||||
// Version of the policy document
|
||||
Version string `json:"version"`
|
||||
|
||||
// TrustPolicies include each policy statement
|
||||
TrustPolicies []BlobTrustPolicy `json:"trustPolicies"`
|
||||
}
|
||||
|
||||
// BlobTrustPolicy represents a policy statement in the blob trust policy
|
||||
// document
|
||||
type BlobTrustPolicy struct {
|
||||
// Name of the policy statement
|
||||
Name string `json:"name"`
|
||||
|
||||
// SignatureVerification setting for this policy statement
|
||||
SignatureVerification SignatureVerification `json:"signatureVerification"`
|
||||
|
||||
// TrustStores this policy statement uses
|
||||
TrustStores []string `json:"trustStores"`
|
||||
|
||||
// TrustedIdentities this policy statement pins
|
||||
TrustedIdentities []string `json:"trustedIdentities"`
|
||||
|
||||
// GlobalPolicy defines if policy statement is global or not
|
||||
GlobalPolicy bool `json:"globalPolicy,omitempty"`
|
||||
}
|
||||
|
||||
var supportedBlobPolicyVersions = []string{"1.0"}
|
||||
|
||||
// LoadBlobDocument loads a blob trust policy document from a local file system
|
||||
func LoadBlobDocument() (*BlobDocument, error) {
|
||||
var doc BlobDocument
|
||||
err := getDocument(dir.PathBlobTrustPolicy, &doc)
|
||||
return &doc, err
|
||||
}
|
||||
|
||||
// Validate validates a blob trust policy document according to its version's
|
||||
// rule set.
|
||||
// If any rule is violated, returns an error.
|
||||
func (policyDoc *BlobDocument) Validate() error {
|
||||
// sanity check
|
||||
if policyDoc == nil {
|
||||
return errors.New("blob trust policy document cannot be nil")
|
||||
}
|
||||
|
||||
// Validate Version
|
||||
if policyDoc.Version == "" {
|
||||
return errors.New("blob trust policy document has empty version, version must be specified")
|
||||
}
|
||||
if !slices.Contains(supportedBlobPolicyVersions, policyDoc.Version) {
|
||||
return fmt.Errorf("blob trust policy document uses unsupported version %q", policyDoc.Version)
|
||||
}
|
||||
|
||||
// Validate the policy according to 1.0 rules
|
||||
if len(policyDoc.TrustPolicies) == 0 {
|
||||
return errors.New("blob trust policy document can not have zero trust policy statements")
|
||||
}
|
||||
policyNames := set.New[string]()
|
||||
var foundGlobalPolicy bool
|
||||
for _, statement := range policyDoc.TrustPolicies {
|
||||
// Verify unique policy statement names across the policy document
|
||||
if policyNames.Contains(statement.Name) {
|
||||
return fmt.Errorf("multiple blob trust policy statements use the same name %q, statement names must be unique", statement.Name)
|
||||
}
|
||||
if err := validatePolicyCore(statement.Name, statement.SignatureVerification, statement.TrustStores, statement.TrustedIdentities); err != nil {
|
||||
return fmt.Errorf("blob trust policy: %w", err)
|
||||
}
|
||||
if statement.GlobalPolicy {
|
||||
if foundGlobalPolicy {
|
||||
return errors.New("multiple blob trust policy statements have globalPolicy set to true. Only one trust policy statement can be marked as global policy")
|
||||
}
|
||||
|
||||
// verificationLevel is skip
|
||||
if reflect.DeepEqual(statement.SignatureVerification.VerificationLevel, LevelSkip) {
|
||||
return errors.New("global blob trust policy statement cannot have verification level set to skip")
|
||||
}
|
||||
foundGlobalPolicy = true
|
||||
}
|
||||
policyNames.Add(statement.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetApplicableTrustPolicy returns a pointer to the deep copied [BlobTrustPolicy]
|
||||
// for given policy name.
|
||||
// see https://github.com/notaryproject/specifications/tree/9c81dc773508dedc5a81c02c8d805de04f65050b/specs/trust-store-trust-policy.md#blob-trust-policy
|
||||
func (policyDoc *BlobDocument) GetApplicableTrustPolicy(policyName string) (*BlobTrustPolicy, error) {
|
||||
if strings.TrimSpace(policyName) == "" {
|
||||
return nil, errors.New("policy name cannot be empty")
|
||||
}
|
||||
for _, policyStatement := range policyDoc.TrustPolicies {
|
||||
// exact match
|
||||
if policyStatement.Name == policyName {
|
||||
return (&policyStatement).clone(), nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no applicable blob trust policy with name %q", policyName)
|
||||
}
|
||||
|
||||
// GetGlobalTrustPolicy returns a pointer to the deep copied [BlobTrustPolicy]
|
||||
// that is marked as global policy.
|
||||
// see https://github.com/notaryproject/specifications/tree/9c81dc773508dedc5a81c02c8d805de04f65050b/specs/trust-store-trust-policy.md#blob-trust-policy
|
||||
func (policyDoc *BlobDocument) GetGlobalTrustPolicy() (*BlobTrustPolicy, error) {
|
||||
for _, policyStatement := range policyDoc.TrustPolicies {
|
||||
if policyStatement.GlobalPolicy {
|
||||
return (&policyStatement).clone(), nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no global blob trust policy")
|
||||
}
|
||||
|
||||
// clone returns a pointer to the deep copied [BlobTrustPolicy]
|
||||
func (t *BlobTrustPolicy) clone() *BlobTrustPolicy {
|
||||
return &BlobTrustPolicy{
|
||||
Name: t.Name,
|
||||
SignatureVerification: t.SignatureVerification,
|
||||
TrustedIdentities: append([]string(nil), t.TrustedIdentities...),
|
||||
TrustStores: append([]string(nil), t.TrustStores...),
|
||||
GlobalPolicy: t.GlobalPolicy,
|
||||
}
|
||||
}
|
|
@ -1,168 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package trustpolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
)
|
||||
|
||||
func TestLoadBlobDocument(t *testing.T) {
|
||||
tempRoot := t.TempDir()
|
||||
dir.UserConfigDir = tempRoot
|
||||
path := filepath.Join(tempRoot, "trustpolicy.blob.json")
|
||||
policyJson, _ := json.Marshal(dummyBlobPolicyDocument())
|
||||
if err := os.WriteFile(path, policyJson, 0600); err != nil {
|
||||
t.Fatalf("TestLoadBlobDocument write policy file failed. Error: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { os.RemoveAll(tempRoot) })
|
||||
|
||||
if _, err := LoadBlobDocument(); err != nil {
|
||||
t.Fatalf("LoadBlobDocument() should not throw error for an existing policy file. Error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_BlobDocument(t *testing.T) {
|
||||
policyDoc := dummyBlobPolicyDocument()
|
||||
if err := policyDoc.Validate(); err != nil {
|
||||
t.Fatalf("Validate() returned error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidate_BlobDocument_Error(t *testing.T) {
|
||||
// Sanity check
|
||||
var nilPolicyDoc *BlobDocument
|
||||
err := nilPolicyDoc.Validate()
|
||||
if err == nil || err.Error() != "blob trust policy document cannot be nil" {
|
||||
t.Fatalf("nil policyDoc should return error")
|
||||
}
|
||||
|
||||
// empty Version
|
||||
policyDoc := dummyBlobPolicyDocument()
|
||||
policyDoc.Version = ""
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "blob trust policy document has empty version, version must be specified" {
|
||||
t.Fatalf("empty version should return error")
|
||||
}
|
||||
|
||||
// Invalid Version
|
||||
policyDoc = dummyBlobPolicyDocument()
|
||||
policyDoc.Version = "invalid"
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "blob trust policy document uses unsupported version \"invalid\"" {
|
||||
t.Fatalf("invalid version should return error")
|
||||
}
|
||||
|
||||
// No Policy Statements
|
||||
policyDoc = dummyBlobPolicyDocument()
|
||||
policyDoc.TrustPolicies = nil
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "blob trust policy document can not have zero trust policy statements" {
|
||||
t.Fatalf("zero policy statements should return error")
|
||||
}
|
||||
|
||||
// No Policy Statement Name
|
||||
policyDoc = dummyBlobPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].Name = ""
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "blob trust policy: a trust policy statement is missing a name, every statement requires a name" {
|
||||
t.Fatalf("policy statement with no name should return an error")
|
||||
}
|
||||
|
||||
// multiple global rust policy
|
||||
policyDoc = dummyBlobPolicyDocument()
|
||||
policyStatement1 := policyDoc.TrustPolicies[0].clone()
|
||||
policyStatement1.GlobalPolicy = true
|
||||
policyStatement2 := policyDoc.TrustPolicies[0].clone()
|
||||
policyStatement2.Name = "test-statement-name-2"
|
||||
policyStatement2.GlobalPolicy = true
|
||||
policyDoc.TrustPolicies = []BlobTrustPolicy{*policyStatement1, *policyStatement2}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "multiple blob trust policy statements have globalPolicy set to true. Only one trust policy statement can be marked as global policy" {
|
||||
t.Error(err)
|
||||
t.Fatalf("multiple global blob policy should return error")
|
||||
}
|
||||
|
||||
// Policy Document with duplicate policy statement names
|
||||
policyDoc = dummyBlobPolicyDocument()
|
||||
policyStatement1 = policyDoc.TrustPolicies[0].clone()
|
||||
policyStatement2 = policyDoc.TrustPolicies[0].clone()
|
||||
policyDoc.TrustPolicies = []BlobTrustPolicy{*policyStatement1, *policyStatement2}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "multiple blob trust policy statements use the same name \"test-statement-name\", statement names must be unique" {
|
||||
t.Fatalf("policy statements with same name should return error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetApplicableTrustPolicy(t *testing.T) {
|
||||
policyDoc := dummyBlobPolicyDocument()
|
||||
|
||||
policyStatement := policyDoc.TrustPolicies[0].clone()
|
||||
policyStatement1 := policyStatement.clone()
|
||||
policyStatement1.Name = "test-statement-name-1"
|
||||
policyStatement1.GlobalPolicy = true
|
||||
policyStatement2 := policyStatement.clone()
|
||||
policyStatement2.Name = "test-statement-name-2"
|
||||
policyDoc.TrustPolicies = []BlobTrustPolicy{*policyStatement, *policyStatement1, *policyStatement2}
|
||||
|
||||
validateGetApplicableTrustPolicy(t, policyDoc, "test-statement-name-2", policyStatement2)
|
||||
validateGetApplicableTrustPolicy(t, policyDoc, "test-statement-name", policyStatement)
|
||||
}
|
||||
|
||||
func TestGetApplicableTrustPolicy_Error(t *testing.T) {
|
||||
policyDoc := dummyBlobPolicyDocument()
|
||||
t.Run("empty policy name", func(t *testing.T) {
|
||||
_, err := policyDoc.GetApplicableTrustPolicy("")
|
||||
if err == nil || err.Error() != "policy name cannot be empty" {
|
||||
t.Fatalf("GetApplicableTrustPolicy() returned error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non existent policy name", func(t *testing.T) {
|
||||
_, err := policyDoc.GetApplicableTrustPolicy("blaah")
|
||||
if err == nil || err.Error() != "no applicable blob trust policy with name \"blaah\"" {
|
||||
t.Fatalf("GetApplicableTrustPolicy() returned error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetGlobalTrustPolicy(t *testing.T) {
|
||||
policyDoc := dummyBlobPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].GlobalPolicy = true
|
||||
|
||||
policy, err := policyDoc.GetGlobalTrustPolicy()
|
||||
if err != nil {
|
||||
t.Fatalf("GetGlobalTrustPolicy() returned error: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(*policy, policyDoc.TrustPolicies[0]) {
|
||||
t.Fatalf("GetGlobalTrustPolicy() returned unexpected policy")
|
||||
}
|
||||
}
|
||||
|
||||
func validateGetApplicableTrustPolicy(t *testing.T, policyDoc BlobDocument, policyName string, expectedPolicy *BlobTrustPolicy) {
|
||||
policy, err := policyDoc.GetApplicableTrustPolicy(policyName)
|
||||
if err != nil {
|
||||
t.Fatalf("GetApplicableTrustPolicy() returned error: %v", err)
|
||||
}
|
||||
|
||||
if reflect.DeepEqual(policy, *expectedPolicy) {
|
||||
t.Fatalf("GetApplicableTrustPolicy() returned unexpected policy for %s", policyName)
|
||||
}
|
||||
}
|
|
@ -1,254 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package trustpolicy
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
set "github.com/notaryproject/notation-go/internal/container"
|
||||
"github.com/notaryproject/notation-go/internal/slices"
|
||||
"github.com/notaryproject/notation-go/internal/trustpolicy"
|
||||
)
|
||||
|
||||
// OCIDocument represents a trustpolicy.oci.json document for OCI artifacts
|
||||
type OCIDocument struct {
|
||||
// Version of the policy document
|
||||
Version string `json:"version"`
|
||||
|
||||
// TrustPolicies include each policy statement
|
||||
TrustPolicies []OCITrustPolicy `json:"trustPolicies"`
|
||||
}
|
||||
|
||||
// OCITrustPolicy represents a policy statement in the OCI trust policy document
|
||||
type OCITrustPolicy struct {
|
||||
// Name of the policy statement
|
||||
Name string `json:"name"`
|
||||
|
||||
// SignatureVerification setting for this policy statement
|
||||
SignatureVerification SignatureVerification `json:"signatureVerification"`
|
||||
|
||||
// TrustStores this policy statement uses
|
||||
TrustStores []string `json:"trustStores"`
|
||||
|
||||
// TrustedIdentities this policy statement pins
|
||||
TrustedIdentities []string `json:"trustedIdentities"`
|
||||
|
||||
// RegistryScopes that this policy statement affects
|
||||
RegistryScopes []string `json:"registryScopes"`
|
||||
}
|
||||
|
||||
// Document represents a trustPolicy.json document
|
||||
//
|
||||
// Deprecated: Document exists for historical compatibility and
|
||||
// should not be used. To create OCI Document, use [OCIDocument].
|
||||
type Document = OCIDocument
|
||||
|
||||
// TrustPolicy represents a policy statement in the policy document
|
||||
//
|
||||
// Deprecated: TrustPolicy exists for historical compatibility and
|
||||
// should not be used. To create OCI TrustPolicy, use [OCITrustPolicy].
|
||||
type TrustPolicy = OCITrustPolicy
|
||||
|
||||
// LoadDocument loads a trust policy document from a local file system
|
||||
//
|
||||
// Deprecated: LoadDocument function exists for historical compatibility and
|
||||
// should not be used. To load OCI Document, use [LoadOCIDocument] function.
|
||||
var LoadDocument = LoadOCIDocument
|
||||
|
||||
var supportedOCIPolicyVersions = []string{"1.0"}
|
||||
|
||||
// LoadOCIDocument retrieves a trust policy document from the local file system.
|
||||
// It attempts to read from [dir.PathOCITrustPolicy] first; if not found,
|
||||
// it tries [dir.PathTrustPolicy].
|
||||
// If both dir.PathOCITrustPolicy and dir.PathTrustPolicy exist,
|
||||
// dir.PathOCITrustPolicy will be read.
|
||||
func LoadOCIDocument() (*OCIDocument, error) {
|
||||
var doc OCIDocument
|
||||
|
||||
// attempt to load the document from dir.PathOCITrustPolicy
|
||||
if err := getDocument(dir.PathOCITrustPolicy, &doc); err != nil {
|
||||
// if the document is not found at the first path, try the second path
|
||||
if errors.As(err, &errPolicyNotExist{}) {
|
||||
if err := getDocument(dir.PathTrustPolicy, &doc); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
// if an error occurred other than the document not found, return it
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
// Validate validates a policy document according to its version's rule set.
|
||||
// if any rule is violated, returns an error
|
||||
func (policyDoc *OCIDocument) Validate() error {
|
||||
// sanity check
|
||||
if policyDoc == nil {
|
||||
return errors.New("oci trust policy document cannot be nil")
|
||||
}
|
||||
|
||||
// Validate Version
|
||||
if policyDoc.Version == "" {
|
||||
return errors.New("oci trust policy document has empty version, version must be specified")
|
||||
}
|
||||
if !slices.Contains(supportedOCIPolicyVersions, policyDoc.Version) {
|
||||
return fmt.Errorf("oci trust policy document uses unsupported version %q", policyDoc.Version)
|
||||
}
|
||||
|
||||
// Validate the policy according to 1.0 rules
|
||||
if len(policyDoc.TrustPolicies) == 0 {
|
||||
return errors.New("oci trust policy document can not have zero trust policy statements")
|
||||
}
|
||||
policyNames := set.New[string]()
|
||||
for _, statement := range policyDoc.TrustPolicies {
|
||||
// Verify unique policy statement names across the policy document
|
||||
if policyNames.Contains(statement.Name) {
|
||||
return fmt.Errorf("multiple oci trust policy statements use the same name %q, statement names must be unique", statement.Name)
|
||||
}
|
||||
if err := validatePolicyCore(statement.Name, statement.SignatureVerification, statement.TrustStores, statement.TrustedIdentities); err != nil {
|
||||
return fmt.Errorf("oci trust policy: %w", err)
|
||||
}
|
||||
policyNames.Add(statement.Name)
|
||||
}
|
||||
|
||||
// Verify registry scopes are valid
|
||||
if err := validateRegistryScopes(policyDoc); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetApplicableTrustPolicy returns a pointer to the deep copied [OCITrustPolicy]
|
||||
// statement that applies to the given registry scope. If no applicable trust
|
||||
// policy is found, returns an error.
|
||||
// see https://github.com/notaryproject/specifications/tree/9c81dc773508dedc5a81c02c8d805de04f65050b/specs/trust-store-trust-policy.md#selecting-a-trust-policy-based-on-artifact-uri
|
||||
func (policyDoc *OCIDocument) GetApplicableTrustPolicy(artifactReference string) (*OCITrustPolicy, error) {
|
||||
artifactPath, err := getArtifactPathFromReference(artifactReference)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var wildcardPolicy *OCITrustPolicy
|
||||
var applicablePolicy *OCITrustPolicy
|
||||
for _, policyStatement := range policyDoc.TrustPolicies {
|
||||
if slices.Contains(policyStatement.RegistryScopes, trustpolicy.Wildcard) {
|
||||
// we need to deep copy because we can't use the loop variable
|
||||
// address. see https://stackoverflow.com/a/45967429
|
||||
wildcardPolicy = (&policyStatement).clone()
|
||||
} else if slices.Contains(policyStatement.RegistryScopes, artifactPath) {
|
||||
applicablePolicy = (&policyStatement).clone()
|
||||
}
|
||||
}
|
||||
if applicablePolicy != nil {
|
||||
// a policy with exact match for registry scope takes precedence over
|
||||
// a wildcard (*) policy.
|
||||
return applicablePolicy, nil
|
||||
} else if wildcardPolicy != nil {
|
||||
return wildcardPolicy, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("artifact %q has no applicable oci trust policy statement. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: %s", artifactReference, trustPolicyLink)
|
||||
}
|
||||
}
|
||||
|
||||
// clone returns a pointer to the deep copied [OCITrustPolicy]
|
||||
func (t *OCITrustPolicy) clone() *OCITrustPolicy {
|
||||
return &OCITrustPolicy{
|
||||
Name: t.Name,
|
||||
SignatureVerification: t.SignatureVerification,
|
||||
TrustedIdentities: append([]string(nil), t.TrustedIdentities...),
|
||||
TrustStores: append([]string(nil), t.TrustStores...),
|
||||
RegistryScopes: append([]string(nil), t.RegistryScopes...),
|
||||
}
|
||||
}
|
||||
|
||||
// validateRegistryScopes validates if the policy document is following the
|
||||
// Notary Project spec rules for registry scopes
|
||||
func validateRegistryScopes(policyDoc *OCIDocument) error {
|
||||
registryScopeCount := make(map[string]int)
|
||||
for _, statement := range policyDoc.TrustPolicies {
|
||||
// Verify registry scopes are valid
|
||||
if len(statement.RegistryScopes) == 0 {
|
||||
return fmt.Errorf("oci trust policy statement %q has zero registry scopes, it must specify registry scopes with at least one value", statement.Name)
|
||||
}
|
||||
if len(statement.RegistryScopes) > 1 && slices.Contains(statement.RegistryScopes, trustpolicy.Wildcard) {
|
||||
return fmt.Errorf("oci trust policy statement %q uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values", statement.Name)
|
||||
}
|
||||
for _, scope := range statement.RegistryScopes {
|
||||
if scope != trustpolicy.Wildcard {
|
||||
if err := validateRegistryScopeFormat(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
registryScopeCount[scope]++
|
||||
}
|
||||
}
|
||||
|
||||
// Verify one policy statement per registry scope
|
||||
for key := range registryScopeCount {
|
||||
if registryScopeCount[key] > 1 {
|
||||
return fmt.Errorf("registry scope %q is present in multiple oci trust policy statements, one registry scope value can only be associated with one statement", key)
|
||||
}
|
||||
}
|
||||
|
||||
// No error
|
||||
return nil
|
||||
}
|
||||
|
||||
func getArtifactPathFromReference(artifactReference string) (string, error) {
|
||||
// TODO support more types of URI like "domain.com/repository",
|
||||
// "domain.com/repository:tag"
|
||||
i := strings.LastIndex(artifactReference, "@")
|
||||
if i < 0 {
|
||||
return "", fmt.Errorf("artifact URI %q could not be parsed, make sure it is the fully qualified oci artifact URI without the scheme/protocol. e.g domain.com:80/my/repository@sha256:digest", artifactReference)
|
||||
}
|
||||
artifactPath := artifactReference[:i]
|
||||
if err := validateRegistryScopeFormat(artifactPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return artifactPath, nil
|
||||
}
|
||||
|
||||
// validateRegistryScopeFormat validates if a scope is following the format
|
||||
// defined in distribution spec
|
||||
func validateRegistryScopeFormat(scope string) error {
|
||||
// Domain and Repository regexes are adapted from distribution
|
||||
// implementation
|
||||
// https://github.com/distribution/distribution/blob/main/reference/regexp.go#L31
|
||||
domainRegexp := regexp.MustCompile(`^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?$`)
|
||||
repositoryRegexp := regexp.MustCompile(`^[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`)
|
||||
ensureMessage := "make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout"
|
||||
errorMessage := "registry scope %q is not valid, " + ensureMessage
|
||||
errorWildCardMessage := "registry scope %q with wild card(s) is not valid, " + ensureMessage
|
||||
|
||||
// Check for presence of * in scope
|
||||
if len(scope) > 1 && strings.Contains(scope, "*") {
|
||||
return fmt.Errorf(errorWildCardMessage, scope)
|
||||
}
|
||||
domain, repository, found := strings.Cut(scope, "/")
|
||||
if !found {
|
||||
return fmt.Errorf(errorMessage, scope)
|
||||
}
|
||||
if domain == "" || repository == "" || !domainRegexp.MatchString(domain) || !repositoryRegexp.MatchString(repository) {
|
||||
return fmt.Errorf(errorMessage, scope)
|
||||
}
|
||||
|
||||
// No errors
|
||||
return nil
|
||||
}
|
|
@ -1,394 +0,0 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package trustpolicy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
)
|
||||
|
||||
func TestLoadOCIDocumentFromOldFileLocation(t *testing.T) {
|
||||
tempRoot := t.TempDir()
|
||||
dir.UserConfigDir = tempRoot
|
||||
path := filepath.Join(tempRoot, "trustpolicy.json")
|
||||
policyJson, _ := json.Marshal(dummyOCIPolicyDocument())
|
||||
if err := os.WriteFile(path, policyJson, 0600); err != nil {
|
||||
t.Fatalf("TestLoadOCIDocument write policy file failed. Error: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { os.RemoveAll(tempRoot) })
|
||||
|
||||
if _, err := LoadOCIDocument(); err != nil {
|
||||
t.Fatalf("LoadOCIDocument() should not throw error for an existing policy file. Error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOCIDocumentFromNewFileLocation(t *testing.T) {
|
||||
tempRoot := t.TempDir()
|
||||
dir.UserConfigDir = tempRoot
|
||||
path := filepath.Join(tempRoot, "trustpolicy.oci.json")
|
||||
policyJson, _ := json.Marshal(dummyOCIPolicyDocument())
|
||||
if err := os.WriteFile(path, policyJson, 0600); err != nil {
|
||||
t.Fatalf("TestLoadOCIDocument write policy file failed. Error: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { os.RemoveAll(tempRoot) })
|
||||
|
||||
if _, err := LoadOCIDocument(); err != nil {
|
||||
t.Fatalf("LoadOCIDocument() should not throw error for an existing policy file. Error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOCIDocumentError(t *testing.T) {
|
||||
tempRoot := t.TempDir()
|
||||
dir.UserConfigDir = tempRoot
|
||||
if _, err := LoadOCIDocument(); err == nil {
|
||||
t.Fatalf("LoadOCIDocument() should throw error if OCI trust policy is not found")
|
||||
}
|
||||
}
|
||||
|
||||
// TestApplicableTrustPolicy tests filtering policies against registry scopes
|
||||
func TestApplicableTrustPolicy(t *testing.T) {
|
||||
policyDoc := dummyOCIPolicyDocument()
|
||||
|
||||
policyStatement := policyDoc.TrustPolicies[0]
|
||||
policyStatement.Name = "test-statement-name-1"
|
||||
registryScope := "registry.wabbit-networks.io/software/unsigned/net-utils"
|
||||
registryUri := fmt.Sprintf("%s@sha256:hash", registryScope)
|
||||
policyStatement.RegistryScopes = []string{registryScope}
|
||||
policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "strict"}
|
||||
|
||||
policyDoc.TrustPolicies = []OCITrustPolicy{
|
||||
policyStatement,
|
||||
}
|
||||
// existing Registry Scope
|
||||
policy, err := (&policyDoc).GetApplicableTrustPolicy(registryUri)
|
||||
if policy.Name != policyStatement.Name || err != nil {
|
||||
t.Fatalf("GetApplicableTrustPolicy() should return %q for registry scope %q", policyStatement.Name, registryScope)
|
||||
}
|
||||
|
||||
// non-existing Registry Scope
|
||||
policy, err = (&policyDoc).GetApplicableTrustPolicy("non.existing.scope/repo@sha256:hash")
|
||||
if policy != nil || err == nil || err.Error() != "artifact \"non.existing.scope/repo@sha256:hash\" has no applicable oci trust policy statement. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: https://notaryproject.dev/docs/quickstart/#create-a-trust-policy" {
|
||||
t.Fatalf("GetApplicableTrustPolicy() should return nil for non existing registry scope")
|
||||
}
|
||||
|
||||
// wildcard registry scope
|
||||
wildcardStatement := OCITrustPolicy{
|
||||
Name: "test-statement-name-2",
|
||||
SignatureVerification: SignatureVerification{VerificationLevel: "skip"},
|
||||
TrustStores: []string{},
|
||||
TrustedIdentities: []string{},
|
||||
RegistryScopes: []string{"*"},
|
||||
}
|
||||
|
||||
policyDoc.TrustPolicies = []OCITrustPolicy{
|
||||
policyStatement,
|
||||
wildcardStatement,
|
||||
}
|
||||
policy, err = (&policyDoc).GetApplicableTrustPolicy("some.registry.that/has.no.policy@sha256:hash")
|
||||
if policy.Name != wildcardStatement.Name || err != nil {
|
||||
t.Fatalf("GetApplicableTrustPolicy() should return wildcard policy for registry scope \"some.registry.that/has.no.policy\"")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidatePolicyDocument calls policyDoc.Validate()
|
||||
// and tests various validations on policy elements
|
||||
func TestValidateInvalidPolicyDocument(t *testing.T) {
|
||||
// Sanity check
|
||||
var nilPolicyDoc *OCIDocument
|
||||
err := nilPolicyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy document cannot be nil" {
|
||||
t.Fatalf("nil policyDoc should return error")
|
||||
}
|
||||
|
||||
// Invalid Version
|
||||
policyDoc := dummyOCIPolicyDocument()
|
||||
policyDoc.Version = "invalid"
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy document uses unsupported version \"invalid\"" {
|
||||
t.Fatalf("invalid version should return error")
|
||||
}
|
||||
|
||||
// No Policy Statements
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies = nil
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy document can not have zero trust policy statements" {
|
||||
t.Fatalf("zero policy statements should return error")
|
||||
}
|
||||
|
||||
// No Policy Statement Name
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].Name = ""
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: a trust policy statement is missing a name, every statement requires a name" {
|
||||
t.Fatalf("policy statement with no name should return an error")
|
||||
}
|
||||
|
||||
// No Registry Scopes
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].RegistryScopes = nil
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy statement \"test-statement-name\" has zero registry scopes, it must specify registry scopes with at least one value" {
|
||||
t.Fatalf("policy statement with registry scopes should return error")
|
||||
}
|
||||
|
||||
// Multiple policy statements with same registry scope
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyStatement1 := policyDoc.TrustPolicies[0].clone()
|
||||
policyStatement2 := policyDoc.TrustPolicies[0].clone()
|
||||
policyStatement2.Name = "test-statement-name-2"
|
||||
policyDoc.TrustPolicies = []OCITrustPolicy{*policyStatement1, *policyStatement2}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "registry scope \"registry.acme-rockets.io/software/net-monitor\" is present in multiple oci trust policy statements, one registry scope value can only be associated with one statement" {
|
||||
t.Fatalf("Policy statements with same registry scope should return error %q", err)
|
||||
}
|
||||
|
||||
// Registry scopes with a wildcard
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].RegistryScopes = []string{"*", "registry.acme-rockets.io/software/net-monitor"}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy statement \"test-statement-name\" uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values" {
|
||||
t.Fatalf("policy statement with more than a wildcard registry scope should return error")
|
||||
}
|
||||
|
||||
// Invalid SignatureVerification
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].SignatureVerification = SignatureVerification{VerificationLevel: "invalid"}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has invalid signatureVerification: invalid signature verification level \"invalid\"" {
|
||||
t.Fatalf("policy statement with invalid SignatureVerification should return error")
|
||||
}
|
||||
|
||||
// Invalid SignatureVerification VerifyTimestamp
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].SignatureVerification.VerifyTimestamp = "invalid"
|
||||
expectedErrMsg := "oci trust policy: trust policy statement \"test-statement-name\" has invalid signatureVerification: verifyTimestamp must be \"always\" or \"afterCertExpiry\", but got \"invalid\""
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
|
||||
// strict SignatureVerification should have a trust store
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].TrustStores = []string{}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" {
|
||||
t.Fatalf("strict SignatureVerification should have a trust store")
|
||||
}
|
||||
|
||||
// strict SignatureVerification should have trusted identities
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].TrustedIdentities = []string{}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" {
|
||||
t.Fatalf("strict SignatureVerification should have trusted identities")
|
||||
}
|
||||
|
||||
// skip SignatureVerification should not have trust store or trusted identities
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].SignatureVerification = SignatureVerification{VerificationLevel: "skip"}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped" {
|
||||
t.Fatalf("strict SignatureVerification should have trusted identities")
|
||||
}
|
||||
|
||||
// Empty Trusted Identity should throw error
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].TrustedIdentities = []string{""}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has an empty trusted identity" {
|
||||
t.Fatalf("policy statement with empty trusted identity should return error")
|
||||
}
|
||||
|
||||
// Trusted Identity without separator should throw error
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].TrustedIdentities = []string{"x509.subject"}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has trusted identity \"x509.subject\" missing separator" {
|
||||
t.Fatalf("policy statement with trusted identity missing separator should return error")
|
||||
}
|
||||
|
||||
// Empty Trusted Identity value should throw error
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].TrustedIdentities = []string{"x509.subject:"}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:\" without an identity value" {
|
||||
t.Fatalf("policy statement with trusted identity missing identity value should return error")
|
||||
}
|
||||
|
||||
// trust store/trusted identities are optional for skip SignatureVerification
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].SignatureVerification = SignatureVerification{VerificationLevel: "skip"}
|
||||
policyDoc.TrustPolicies[0].TrustStores = []string{}
|
||||
policyDoc.TrustPolicies[0].TrustedIdentities = []string{}
|
||||
err = policyDoc.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("skip SignatureVerification should not require a trust store or trusted identities")
|
||||
}
|
||||
|
||||
// Trust Store missing separator
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].TrustStores = []string{"ca"}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has malformed trust store value \"ca\". The required format is <TrustStoreType>:<TrustStoreName>" {
|
||||
t.Fatalf("policy statement with trust store missing separator should return error")
|
||||
}
|
||||
|
||||
// Invalid Trust Store type
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].TrustStores = []string{"invalid:test-trust-store"}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" uses an unsupported trust store type \"invalid\" in trust store value \"invalid:test-trust-store\"" {
|
||||
t.Fatalf("policy statement with invalid trust store type should return error")
|
||||
}
|
||||
|
||||
// Empty Named Store
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].TrustStores = []string{"ca:"}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" uses an unsupported trust store name \"\" in trust store value \"ca:\". Named store name needs to follow [a-zA-Z0-9_.-]+ format" {
|
||||
t.Fatalf("policy statement with trust store missing named store should return error")
|
||||
}
|
||||
|
||||
// trusted identities with a wildcard
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyDoc.TrustPolicies[0].TrustedIdentities = []string{"*", "test-identity"}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values" {
|
||||
t.Fatalf("policy statement with more than a wildcard trusted identity should return error")
|
||||
}
|
||||
|
||||
// Policy Document with duplicate policy statement names
|
||||
policyDoc = dummyOCIPolicyDocument()
|
||||
policyStatement1 = policyDoc.TrustPolicies[0].clone()
|
||||
policyStatement2 = policyDoc.TrustPolicies[0].clone()
|
||||
policyStatement2.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"}
|
||||
policyDoc.TrustPolicies = []OCITrustPolicy{*policyStatement1, *policyStatement2}
|
||||
err = policyDoc.Validate()
|
||||
if err == nil || err.Error() != "multiple oci trust policy statements use the same name \"test-statement-name\", statement names must be unique" {
|
||||
t.Fatalf("policy statements with same name should return error")
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidRegistryScopes tests valid scopes are accepted
|
||||
func TestValidRegistryScopes(t *testing.T) {
|
||||
policyDoc := dummyOCIPolicyDocument()
|
||||
validScopes := []string{
|
||||
"*", "example.com/rep", "example.com:8080/rep/rep2", "example.com/rep/subrep/subsub",
|
||||
"10.10.10.10:8080/rep/rep2", "domain/rep", "domain:1234/rep",
|
||||
}
|
||||
|
||||
for _, scope := range validScopes {
|
||||
policyDoc.TrustPolicies[0].RegistryScopes = []string{scope}
|
||||
err := policyDoc.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("valid registry scope should not return error. Error : %q", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvalidRegistryScopes tests invalid scopes are rejected
|
||||
func TestInvalidRegistryScopes(t *testing.T) {
|
||||
policyDoc := dummyOCIPolicyDocument()
|
||||
invalidScopes := []string{
|
||||
"", "1:1", "a,b", "abcd", "1111", "1,2", "example.com/rep:tag",
|
||||
"example.com/rep/subrep/sub:latest", "example.com", "rep/rep2:latest",
|
||||
"repository", "10.10.10.10", "10.10.10.10:8080/rep/rep2:latest",
|
||||
}
|
||||
|
||||
for _, scope := range invalidScopes {
|
||||
policyDoc.TrustPolicies[0].RegistryScopes = []string{scope}
|
||||
err := policyDoc.Validate()
|
||||
if err == nil || err.Error() != "registry scope \""+scope+"\" is not valid, make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" {
|
||||
t.Fatalf("invalid registry scope should return error. Error : %q", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test invalid scope with wild card suffix
|
||||
invalidWildCardScopes := []string{"example.com/*", "*/", "example*/", "ex*test"}
|
||||
for _, scope := range invalidWildCardScopes {
|
||||
policyDoc.TrustPolicies[0].RegistryScopes = []string{scope}
|
||||
err := policyDoc.Validate()
|
||||
if err == nil || err.Error() != "registry scope \""+scope+"\" with wild card(s) is not valid, make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" {
|
||||
t.Fatalf("invalid registry scope should return error. Error : %q", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateValidPolicyDocument tests a happy policy document
|
||||
func TestValidateValidPolicyDocument(t *testing.T) {
|
||||
policyDoc := dummyOCIPolicyDocument()
|
||||
|
||||
policyStatement1 := policyDoc.TrustPolicies[0].clone()
|
||||
|
||||
policyStatement2 := policyStatement1.clone()
|
||||
policyStatement2.Name = "test-statement-name-2"
|
||||
policyStatement2.RegistryScopes = []string{"registry.wabbit-networks.io/software/unsigned/net-utils"}
|
||||
policyStatement2.SignatureVerification = SignatureVerification{VerificationLevel: "permissive"}
|
||||
|
||||
policyStatement3 := policyStatement1.clone()
|
||||
policyStatement3.Name = "test-statement-name-3"
|
||||
policyStatement3.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"}
|
||||
policyStatement3.TrustStores = []string{}
|
||||
policyStatement3.TrustedIdentities = []string{}
|
||||
policyStatement3.SignatureVerification = SignatureVerification{VerificationLevel: "skip"}
|
||||
|
||||
policyStatement4 := policyStatement1.clone()
|
||||
policyStatement4.Name = "test-statement-name-4"
|
||||
policyStatement4.RegistryScopes = []string{"*"}
|
||||
policyStatement4.TrustStores = []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store-2"}
|
||||
policyStatement4.SignatureVerification = SignatureVerification{VerificationLevel: "audit"}
|
||||
|
||||
policyStatement5 := policyStatement1.clone()
|
||||
policyStatement5.Name = "test-statement-name-5"
|
||||
policyStatement5.RegistryScopes = []string{"registry.acme-rockets2.io/software"}
|
||||
policyStatement5.TrustedIdentities = []string{"*"}
|
||||
policyStatement5.SignatureVerification = SignatureVerification{VerificationLevel: "strict"}
|
||||
|
||||
policyStatement6 := policyStatement1.clone()
|
||||
policyStatement6.Name = "test-statement-name-6"
|
||||
policyStatement6.RegistryScopes = []string{"registry.acme-rockets.io/software/net-monitor6"}
|
||||
policyStatement6.SignatureVerification.VerifyTimestamp = ""
|
||||
|
||||
policyStatement7 := policyStatement1.clone()
|
||||
policyStatement7.Name = "test-statement-name-7"
|
||||
policyStatement7.RegistryScopes = []string{"registry.acme-rockets.io/software/net-monitor7"}
|
||||
policyStatement7.SignatureVerification.VerifyTimestamp = OptionAlways
|
||||
|
||||
policyStatement8 := policyStatement1.clone()
|
||||
policyStatement8.Name = "test-statement-name-8"
|
||||
policyStatement8.RegistryScopes = []string{"registry.acme-rockets.io/software/net-monitor8"}
|
||||
policyStatement8.SignatureVerification.VerifyTimestamp = OptionAfterCertExpiry
|
||||
|
||||
policyDoc.TrustPolicies = []OCITrustPolicy{
|
||||
*policyStatement1,
|
||||
*policyStatement2,
|
||||
*policyStatement3,
|
||||
*policyStatement4,
|
||||
*policyStatement5,
|
||||
*policyStatement6,
|
||||
*policyStatement7,
|
||||
*policyStatement8,
|
||||
}
|
||||
|
||||
err := policyDoc.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("validation failed on a good policy document. Error : %q", err)
|
||||
}
|
||||
}
|
|
@ -21,6 +21,8 @@ import (
|
|||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
|
@ -63,19 +65,6 @@ const (
|
|||
ActionSkip ValidationAction = "skip"
|
||||
)
|
||||
|
||||
// TimestampOption is an enum for timestamp verifiction options such as Always,
|
||||
// AfterCertExpiry.
|
||||
type TimestampOption string
|
||||
|
||||
const (
|
||||
// OptionAlways denotes always perform timestamp verification
|
||||
OptionAlways TimestampOption = "always"
|
||||
|
||||
// OptionAfterCertExpiry denotes perform timestamp verification only if
|
||||
// the signing certificate chain has expired
|
||||
OptionAfterCertExpiry TimestampOption = "afterCertExpiry"
|
||||
)
|
||||
|
||||
var (
|
||||
LevelStrict = &VerificationLevel{
|
||||
Name: "strict",
|
||||
|
@ -145,22 +134,191 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
var supportedPolicyVersions = []string{"1.0"}
|
||||
|
||||
// Document represents a trustPolicy.json document
|
||||
type Document struct {
|
||||
// Version of the policy document
|
||||
Version string `json:"version"`
|
||||
|
||||
// TrustPolicies include each policy statement
|
||||
TrustPolicies []TrustPolicy `json:"trustPolicies"`
|
||||
}
|
||||
|
||||
// TrustPolicy represents a policy statement in the policy document
|
||||
type TrustPolicy struct {
|
||||
// Name of the policy statement
|
||||
Name string `json:"name"`
|
||||
|
||||
// RegistryScopes that this policy statement affects
|
||||
RegistryScopes []string `json:"registryScopes"`
|
||||
|
||||
// SignatureVerification setting for this policy statement
|
||||
SignatureVerification SignatureVerification `json:"signatureVerification"`
|
||||
|
||||
// TrustStores this policy statement uses
|
||||
TrustStores []string `json:"trustStores,omitempty"`
|
||||
|
||||
// TrustedIdentities this policy statement pins
|
||||
TrustedIdentities []string `json:"trustedIdentities,omitempty"`
|
||||
}
|
||||
|
||||
// SignatureVerification represents verification configuration in a trust policy
|
||||
type SignatureVerification struct {
|
||||
VerificationLevel string `json:"level"`
|
||||
Override map[ValidationType]ValidationAction `json:"override,omitempty"`
|
||||
VerifyTimestamp TimestampOption `json:"verifyTimestamp,omitempty"`
|
||||
}
|
||||
|
||||
type errPolicyNotExist struct{}
|
||||
// Validate validates a policy document according to its version's rule set.
|
||||
// if any rule is violated, returns an error
|
||||
func (policyDoc *Document) Validate() error {
|
||||
// sanity check
|
||||
if policyDoc == nil {
|
||||
return errors.New("trust policy document cannot be nil")
|
||||
}
|
||||
|
||||
func (e errPolicyNotExist) Error() string {
|
||||
return fmt.Sprintf("trust policy is not present. To create a trust policy, see: %s", trustPolicyLink)
|
||||
// Validate Version
|
||||
if policyDoc.Version == "" {
|
||||
return errors.New("trust policy document is missing or has empty version, it must be specified")
|
||||
}
|
||||
if !slices.Contains(supportedPolicyVersions, policyDoc.Version) {
|
||||
return fmt.Errorf("trust policy document uses unsupported version %q", policyDoc.Version)
|
||||
}
|
||||
|
||||
// Validate the policy according to 1.0 rules
|
||||
if len(policyDoc.TrustPolicies) == 0 {
|
||||
return errors.New("trust policy document can not have zero trust policy statements")
|
||||
}
|
||||
|
||||
policyStatementNameCount := make(map[string]int)
|
||||
|
||||
for _, statement := range policyDoc.TrustPolicies {
|
||||
|
||||
// Verify statement name is valid
|
||||
if statement.Name == "" {
|
||||
return errors.New("a trust policy statement is missing a name, every statement requires a name")
|
||||
}
|
||||
policyStatementNameCount[statement.Name]++
|
||||
|
||||
// Verify signature verification is valid
|
||||
verificationLevel, err := statement.SignatureVerification.GetVerificationLevel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("trust policy statement %q has invalid signatureVerification: %w", statement.Name, err)
|
||||
}
|
||||
|
||||
// Any signature verification other than "skip" needs a trust store and
|
||||
// trusted identities
|
||||
if verificationLevel.Name == "skip" {
|
||||
if len(statement.TrustStores) > 0 || len(statement.TrustedIdentities) > 0 {
|
||||
return fmt.Errorf("trust policy statement %q is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped", statement.Name)
|
||||
}
|
||||
} else {
|
||||
if len(statement.TrustStores) == 0 || len(statement.TrustedIdentities) == 0 {
|
||||
return fmt.Errorf("trust policy statement %q is either missing trust stores or trusted identities, both must be specified", statement.Name)
|
||||
}
|
||||
|
||||
// Verify Trust Store is valid
|
||||
if err := validateTrustStore(statement); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify Trusted Identities are valid
|
||||
if err := validateTrustedIdentities(statement); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Verify registry scopes are valid
|
||||
if err := validateRegistryScopes(policyDoc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify unique policy statement names across the policy document
|
||||
for key := range policyStatementNameCount {
|
||||
if policyStatementNameCount[key] > 1 {
|
||||
return fmt.Errorf("multiple trust policy statements use the same name %q, statement names must be unique", key)
|
||||
}
|
||||
}
|
||||
|
||||
// No errors
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetVerificationLevel returns [VerificationLevel] for the given
|
||||
// [SignatureVerification] struct.
|
||||
// It throws error if SignatureVerification is invalid.
|
||||
// GetApplicableTrustPolicy returns a pointer to the deep copied TrustPolicy
|
||||
// statement that applies to the given registry scope. If no applicable trust
|
||||
// policy is found, returns an error
|
||||
// see https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.2/specs/trust-store-trust-policy.md#selecting-a-trust-policy-based-on-artifact-uri
|
||||
func (trustPolicyDoc *Document) GetApplicableTrustPolicy(artifactReference string) (*TrustPolicy, error) {
|
||||
artifactPath, err := getArtifactPathFromReference(artifactReference)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var wildcardPolicy *TrustPolicy
|
||||
var applicablePolicy *TrustPolicy
|
||||
for _, policyStatement := range trustPolicyDoc.TrustPolicies {
|
||||
if slices.Contains(policyStatement.RegistryScopes, trustpolicy.Wildcard) {
|
||||
// we need to deep copy because we can't use the loop variable
|
||||
// address. see https://stackoverflow.com/a/45967429
|
||||
wildcardPolicy = (&policyStatement).clone()
|
||||
} else if slices.Contains(policyStatement.RegistryScopes, artifactPath) {
|
||||
applicablePolicy = (&policyStatement).clone()
|
||||
}
|
||||
}
|
||||
|
||||
if applicablePolicy != nil {
|
||||
// a policy with exact match for registry scope takes precedence over
|
||||
// a wildcard (*) policy.
|
||||
return applicablePolicy, nil
|
||||
} else if wildcardPolicy != nil {
|
||||
return wildcardPolicy, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("artifact %q has no applicable trust policy. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: %s", artifactReference, trustPolicyLink)
|
||||
}
|
||||
}
|
||||
|
||||
// LoadDocument loads a trust policy document from a local file system
|
||||
func LoadDocument() (*Document, error) {
|
||||
path, err := dir.ConfigFS().SysPath(dir.PathTrustPolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// throw error if path is a directory or a symlink or does not exist.
|
||||
fileInfo, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, fmt.Errorf("trust policy is not present. To create a trust policy, see: %s", trustPolicyLink)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mode := fileInfo.Mode()
|
||||
if mode.IsDir() || mode&fs.ModeSymlink != 0 {
|
||||
return nil, fmt.Errorf("trust policy is not a regular file (symlinks are not supported). To create a trust policy, see: %s", trustPolicyLink)
|
||||
}
|
||||
|
||||
jsonFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
return nil, fmt.Errorf("unable to read trust policy due to file permissions, please verify the permissions of %s", filepath.Join(dir.UserConfigDir, dir.PathTrustPolicy))
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer jsonFile.Close()
|
||||
|
||||
policyDocument := &Document{}
|
||||
err = json.NewDecoder(jsonFile).Decode(policyDocument)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("malformed trust policy. To create a trust policy, see: %s", trustPolicyLink)
|
||||
}
|
||||
return policyDocument, nil
|
||||
}
|
||||
|
||||
// GetVerificationLevel returns VerificationLevel struct for the given
|
||||
// SignatureVerification struct throws error if SignatureVerification is invalid
|
||||
func (signatureVerification *SignatureVerification) GetVerificationLevel() (*VerificationLevel, error) {
|
||||
if signatureVerification.VerificationLevel == "" {
|
||||
return nil, errors.New("signature verification level is empty or missing in the trust policy statement")
|
||||
|
@ -175,13 +333,16 @@ func (signatureVerification *SignatureVerification) GetVerificationLevel() (*Ver
|
|||
if baseLevel == nil {
|
||||
return nil, fmt.Errorf("invalid signature verification level %q", signatureVerification.VerificationLevel)
|
||||
}
|
||||
|
||||
if len(signatureVerification.Override) == 0 {
|
||||
// nothing to override, return the base verification level
|
||||
return baseLevel, nil
|
||||
}
|
||||
|
||||
if baseLevel == LevelSkip {
|
||||
return nil, fmt.Errorf("signature verification level %q can't be used to customize signature verification", baseLevel.Name)
|
||||
}
|
||||
|
||||
customVerificationLevel := &VerificationLevel{
|
||||
Name: "custom",
|
||||
Enforcement: make(map[ValidationType]ValidationAction),
|
||||
|
@ -216,139 +377,79 @@ func (signatureVerification *SignatureVerification) GetVerificationLevel() (*Ver
|
|||
if validationAction == "" {
|
||||
return nil, fmt.Errorf("verification action %q in custom signature verification is not supported, supported values are %q", value, ValidationActions)
|
||||
}
|
||||
|
||||
if validationType == TypeIntegrity {
|
||||
return nil, fmt.Errorf("%q verification can not be overridden in custom signature verification", key)
|
||||
} else if validationType != TypeRevocation && validationAction == ActionSkip {
|
||||
return nil, fmt.Errorf("%q verification can not be skipped in custom signature verification", key)
|
||||
}
|
||||
|
||||
customVerificationLevel.Enforcement[validationType] = validationAction
|
||||
}
|
||||
return customVerificationLevel, nil
|
||||
}
|
||||
|
||||
func getDocument(path string, v any) error {
|
||||
path, err := dir.ConfigFS().SysPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
// clone returns a pointer to the deeply copied TrustPolicy
|
||||
func (t *TrustPolicy) clone() *TrustPolicy {
|
||||
return &TrustPolicy{
|
||||
Name: t.Name,
|
||||
SignatureVerification: t.SignatureVerification,
|
||||
RegistryScopes: append([]string(nil), t.RegistryScopes...),
|
||||
TrustedIdentities: append([]string(nil), t.TrustedIdentities...),
|
||||
TrustStores: append([]string(nil), t.TrustStores...),
|
||||
}
|
||||
|
||||
// throw error if path is a directory or a symlink or does not exist.
|
||||
fileInfo, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return errPolicyNotExist{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
mode := fileInfo.Mode()
|
||||
if mode.IsDir() || mode&fs.ModeSymlink != 0 {
|
||||
return fmt.Errorf("trust policy is not a regular file (symlinks are not supported). To create a trust policy, see: %s", trustPolicyLink)
|
||||
}
|
||||
jsonFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrPermission) {
|
||||
return fmt.Errorf("unable to read trust policy due to file permissions, please verify the permissions of %s", path)
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer jsonFile.Close()
|
||||
|
||||
err = json.NewDecoder(jsonFile).Decode(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("malformed trust policy. To create a trust policy, see: %s", trustPolicyLink)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePolicyCore(name string, signatureVerification SignatureVerification, trustStores, trustedIdentities []string) error {
|
||||
// Verify statement name is valid
|
||||
if name == "" {
|
||||
return errors.New("a trust policy statement is missing a name, every statement requires a name")
|
||||
}
|
||||
|
||||
// Verify signature verification is valid
|
||||
verificationLevel, err := signatureVerification.GetVerificationLevel()
|
||||
if err != nil {
|
||||
return fmt.Errorf("trust policy statement %q has invalid signatureVerification: %w", name, err)
|
||||
}
|
||||
if signatureVerification.VerifyTimestamp != "" &&
|
||||
signatureVerification.VerifyTimestamp != OptionAlways &&
|
||||
signatureVerification.VerifyTimestamp != OptionAfterCertExpiry {
|
||||
return fmt.Errorf("trust policy statement %q has invalid signatureVerification: verifyTimestamp must be %q or %q, but got %q", name, OptionAlways, OptionAfterCertExpiry, signatureVerification.VerifyTimestamp)
|
||||
}
|
||||
|
||||
// Any signature verification other than "skip" needs a trust store and
|
||||
// trusted identities
|
||||
if verificationLevel.Name == "skip" {
|
||||
if len(trustStores) > 0 || len(trustedIdentities) > 0 {
|
||||
return fmt.Errorf("trust policy statement %q is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped", name)
|
||||
}
|
||||
} else {
|
||||
if len(trustStores) == 0 || len(trustedIdentities) == 0 {
|
||||
return fmt.Errorf("trust policy statement %q is either missing trust stores or trusted identities, both must be specified", name)
|
||||
}
|
||||
|
||||
// Verify Trust Store is valid
|
||||
if err := validateTrustStore(name, trustStores); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify Trusted Identities are valid
|
||||
if err := validateTrustedIdentities(name, trustedIdentities); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTrustStore validates if the policy statement is following the
|
||||
// Notary Project spec rules for truststore
|
||||
func validateTrustStore(policyName string, trustStores []string) error {
|
||||
for _, trustStore := range trustStores {
|
||||
// Notary Project spec rules for truststores
|
||||
func validateTrustStore(statement TrustPolicy) error {
|
||||
for _, trustStore := range statement.TrustStores {
|
||||
storeType, namedStore, found := strings.Cut(trustStore, ":")
|
||||
if !found {
|
||||
return fmt.Errorf("trust policy statement %q has malformed trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", policyName, trustStore)
|
||||
return fmt.Errorf("trust policy statement %q has malformed trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", statement.Name, trustStore)
|
||||
}
|
||||
if !isValidTrustStoreType(storeType) {
|
||||
return fmt.Errorf("trust policy statement %q uses an unsupported trust store type %q in trust store value %q", policyName, storeType, trustStore)
|
||||
return fmt.Errorf("trust policy statement %q uses an unsupported trust store type %q in trust store value %q", statement.Name, storeType, trustStore)
|
||||
}
|
||||
if !file.IsValidFileName(namedStore) {
|
||||
return fmt.Errorf("trust policy statement %q uses an unsupported trust store name %q in trust store value %q. Named store name needs to follow [a-zA-Z0-9_.-]+ format", policyName, namedStore, trustStore)
|
||||
return fmt.Errorf("trust policy statement %q uses an unsupported trust store name %q in trust store value %q. Named store name needs to follow [a-zA-Z0-9_.-]+ format", statement.Name, namedStore, trustStore)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateTrustedIdentities validates if the policy statement is following the
|
||||
// Notary Project spec rules for trusted identities
|
||||
func validateTrustedIdentities(policyName string, tis []string) error {
|
||||
// If there is a wildcard in trusted identities, there shouldn't be any other
|
||||
func validateTrustedIdentities(statement TrustPolicy) error {
|
||||
// If there is a wildcard in trusted identies, there shouldn't be any other
|
||||
//identities
|
||||
if len(tis) > 1 && slices.Contains(tis, trustpolicy.Wildcard) {
|
||||
return fmt.Errorf("trust policy statement %q uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values", policyName)
|
||||
if len(statement.TrustedIdentities) > 1 && slices.Contains(statement.TrustedIdentities, trustpolicy.Wildcard) {
|
||||
return fmt.Errorf("trust policy statement %q uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values", statement.Name)
|
||||
}
|
||||
|
||||
var parsedDNs []parsedDN
|
||||
// If there are trusted identities, verify they are valid
|
||||
for _, identity := range tis {
|
||||
for _, identity := range statement.TrustedIdentities {
|
||||
if identity == "" {
|
||||
return fmt.Errorf("trust policy statement %q has an empty trusted identity", policyName)
|
||||
return fmt.Errorf("trust policy statement %q has an empty trusted identity", statement.Name)
|
||||
}
|
||||
|
||||
if identity != trustpolicy.Wildcard {
|
||||
identityPrefix, identityValue, found := strings.Cut(identity, ":")
|
||||
if !found {
|
||||
return fmt.Errorf("trust policy statement %q has trusted identity %q missing separator", policyName, identity)
|
||||
return fmt.Errorf("trust policy statement %q has trusted identity %q missing separator", statement.Name, identity)
|
||||
}
|
||||
|
||||
// notation natively supports x509.subject identities only
|
||||
if identityPrefix == trustpolicy.X509Subject {
|
||||
// identityValue cannot be empty
|
||||
if identityValue == "" {
|
||||
return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity value", policyName, identity)
|
||||
return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity value", statement.Name, identity)
|
||||
}
|
||||
dn, err := pkix.ParseDistinguishedName(identityValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("trust policy statement %q has trusted identity %q with invalid identity value: %w", policyName, identity, err)
|
||||
return fmt.Errorf("trust policy statement %q has trusted identity %q with invalid identity value: %w", statement.Name, identity, err)
|
||||
}
|
||||
parsedDNs = append(parsedDNs, parsedDN{RawString: identity, ParsedMap: dn})
|
||||
}
|
||||
|
@ -356,7 +457,7 @@ func validateTrustedIdentities(policyName string, tis []string) error {
|
|||
}
|
||||
|
||||
// Verify there are no overlapping DNs
|
||||
if err := validateOverlappingDNs(policyName, parsedDNs); err != nil {
|
||||
if err := validateOverlappingDNs(statement.Name, parsedDNs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -364,6 +465,39 @@ func validateTrustedIdentities(policyName string, tis []string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// validateRegistryScopes validates if the policy document is following the
|
||||
// Notary Project spec rules for registry scopes
|
||||
func validateRegistryScopes(policyDoc *Document) error {
|
||||
registryScopeCount := make(map[string]int)
|
||||
for _, statement := range policyDoc.TrustPolicies {
|
||||
// Verify registry scopes are valid
|
||||
if len(statement.RegistryScopes) == 0 {
|
||||
return fmt.Errorf("trust policy statement %q has zero registry scopes, it must specify registry scopes with at least one value", statement.Name)
|
||||
}
|
||||
if len(statement.RegistryScopes) > 1 && slices.Contains(statement.RegistryScopes, trustpolicy.Wildcard) {
|
||||
return fmt.Errorf("trust policy statement %q uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values", statement.Name)
|
||||
}
|
||||
for _, scope := range statement.RegistryScopes {
|
||||
if scope != trustpolicy.Wildcard {
|
||||
if err := validateRegistryScopeFormat(scope); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
registryScopeCount[scope]++
|
||||
}
|
||||
}
|
||||
|
||||
// Verify one policy statement per registry scope
|
||||
for key := range registryScopeCount {
|
||||
if registryScopeCount[key] > 1 {
|
||||
return fmt.Errorf("registry scope %q is present in multiple trust policy statements, one registry scope value can only be associated with one statement", key)
|
||||
}
|
||||
}
|
||||
|
||||
// No error
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOverlappingDNs(policyName string, parsedDNs []parsedDN) error {
|
||||
for i, dn1 := range parsedDNs {
|
||||
for j, dn2 := range parsedDNs {
|
||||
|
@ -372,11 +506,12 @@ func validateOverlappingDNs(policyName string, parsedDNs []parsedDN) error {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidTrustStoreType returns true if the given string is a valid
|
||||
// [truststore.Type], otherwise false.
|
||||
// truststore.Type, otherwise false.
|
||||
func isValidTrustStoreType(s string) bool {
|
||||
for _, p := range truststore.Types {
|
||||
if s == string(p) {
|
||||
|
@ -386,8 +521,53 @@ func isValidTrustStoreType(s string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// parsedDN holds raw and parsed Distinguished Names
|
||||
func getArtifactPathFromReference(artifactReference string) (string, error) {
|
||||
// TODO support more types of URI like "domain.com/repository",
|
||||
// "domain.com/repository:tag"
|
||||
i := strings.LastIndex(artifactReference, "@")
|
||||
if i < 0 {
|
||||
return "", fmt.Errorf("artifact URI %q could not be parsed, make sure it is the fully qualified OCI artifact URI without the scheme/protocol. e.g domain.com:80/my/repository@sha256:digest", artifactReference)
|
||||
}
|
||||
|
||||
artifactPath := artifactReference[:i]
|
||||
if err := validateRegistryScopeFormat(artifactPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return artifactPath, nil
|
||||
}
|
||||
|
||||
// Internal type to hold raw and parsed Distinguished Names
|
||||
type parsedDN struct {
|
||||
RawString string
|
||||
ParsedMap map[string]string
|
||||
}
|
||||
|
||||
// validateRegistryScopeFormat validates if a scope is following the format
|
||||
// defined in distribution spec
|
||||
func validateRegistryScopeFormat(scope string) error {
|
||||
// Domain and Repository regexes are adapted from distribution
|
||||
// implementation
|
||||
// https://github.com/distribution/distribution/blob/main/reference/regexp.go#L31
|
||||
domainRegexp := regexp.MustCompile(`^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?$`)
|
||||
repositoryRegexp := regexp.MustCompile(`^[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`)
|
||||
ensureMessage := "make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout"
|
||||
errorMessage := "registry scope %q is not valid, " + ensureMessage
|
||||
errorWildCardMessage := "registry scope %q with wild card(s) is not valid, " + ensureMessage
|
||||
|
||||
// Check for presence of * in scope
|
||||
if len(scope) > 1 && strings.Contains(scope, "*") {
|
||||
return fmt.Errorf(errorWildCardMessage, scope)
|
||||
}
|
||||
|
||||
domain, repository, found := strings.Cut(scope, "/")
|
||||
if !found {
|
||||
return fmt.Errorf(errorMessage, scope)
|
||||
}
|
||||
|
||||
if domain == "" || repository == "" || !domainRegexp.MatchString(domain) || !repositoryRegexp.MatchString(repository) {
|
||||
return fmt.Errorf(errorMessage, scope)
|
||||
}
|
||||
|
||||
// No errors
|
||||
return nil
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue