Compare commits

..

No commits in common. "main" and "v0.8.0-alpha.1" have entirely different histories.

217 changed files with 3399 additions and 19254 deletions

21
.github/.codecov.yml vendored
View File

@ -1,21 +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.
coverage:
status:
project:
default:
target: 80%
patch:
default:
target: 80%

View File

@ -1,60 +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: 🐛 Bug or Issue
description: Something is not working as expected or not working at all! Report it here!
labels: [bug, triage]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this issue report. 🛑 Please check existing issues first before continuing: https://github.com/notaryproject/notation-go/issues
- type: textarea
id: verbatim
validations:
required: true
attributes:
label: "What is not working as expected?"
description: "In your own words, describe what the issue is."
- type: textarea
id: expect
validations:
required: true
attributes:
label: "What did you expect to happen?"
description: "A clear and concise description of what you expected to happen."
- type: textarea
id: reproduce
validations:
required: true
attributes:
label: "How can we reproduce it?"
description: "Detailed steps to reproduce the behavior, code snippets are welcome."
- type: textarea
id: environment
validations:
required: true
attributes:
label: Describe your environment
description: "OS and Golang version"
- type: textarea
id: version
validations:
required: true
attributes:
label: What is the version of your notation-go Library?
description: "Check the `go.mod` file for the library version."
- type: markdown
attributes:
value: |
If you want to contribute to this project, we will be happy to guide you through the contribution process especially when you already have a good proposal or understanding of how to fix this issue. Join us at https://slack.cncf.io/ and choose #notary-project channel.

View File

@ -1,18 +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.
blank_issues_enabled: false
contact_links:
- name: Ask a question
url: https://slack.cncf.io/
about: "Join #notary-project channel on CNCF Slack"

View File

@ -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.
name: 🚀 Feature Request
description: Suggest an idea for this project.
labels: [enhancement, triage]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to suggest a useful feature for the project!
- type: textarea
id: problem
validations:
required: true
attributes:
label: "Is your feature request related to a problem?"
description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]"
- type: textarea
id: solution
validations:
required: true
attributes:
label: "What solution do you propose?"
description: "A clear and concise description of what you want to happen."
- type: textarea
id: alternatives
validations:
required: true
attributes:
label: "What alternatives have you considered?"
description: "A clear and concise description of any alternative solutions or features you've considered."
- type: textarea
id: context
validations:
required: false
attributes:
label: "Any additional context?"
description: "Add any other context or screenshots about the feature request here."
- type: markdown
attributes:
value: |
If you want to contribute to this project, we will be happy to guide you through the contribution process especially when you already have a good proposal or understanding of how to improve the functionality. Join us at https://slack.cncf.io/ and choose #notary-project channel.

View File

@ -1,16 +1,3 @@
# 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.
version: 2
updates:
- package-ecosystem: "gomod"

44
.github/licenserc.yml vendored
View File

@ -1,44 +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.
header:
license:
spdx-id: Apache-2.0
content: |
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.
paths-ignore:
- '**/*.md'
- 'CODEOWNERS'
- 'LICENSE'
- 'MAINTAINERS'
- 'go.mod'
- 'go.sum'
- '**/testdata/**'
comment: on-failure
dependency:
files:
- go.mod

View File

@ -1,27 +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.
on:
issues:
types:
- opened
jobs:
add-to-project:
name: Add issue to project
runs-on: ubuntu-latest
steps:
- uses: actions/add-to-project@main
with:
project-url: https://github.com/orgs/notaryproject/projects/10
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}

View File

@ -1,30 +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: build
on:
push:
branches:
- main
- release-*
pull_request:
branches:
- main
- release-*
jobs:
build:
uses: notaryproject/notation-core-go/.github/workflows/reusable-build.yml@main
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,30 +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: "CodeQL"
on:
push:
branches:
- main
- release-*
pull_request:
branches:
- main
- release-*
schedule:
- cron: '29 2 * * 5'
jobs:
analyze:
uses: notaryproject/notation-core-go/.github/workflows/reusable-codeql.yml@main

View File

@ -1,32 +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: License Checker
on:
push:
branches:
- main
- release-*
pull_request:
branches:
- main
- release-*
permissions:
contents: write
pull-requests: write
jobs:
check-license:
uses: notaryproject/notation-core-go/.github/workflows/reusable-license-checker.yml@main

View File

@ -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

42
.github/workflows/test.yml vendored Normal file
View File

@ -0,0 +1,42 @@
name: test
on:
push:
branches: main
pull_request:
branches: main
jobs:
build:
name: Continuous Testing
runs-on: ubuntu-20.04
strategy:
matrix:
go-version: [1.17]
fail-fast: true
steps:
- name: Set up Go ${{ matrix.go-version }}
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Check out code
uses: actions/checkout@v3
- name: Cache Go modules
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Build and test
run: make test
line_endings:
name: Check Line Endings
runs-on: ubuntu-20.04
strategy:
fail-fast: true
steps:
- name: Check out code
uses: actions/checkout@v3
- name: Check line endings
run: make check-line-endings

25
.gitignore vendored
View File

@ -1,24 +1 @@
# 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.
# Code Editors
.vscode
.idea
*.sublime-project
*.sublime-workspace
# Custom
coverage.txt
# tmp directory was generated by example_remoteVerify_test.go
tmp/
.vscode

View File

@ -1,3 +1 @@
# 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
* @notaryproject/notation-maintainers

View File

@ -1,21 +0,0 @@
# Org-Level Maintainers (in alphabetical order)
# Pattern: [First Name] [Last Name] <[Email Address]> ([GitHub Handle])
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)
# Repo-Level Maintainers (in alphabetical order)
# Note: This is for the notaryproject/notation-go repo
Milind Gokarn <gokarnm@amazon.com> (@gokarnm)
Patrick Zheng <patrickzheng@microsoft.com> (@Two-Hearts)
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)

View File

@ -1,26 +1,13 @@
# 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.
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}'
.PHONY: all
all: test
all: check-line-endings test
.PHONY: test
test: check-line-endings ## run unit tests
go test -race -v -coverprofile=coverage.txt -covermode=atomic ./...
test: ## run unit tests
go test ./...
.PHONY: clean
clean:
@ -29,7 +16,6 @@ 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
.PHONY: fix-line-endings
fix-line-endings: ## fix line endings

View File

@ -1,28 +1,15 @@
# notation-go
[![Build Status](https://github.com/notaryproject/notation-go/actions/workflows/build.yml/badge.svg?event=push&branch=main)](https://github.com/notaryproject/notation-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain)
[![Codecov](https://codecov.io/gh/notaryproject/notation-go/branch/main/graph/badge.svg)](https://codecov.io/gh/notaryproject/notation-go)
[![Go Reference](https://pkg.go.dev/badge/github.com/notaryproject/notation-go.svg)](https://pkg.go.dev/github.com/notaryproject/notation-go@main)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/notaryproject/notation-go/badge)](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/).
# Notation
A collection of libraries for supporting Notation sign, verify, push, pull of oci artifacts. Based on Notary V2 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

View File

@ -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.

View File

@ -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 config
import (
"encoding/json"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/notaryproject/notation-go/dir"
)
// save stores the cfg struct to file
func save(filePath string, cfg interface{}) error {
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, 0700); err != nil {
return err
}
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer file.Close()
encoder := json.NewEncoder(file)
encoder.SetIndent("", " ")
return encoder.Encode(cfg)
}
// load reads file, parses json and stores in cfg struct
func load(filePath string, cfg interface{}) error {
path, err := dir.ConfigFS().SysPath(filePath)
if err != nil {
return err
}
// throw error if path is a directory or is a symlink or does not exist.
fileInfo, err := os.Lstat(path)
if err != nil {
return err
}
mode := fileInfo.Mode()
if mode.IsDir() || mode&fs.ModeSymlink != 0 {
return fmt.Errorf("%q is not a regular file (symlinks are not supported)", path)
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
return json.NewDecoder(file).Decode(cfg)
}

View File

@ -1,51 +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 (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/notaryproject/notation-go/dir"
)
func TestLoadNonExistentFile(t *testing.T) {
dir.UserConfigDir = "testdata/valid"
var config string
err := load("non-existent", &config)
if err == nil {
t.Fatalf("load() expected error but not found")
}
}
func TestLoadSymlink(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
root := t.TempDir()
dir.UserConfigDir = root
fileName := "symlink"
os.Symlink("testdata/valid/config.json", filepath.Join(root, fileName))
expectedError := fmt.Sprintf("\"%s/%s\" is not a regular file (symlinks are not supported)", dir.UserConfigDir, fileName)
var config string
err := load(fileName, &config)
if err != nil && err.Error() != expectedError {
t.Fatalf("load() expected error= %s but found= %v", expectedError, err)
}
}

View File

@ -1,61 +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 provides the ability to load and save config.json and
// signingkeys.json.
package config
import (
"errors"
"io/fs"
"github.com/notaryproject/notation-go/dir"
)
// Config reflects the config.json file.
// Specification: https://github.com/notaryproject/notation/pull/76
type Config struct {
InsecureRegistries []string `json:"insecureRegistries"`
CredentialsStore string `json:"credsStore,omitempty"`
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
// SignatureFormat defines the signature envelope type for signing
SignatureFormat string `json:"signatureFormat,omitempty"`
}
// NewConfig creates a new config file
func NewConfig() *Config {
return &Config{}
}
// Save stores the config to file
func (c *Config) Save() error {
path, err := dir.ConfigFS().SysPath(dir.PathConfigFile)
if err != nil {
return err
}
return save(path, c)
}
// LoadConfig reads the config from file or return a default config if not found.
func LoadConfig() (*Config, error) {
var config Config
err := load(dir.PathConfigFile, &config)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return NewConfig(), nil
}
return nil, err
}
return &config, nil
}

View File

@ -1,65 +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 (
"reflect"
"testing"
"github.com/notaryproject/notation-go/dir"
)
var sampleConfig = &Config{
InsecureRegistries: []string{
"registry.wabbit-networks.io",
},
SignatureFormat: "jws",
}
func TestLoadFile(t *testing.T) {
dir.UserConfigDir = "./testdata/valid"
got, err := LoadConfig()
if err != nil {
t.Fatalf("LoadConfig() error. err = %v", err)
}
if !reflect.DeepEqual(got, sampleConfig) {
t.Errorf("loadFile() = %v, want %v", got, sampleConfig)
}
}
func TestSaveFile(t *testing.T) {
root := t.TempDir()
dir.UserConfigDir = root
sampleConfig.Save()
config, err := LoadConfig()
if err != nil {
t.Fatal("Load config file from temp dir failed")
}
if !reflect.DeepEqual(sampleConfig, config) {
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())
}
}

View File

@ -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"
}

View File

@ -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")
}
}

View File

@ -1,251 +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 (
"context"
"crypto/tls"
"errors"
"fmt"
"io/fs"
"github.com/notaryproject/notation-go/internal/slices"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/plugin"
"github.com/notaryproject/notation-go/dir"
set "github.com/notaryproject/notation-go/internal/container"
)
// X509KeyPair contains the paths of a public/private key pair files.
type X509KeyPair struct {
KeyPath string `json:"keyPath,omitempty"`
CertificatePath string `json:"certPath,omitempty"`
}
// ExternalKey contains the necessary information to delegate
// the signing operation to the named plugin.
type ExternalKey struct {
ID string `json:"id,omitempty"`
PluginName string `json:"pluginName,omitempty"`
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
}
// KeySuite is a named key suite.
type KeySuite struct {
Name string `json:"name"`
*X509KeyPair
*ExternalKey
}
// SigningKeys reflects the signingkeys.json file.
type SigningKeys struct {
Default *string `json:"default,omitempty"`
Keys []KeySuite `json:"keys"`
}
// NewSigningKeys creates a new signingkeys config file
func NewSigningKeys() *SigningKeys {
return &SigningKeys{Keys: []KeySuite{}}
}
// Add adds new signing key
func (s *SigningKeys) Add(name, keyPath, certPath string, markDefault bool) error {
if name == "" {
return ErrKeyNameEmpty
}
_, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return err
}
ks := KeySuite{
Name: name,
X509KeyPair: &X509KeyPair{
KeyPath: keyPath,
CertificatePath: certPath,
},
}
return s.add(ks, markDefault)
}
// AddPlugin adds new plugin based signing key
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
}
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{
ID: id,
PluginName: pluginName,
PluginConfig: pluginConfig,
},
}
if err = s.add(ks, markDefault); err != nil {
logger.Error("Failed to add key with error: %v", err)
return err
}
logger.Debugf("Added key with name %s - {%+v}", keyName, ks)
return nil
}
// Get returns signing key for the given name
func (s *SigningKeys) Get(keyName string) (KeySuite, error) {
if keyName == "" {
return KeySuite{}, ErrKeyNameEmpty
}
idx := slices.IndexIsser(s.Keys, keyName)
if idx < 0 {
return KeySuite{}, KeyNotFoundError{KeyName: keyName}
}
return s.Keys[idx], nil
}
// GetDefault returns default signing key
func (s *SigningKeys) GetDefault() (KeySuite, error) {
if s.Default == nil {
return KeySuite{}, errors.New("default signing key not set." +
" Please set default signing key or specify a key name")
}
return s.Get(*s.Default)
}
// Remove deletes given signing keys and returns a slice of deleted key names
func (s *SigningKeys) Remove(keyName ...string) ([]string, error) {
var deletedNames []string
for _, name := range keyName {
if name == "" {
return deletedNames, ErrKeyNameEmpty
}
idx := slices.IndexIsser(s.Keys, name)
if idx < 0 {
return deletedNames, KeyNotFoundError{KeyName: name}
}
s.Keys = slices.Delete(s.Keys, idx)
deletedNames = append(deletedNames, name)
if s.Default != nil && *s.Default == name {
s.Default = nil
}
}
return deletedNames, nil
}
// UpdateDefault updates default signing key
func (s *SigningKeys) UpdateDefault(keyName string) error {
if keyName == "" {
return ErrKeyNameEmpty
}
if !slices.ContainsIsser(s.Keys, keyName) {
return KeyNotFoundError{KeyName: keyName}
}
s.Default = &keyName
return nil
}
// Save SigningKeys to signingkeys.json file
func (s *SigningKeys) Save() error {
path, err := dir.ConfigFS().SysPath(dir.PathSigningKeys)
if err != nil {
return err
}
if err := validateKeys(s); err != nil {
return err
}
return save(path, s)
}
// LoadSigningKeys reads the signingkeys.json file
// or return a default config if not found.
func LoadSigningKeys() (*SigningKeys, error) {
var config SigningKeys
err := load(dir.PathSigningKeys, &config)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return NewSigningKeys(), nil
}
return nil, err
}
if err := validateKeys(&config); err != nil {
return nil, err
}
return &config, nil
}
// LoadExecSaveSigningKeys loads signing key, executes given function and
// then saves the signing key
func LoadExecSaveSigningKeys(fn func(keys *SigningKeys) error) error {
// core process
signingKeys, err := LoadSigningKeys()
if err != nil {
return err
}
if err := fn(signingKeys); err != nil {
return err
}
return signingKeys.Save()
}
// Is checks whether the given name is equal with the Name variable
func (k KeySuite) Is(name string) bool {
return k.Name == name
}
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
}
func validateKeys(config *SigningKeys) error {
keys := config.Keys
uniqueKeyNames := set.NewWithSize[string](len(keys))
for _, key := range keys {
if len(key.Name) == 0 {
return fmt.Errorf("malformed %s: key name cannot be empty", dir.PathSigningKeys)
}
if uniqueKeyNames.Contains(key.Name) {
return fmt.Errorf("malformed %s: multiple keys with name '%s' found", dir.PathSigningKeys, key.Name)
}
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
}

View File

@ -1,456 +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 (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/notaryproject/notation-core-go/testhelper"
"github.com/notaryproject/notation-go/dir"
)
var sampleSigningKeysInfo = SigningKeys{
Default: Ptr("wabbit-networks"),
Keys: []KeySuite{
{
Name: "wabbit-networks",
X509KeyPair: &X509KeyPair{
KeyPath: "/home/demo/.config/notation/localkeys/wabbit-networks.key",
CertificatePath: "/home/demo/.config/notation/localkeys/wabbit-networks.crt",
},
},
{
Name: "import.acme-rockets",
X509KeyPair: &X509KeyPair{
KeyPath: "/home/demo/.config/notation/localkeys/import.acme-rockets.key",
CertificatePath: "/home/demo/.config/notation/localkeys/import.acme-rockets.crt",
},
},
{
Name: "external-key",
ExternalKey: &ExternalKey{
ID: "id1",
PluginName: "pluginX",
PluginConfig: map[string]string{
"key": "value",
},
},
},
},
}
func TestLoadSigningKeysInfo(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
dir.UserConfigDir = "./testdata/valid"
got, err := LoadSigningKeys()
if err != nil {
t.Errorf("LoadSigningKeysInfo() error = \"%v\"", err)
return
}
if !reflect.DeepEqual(sampleSigningKeysInfo.Default, got.Default) {
t.Fatal("signingKeysInfo test failed.")
}
if !reflect.DeepEqual(sampleSigningKeysInfo.Keys, got.Keys) {
t.Fatal("signingKeysInfo test failed.")
}
})
t.Run("DuplicateKeys", func(t *testing.T) {
expectedErr := "malformed signingkeys.json: multiple keys with name 'wabbit-networks' found"
dir.UserConfigDir = "./testdata/malformed-duplicate"
_, err := LoadSigningKeys()
if err == nil || err.Error() != expectedErr {
t.Errorf("LoadSigningKeysInfo() error expected = \"%v\" but found = \"%v\"", expectedErr, err)
}
})
t.Run("InvalidDefault", func(t *testing.T) {
expectedErr := "malformed signingkeys.json: default key 'missing-default' not found"
dir.UserConfigDir = "./testdata/malformed-invalid-default"
_, err := LoadSigningKeys()
if err == nil || err.Error() != expectedErr {
t.Errorf("LoadSigningKeysInfo() error expected = \"%v\" but found = \"%v\"", expectedErr, err)
}
})
}
func TestSaveSigningKeys(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
root := t.TempDir()
dir.UserConfigDir = root
sampleSigningKeysInfo.Save()
info, err := LoadSigningKeys()
if err != nil {
t.Fatal("Load signingkeys.json from temp dir failed.")
}
if !reflect.DeepEqual(sampleSigningKeysInfo.Default, info.Default) {
t.Fatal("Save signingkeys.json failed.")
}
if !reflect.DeepEqual(sampleSigningKeysInfo.Keys, info.Keys) {
t.Fatal("Save signingkeys.json failed.")
}
})
t.Run("ValidWithoutDefault", func(t *testing.T) {
root := t.TempDir()
dir.UserConfigDir = root
sampleSigningKeysInfoNoDefault := deepCopySigningKeys(sampleSigningKeysInfo)
sampleSigningKeysInfoNoDefault.Default = nil
sampleSigningKeysInfoNoDefault.Save()
info, err := LoadSigningKeys()
if err != nil {
t.Fatal("Load signingkeys.json from temp dir failed.")
}
if !reflect.DeepEqual(sampleSigningKeysInfoNoDefault.Default, info.Default) {
t.Fatal("Save signingkeys.json failed.")
}
if !reflect.DeepEqual(sampleSigningKeysInfoNoDefault.Keys, info.Keys) {
t.Fatal("Save signingkeys.json failed.")
}
})
t.Run("DuplicateKeys", func(t *testing.T) {
expectedErr := "malformed signingkeys.json: multiple keys with name 'import.acme-rockets' found"
dir.UserConfigDir = t.TempDir()
duplicateKeySignKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
duplicateKeySignKeysInfo.Keys = append(duplicateKeySignKeysInfo.Keys, KeySuite{
Name: "import.acme-rockets",
X509KeyPair: &X509KeyPair{
KeyPath: "/keypath",
CertificatePath: "/CertificatePath",
},
})
err := duplicateKeySignKeysInfo.Save()
if err == nil || err.Error() != expectedErr {
t.Errorf("Save signingkeys.json failed, error expected = \"%v\" but found = \"%v\"", expectedErr, err)
}
})
t.Run("EmptyKeyName", func(t *testing.T) {
expectedErr := "malformed signingkeys.json: key name cannot be empty"
dir.UserConfigDir = t.TempDir()
emptyKeyNameSignKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
emptyKeyNameSignKeysInfo.Keys[0].Name = ""
err := emptyKeyNameSignKeysInfo.Save()
if err == nil || err.Error() != expectedErr {
t.Errorf("Save signingkeys.json failed, error expected = \"%v\" but found = \"%v\"", expectedErr, err)
}
})
t.Run("InvalidDefault", func(t *testing.T) {
expectedErr := "malformed signingkeys.json: default key 'missing-default' not found"
dir.UserConfigDir = t.TempDir()
invalidDefaultSignKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
invalidDefaultSignKeysInfo.Default = Ptr("missing-default")
err := invalidDefaultSignKeysInfo.Save()
if err == nil || err.Error() != expectedErr {
t.Errorf("Save signingkeys.json failed, error expected = \"%v\" but found = \"%v\"", expectedErr, err)
}
expectedErr = "malformed signingkeys.json: default key name cannot be empty"
invalidDefaultSignKeysInfo.Default = Ptr("")
err = invalidDefaultSignKeysInfo.Save()
if err == nil || err.Error() != expectedErr {
t.Errorf("Save signingkeys.json failed, error expected = \"%v\" but found = \"%v\"", expectedErr, err)
}
})
}
func TestAdd(t *testing.T) {
certPath, keyPath := createTempCertKey(t)
t.Run("WithDefault", func(t *testing.T) {
testSigningKeys := deepCopySigningKeys(sampleSigningKeysInfo)
expectedTestKeyName := "name1"
if err := testSigningKeys.Add(expectedTestKeyName, keyPath, certPath, true); err != nil {
t.Errorf("Add() failed with err= %v", err)
}
expectedSigningKeys := append(deepCopySigningKeys(sampleSigningKeysInfo).Keys, KeySuite{
Name: expectedTestKeyName,
X509KeyPair: &X509KeyPair{
KeyPath: keyPath,
CertificatePath: certPath,
},
})
if expectedTestKeyName != *testSigningKeys.Default {
t.Error("Add() failed, incorrect default key")
}
if !reflect.DeepEqual(testSigningKeys.Keys, expectedSigningKeys) {
t.Error("Add() failed, KeySuite mismatch")
}
})
t.Run("WithoutDefault", func(t *testing.T) {
dir.UserConfigDir = t.TempDir()
testSigningKeys := deepCopySigningKeys(sampleSigningKeysInfo)
expectedTestKeyName := "name2"
certPath, keyPath := createTempCertKey(t)
if err := testSigningKeys.Add(expectedTestKeyName, keyPath, certPath, false); err != nil {
t.Errorf("Add() failed with err= %v", err)
}
expectedSigningKeys := append(deepCopySigningKeys(sampleSigningKeysInfo).Keys, KeySuite{
Name: expectedTestKeyName,
X509KeyPair: &X509KeyPair{
KeyPath: keyPath,
CertificatePath: certPath,
},
})
if *sampleSigningKeysInfo.Default != *testSigningKeys.Default {
t.Error("Add() failed, default key changed")
}
if !reflect.DeepEqual(testSigningKeys.Keys, expectedSigningKeys) {
t.Error("Add() failed, KeySuite mismatch")
}
})
t.Run("InvalidCertKeyLocation", func(t *testing.T) {
err := sampleSigningKeysInfo.Add("name1", "invalid", "invalid", true)
if err == nil {
t.Error("expected Add() to fail for invalid cert and key location")
}
})
t.Run("InvalidName", func(t *testing.T) {
err := sampleSigningKeysInfo.Add("", "invalid", "invalid", true)
if err == nil {
t.Error("expected Add() to fail for empty key name")
}
})
t.Run("InvalidName", func(t *testing.T) {
err := sampleSigningKeysInfo.Add("", "invalid", "invalid", true)
if err == nil {
t.Error("expected Add() to fail for empty key name")
}
})
t.Run("DuplicateKey", func(t *testing.T) {
err := sampleSigningKeysInfo.Add(sampleSigningKeysInfo.Keys[0].Name, "invalid", "invalid", true)
if err == nil {
t.Error("expected Add() to fail for duplicate name")
}
})
}
func TestPluginAdd(t *testing.T) {
config := map[string]string{"key1": "value1"}
name := "name1"
id := "pluginId1"
pluginName := "pluginName1"
t.Run("InvalidCertKeyLocation", func(t *testing.T) {
err := sampleSigningKeysInfo.Add("name1", "invalid", "invalid", true)
if err == nil {
t.Error("expected AddPlugin() to fail for invalid cert and key location")
}
})
t.Run("InvalidName", func(t *testing.T) {
err := sampleSigningKeysInfo.AddPlugin(context.Background(), "", id, pluginName, config, true)
if err == nil {
t.Error("expected AddPlugin() to fail for empty key name")
}
})
t.Run("InvalidId", func(t *testing.T) {
err := sampleSigningKeysInfo.AddPlugin(context.Background(), name, "", pluginName, config, true)
if err == nil {
t.Error("expected AddPlugin() to fail for empty key name")
}
})
t.Run("InvalidPluginName", func(t *testing.T) {
err := sampleSigningKeysInfo.AddPlugin(context.Background(), name, id, "", config, true)
if err == nil {
t.Error("AddPlugin AddPlugin() to fail for empty plugin name")
}
})
}
func TestGet(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
key, err := sampleSigningKeysInfo.Get("external-key")
if err != nil {
t.Errorf("Get() failed with error= %v", err)
}
if !reflect.DeepEqual(key, sampleSigningKeysInfo.Keys[2]) {
t.Errorf("Get() returned %v but expected %v", key, sampleSigningKeysInfo.Keys[2])
}
})
t.Run("NonExistent", func(t *testing.T) {
_, err := sampleSigningKeysInfo.Get("nonExistent")
if 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")
}
})
}
func TestGetDefault(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
key, err := sampleSigningKeysInfo.GetDefault()
if err != nil {
t.Errorf("GetDefault() failed with error= %v", err)
}
if !reflect.DeepEqual(key.Name, *sampleSigningKeysInfo.Default) {
t.Errorf("GetDefault() returned %s but expected %s", key.Name, *sampleSigningKeysInfo.Default)
}
})
t.Run("NoDefault", func(t *testing.T) {
testSigningKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
testSigningKeysInfo.Default = nil
if _, err := testSigningKeysInfo.GetDefault(); err == nil {
t.Error("GetDefault Get() to fail there is no defualt key")
}
})
}
func TestUpdateDefault(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
testSigningKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
newDefault := sampleSigningKeysInfo.Keys[1].Name
err := testSigningKeysInfo.UpdateDefault(newDefault)
if err != nil {
t.Errorf("UpdateDefault() failed with error= %v", err)
}
if !reflect.DeepEqual(newDefault, *testSigningKeysInfo.Default) {
t.Errorf("UpdateDefault() didn't update default key")
}
})
t.Run("NonExistent", func(t *testing.T) {
err := sampleSigningKeysInfo.UpdateDefault("nonExistent")
if 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")
}
})
}
func TestRemove(t *testing.T) {
testKeyName := "wabbit-networks"
testSigningKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
t.Run("Valid", func(t *testing.T) {
keys, err := testSigningKeysInfo.Remove(testKeyName)
if err != nil {
t.Errorf("testSigningKeysInfo() failed with error= %v", err)
}
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 {
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")
}
})
}
func deepCopySigningKeys(keys SigningKeys) SigningKeys {
cpyKeys := make([]KeySuite, len(sampleSigningKeysInfo.Keys))
copy(cpyKeys, keys.Keys)
cpyDefault := *keys.Default
cpySignKeys := keys
cpySignKeys.Default = &cpyDefault
cpySignKeys.Keys = cpyKeys
return cpySignKeys
}
func Ptr[T any](v T) *T {
return &v
}
func createTempCertKey(t *testing.T) (string, string) {
certTuple := testhelper.GetRSARootCertificate()
certPath := filepath.Join(t.TempDir(), "cert.tmp")
certData := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certTuple.Cert.Raw})
if err := os.WriteFile(certPath, certData, 0600); err != nil {
panic(err)
}
keyPath := filepath.Join(t.TempDir(), "key.tmp")
keyBytes, _ := x509.MarshalPKCS8PrivateKey(certTuple.PrivateKey)
keyData := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
if err := os.WriteFile(keyPath, keyData, 0600); err != nil {
panic(err)
}
return certPath, keyPath
}

View File

@ -1,23 +0,0 @@
{
"default": "wabbit-networks",
"keys": [
{
"name": "wabbit-networks",
"keyPath": "/home/demo/.config/notation/localkeys/wabbit-networks.key",
"certPath": "/home/demo/.config/notation/localkeys/wabbit-networks.crt"
},
{
"name": "wabbit-networks",
"keyPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.key",
"certPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.crt"
},
{
"name": "external-key",
"id": "id1",
"pluginName": "pluginX",
"pluginConfig": {
"key": "value"
}
}
]
}

View File

@ -1,23 +0,0 @@
{
"default": "missing-default",
"keys": [
{
"name": "wabbit-networks",
"keyPath": "/home/demo/.config/notation/localkeys/wabbit-networks.key",
"certPath": "/home/demo/.config/notation/localkeys/wabbit-networks.crt"
},
{
"name": "import.acme-rockets",
"keyPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.key",
"certPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.crt"
},
{
"name": "external-key",
"id": "id1",
"pluginName": "pluginX",
"pluginConfig": {
"key": "value"
}
}
]
}

View File

@ -1,6 +0,0 @@
{
"insecureRegistries": [
"registry.wabbit-networks.io"
],
"signatureFormat": "jws"
}

View File

@ -1,23 +0,0 @@
{
"default": "wabbit-networks",
"keys": [
{
"name": "wabbit-networks",
"keyPath": "/home/demo/.config/notation/localkeys/wabbit-networks.key",
"certPath": "/home/demo/.config/notation/localkeys/wabbit-networks.crt"
},
{
"name": "import.acme-rockets",
"keyPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.key",
"certPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.crt"
},
{
"name": "external-key",
"id": "id1",
"pluginName": "pluginX",
"pluginConfig": {
"key": "value"
}
}
]
}

31
crypto/cryptoutil/cert.go Normal file
View File

@ -0,0 +1,31 @@
package cryptoutil
import (
"crypto/x509"
"encoding/pem"
"os"
)
// ReadCertificateFile reads a certificate PEM file.
func ReadCertificateFile(path string) ([]*x509.Certificate, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return ParseCertificatePEM(data)
}
// ParseCertificatePEM parses a certificate PEM.
func ParseCertificatePEM(data []byte) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
block, rest := pem.Decode(data)
for block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}
certs = append(certs, cert)
block, rest = pem.Decode(rest)
}
return certs, nil
}

36
crypto/cryptoutil/key.go Normal file
View File

@ -0,0 +1,36 @@
package cryptoutil
import (
"crypto"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
)
// ReadPrivateKeyFile reads a key PEM file as a signing key.
func ReadPrivateKeyFile(path string) (crypto.PrivateKey, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return ParsePrivateKeyPEM(data)
}
// ParsePrivateKeyPEM parses a PEM as a signing key.
func ParsePrivateKeyPEM(data []byte) (crypto.PrivateKey, error) {
block, _ := pem.Decode(data)
if block == nil {
return nil, errors.New("no PEM data found")
}
switch block.Type {
case "PRIVATE KEY":
return x509.ParsePKCS8PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
}
return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
}

View File

@ -0,0 +1,53 @@
package jwsutil
import "encoding/json"
// Envelope contains a common payload signed by multiple signatures.
type Envelope struct {
Payload string `json:"payload,omitempty"`
Signatures []Signature `json:"signatures,omitempty"`
}
// Size returns the number of enclosed signatures.
func (e Envelope) Size() int {
return len(e.Signatures)
}
// Open opens the evelope and returns the first or default complete signature.
func (e Envelope) Open() CompleteSignature {
if len(e.Signatures) == 0 {
return CompleteSignature{
Payload: e.Payload,
}
}
return CompleteSignature{
Payload: e.Payload,
Signature: e.Signatures[0],
}
}
// UnmarshalJSON parses the JSON serialized JWS.
// Reference: RFC 7515 7.2 JWS JSON Serialization.
func (e *Envelope) UnmarshalJSON(data []byte) error {
var combined struct {
CompleteSignature
Signatures []Signature `json:"signatures"`
}
if err := json.Unmarshal(data, &combined); err != nil {
return ErrInvalidJSONSerialization
}
if len(combined.Signatures) == 0 {
*e = Envelope{
Payload: combined.Payload,
Signatures: []Signature{
combined.Signature,
},
}
} else {
*e = Envelope{
Payload: combined.Payload,
Signatures: combined.Signatures,
}
}
return nil
}

View File

@ -0,0 +1,102 @@
package jwsutil
import (
"encoding/json"
"reflect"
"testing"
)
func TestEnvelope_UnmarshalJSON(t *testing.T) {
tests := []struct {
name string
data string
want Envelope
}{
{
name: "General JWS JSON Serialization Syntax (multiple signatures)",
data: `{
"payload": "test payload",
"signatures": [
{
"protected": "protected foo",
"header": {"unprotected": "foo"},
"signature": "signature foo"
},
{
"protected": "protected bar",
"header": {"unprotected": "bar"},
"signature": "signature bar"
}
]
}`,
want: Envelope{
Payload: "test payload",
Signatures: []Signature{
{
Protected: "protected foo",
Unprotected: []byte(`{"unprotected": "foo"}`),
Signature: "signature foo",
},
{
Protected: "protected bar",
Unprotected: []byte(`{"unprotected": "bar"}`),
Signature: "signature bar",
},
},
},
},
{
name: "General JWS JSON Serialization Syntax (single signature)",
data: `{
"payload": "test payload",
"signatures": [
{
"protected": "protected foo",
"header": {"unprotected": "foo"},
"signature": "signature foo"
}
]
}`,
want: Envelope{
Payload: "test payload",
Signatures: []Signature{
{
Protected: "protected foo",
Unprotected: []byte(`{"unprotected": "foo"}`),
Signature: "signature foo",
},
},
},
},
{
name: "Flattened JWS JSON Serialization Syntax",
data: `{
"payload": "test payload",
"protected": "protected foo",
"header": {"unprotected": "foo"},
"signature": "signature foo"
}`,
want: Envelope{
Payload: "test payload",
Signatures: []Signature{
{
Protected: "protected foo",
Unprotected: []byte(`{"unprotected": "foo"}`),
Signature: "signature foo",
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got Envelope
if err := json.Unmarshal([]byte(tt.data), &got); err != nil {
t.Fatalf("Envelope.UnmarshalJSON() error = %v", err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Envelope.UnmarshalJSON() = %v, want %v", got, tt.want)
}
})
}
}

9
crypto/jwsutil/errors.go Normal file
View File

@ -0,0 +1,9 @@
package jwsutil
import "errors"
// Common errors
var (
ErrInvalidCompactSerialization = errors.New("invalid compact serialization")
ErrInvalidJSONSerialization = errors.New("invalid JSON serialization")
)

View File

@ -0,0 +1,56 @@
// Package jwsutil provides serialization utilities for JWT libraries to comfort JWS.
// Reference: RFC 7515 JSON Web Signature (JWS).
package jwsutil
import (
"encoding/json"
"strings"
)
// Signature represents a detached signature.
type Signature struct {
Protected string `json:"protected,omitempty"`
Unprotected json.RawMessage `json:"header,omitempty"`
Signature string `json:"signature,omitempty"`
}
// CompleteSignature represents a clear signed signature.
// A CompleteSignature can be viewed as an envelope with a single signature in
// flattened JWS JSON serialization syntax.
// Reference: RFC 7515 7.2 JWS JSON Serialization.
type CompleteSignature struct {
Payload string `json:"payload,omitempty"`
Signature
}
// Parse parses the compact serialized JWS.
// Reference: RFC 7515 7.1 JWS Compact Serialization.
func ParseCompact(serialized string) (CompleteSignature, error) {
parts := strings.Split(serialized, ".")
if len(parts) != 3 {
return CompleteSignature{}, ErrInvalidCompactSerialization
}
return CompleteSignature{
Payload: parts[1],
Signature: Signature{
Protected: parts[0],
Signature: parts[2],
},
}, nil
}
// SerializeCompact serialize the signature in JWS Compact Serialization
// Reference: RFC 7515 7.1 JWS Compact Serialization.
func (s CompleteSignature) SerializeCompact() string {
return strings.Join([]string{s.Protected, s.Payload, s.Signature.Signature}, ".")
}
// Enclose packs the signature into an envelope.
func (s CompleteSignature) Enclose() Envelope {
return Envelope{
Payload: s.Payload,
Signatures: []Signature{
s.Signature,
},
}
}

78
crypto/timestamp/http.go Normal file
View File

@ -0,0 +1,78 @@
package timestamp
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
)
// maxBodyLength specifies the max content can be received from the possibly malicious
// remote server.
// The legnth of a regular TSA response with certificates is usually less than 10 KiB.
const maxBodyLength = 1 * 1024 * 1024 // 1 MiB
// httpTimestamper is a HTTP-based timestamper.
type httpTimestamper struct {
rt http.RoundTripper
endpoint string
}
// NewHTTPTimestamper creates a HTTP-based timestamper with the endpoint provided by the TSA.
// http.DefaultTransport is used if nil RoundTripper is passed.
func NewHTTPTimestamper(rt http.RoundTripper, endpoint string) (Timestamper, error) {
if rt == nil {
rt = http.DefaultTransport
}
if _, err := url.Parse(endpoint); err != nil {
return nil, err
}
return &httpTimestamper{
rt: rt,
endpoint: endpoint,
}, nil
}
// Timestamp sends the request to the remote TSA server for timestamping.
// Reference: RFC 3161 3.4 Time-Stamp Protocol via HTTP
func (ts *httpTimestamper) Timestamp(ctx context.Context, req *Request) (*Response, error) {
// prepare for http request
reqBytes, err := req.MarshalBinary()
if err != nil {
return nil, err
}
hReq, err := http.NewRequestWithContext(ctx, http.MethodPost, ts.endpoint, bytes.NewReader(reqBytes))
if err != nil {
return nil, err
}
hReq.Header.Set("Content-Type", "application/timestamp-query")
// send the request to the remote TSA server
hResp, err := ts.rt.RoundTrip(hReq)
if err != nil {
return nil, err
}
defer hResp.Body.Close()
// verify HTTP response
if hResp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %s", hResp.Status)
}
if contentType := hResp.Header.Get("Content-Type"); contentType != "application/timestamp-reply" {
return nil, fmt.Errorf("unexpected response content type: %s", contentType)
}
// read response
body := io.LimitReader(hResp.Body, maxBodyLength)
respBytes, err := io.ReadAll(body)
if err != nil {
return nil, err
}
var resp Response
if err := resp.UnmarshalBinary(respBytes); err != nil {
return nil, err
}
return &resp, nil
}

View File

@ -0,0 +1,250 @@
package timestamp
import (
"bytes"
"context"
"crypto"
"crypto/x509"
"encoding/asn1"
"io"
"net/http"
"net/http/httptest"
"os"
"reflect"
"testing"
"time"
"github.com/notaryproject/notation-go/internal/crypto/hashutil"
"github.com/notaryproject/notation-go/internal/crypto/pki"
)
var testRequest = []byte{
// Request
0x30, 0x37,
// Version
0x02, 0x01, 0x01,
// MessageImprint
0x30, 0x2f,
// MessageImprint.HashAlgorithm
0x30, 0x0b,
// MessageImprint.HashAlgorithm.Algorithm
0x06, 0x09,
0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
// MessageImprint.HashedMessage
0x04, 0x20,
0x83, 0x26, 0xf4, 0x70, 0x9d, 0x40, 0x1d, 0xfa, 0xbf, 0xa7, 0x83, 0x02, 0xfb, 0x1c, 0xde, 0xa0,
0xf1, 0x80, 0x48, 0xa4, 0x40, 0x40, 0xc2, 0x12, 0xbd, 0x8e, 0x28, 0xda, 0x6b, 0xc6, 0x51, 0xc7,
// CertReq
0x01, 0x01, 0xff,
}
func TestHTTPTimestampGranted(t *testing.T) {
// setup test server
testResp, err := os.ReadFile("testdata/granted.tsq")
if err != nil {
t.Fatal("failed to read test response:", err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
const wantContentType = "application/timestamp-query"
if got := r.Header.Get("Content-Type"); got != wantContentType {
t.Fatalf("TimeStampRequest.ContentType = %v, want %v", err, wantContentType)
}
if got, err := io.ReadAll(r.Body); err != nil {
t.Fatalf("TimeStampRequest.Body read error = %v", err)
} else if !bytes.Equal(got, testRequest) {
t.Fatalf("TimeStampRequest.Body = %v, want %v", got, testRequest)
}
// write reply
w.Header().Set("Content-Type", "application/timestamp-reply")
if _, err := w.Write(testResp); err != nil {
t.Error("failed to write response:", err)
}
}))
defer ts.Close()
// do timestamp
tsa, err := NewHTTPTimestamper(nil, ts.URL)
if err != nil {
t.Fatalf("NewHTTPTimestamper() error = %v", err)
}
message := []byte("notation")
req, err := NewRequestFromBytes(message)
if err != nil {
t.Fatalf("NewRequestFromString() error = %v", err)
}
ctx := context.Background()
resp, err := tsa.Timestamp(ctx, req)
if err != nil {
t.Fatalf("httpTimestamper.Timestamp() error = %v", err)
}
wantStatus := pki.StatusGranted
if got := resp.Status.Status; got != wantStatus {
t.Fatalf("Response.Status = %v, want %v", got, wantStatus)
}
// verify timestamp token
token, err := resp.SignedToken()
if err != nil {
t.Fatalf("Response.SignedToken() error = %v", err)
}
roots := x509.NewCertPool()
rootCABytes, err := os.ReadFile("testdata/GlobalSignRootCA.crt")
if err != nil {
t.Fatal("failed to read root CA certificate:", err)
}
if ok := roots.AppendCertsFromPEM(rootCABytes); !ok {
t.Fatal("failed to load root CA certificate")
}
opts := x509.VerifyOptions{
Roots: roots,
CurrentTime: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
}
certs, err := token.Verify(opts)
if err != nil {
t.Fatal("SignedToken.Verify() error =", err)
}
if got := len(certs); got != 1 {
t.Fatalf("SignedToken.Verify() len([]*x509.Certificate) = %v, want %v", got, 1)
}
certThumbprint, err := hashutil.ComputeHash(crypto.SHA256, certs[0].Raw)
if err != nil {
t.Fatal("failed to compute certificate thumbprint:", err)
}
wantCertThumbprint := []byte{
0x13, 0xd6, 0xe9, 0xc4, 0x20, 0xff, 0x6d, 0x4e, 0x27, 0x54, 0x72, 0x8c, 0x68, 0xe7, 0x78, 0x82,
0x65, 0x64, 0x67, 0xdb, 0x9a, 0x19, 0x0f, 0x81, 0x65, 0x97, 0xf6, 0x7f, 0xb6, 0xcc, 0xc6, 0xf9,
}
if !bytes.Equal(certThumbprint, wantCertThumbprint) {
t.Fatalf("SignedToken.Verify() = %v, want %v", certThumbprint, wantCertThumbprint)
}
info, err := token.Info()
if err != nil {
t.Fatal("SignedToken.Info() error =", err)
}
if err := info.Verify(message); err != nil {
t.Errorf("TSTInfo.Verify() error = %v", err)
}
timestamp, accuracy := info.Timestamp()
wantTimestamp := time.Date(2021, 9, 18, 11, 54, 34, 0, time.UTC)
if timestamp != wantTimestamp {
t.Errorf("TSTInfo.Timestamp() Timestamp = %v, want %v", timestamp, wantTimestamp)
}
wantAccuracy := time.Second
if accuracy != wantAccuracy {
t.Errorf("TSTInfo.Timestamp() Accuracy = %v, want %v", accuracy, wantAccuracy)
}
}
func TestHTTPTimestampRejection(t *testing.T) {
// setup test server
testResp, err := os.ReadFile("testdata/rejection.tsq")
if err != nil {
t.Fatal("failed to read test response:", err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
const wantContentType = "application/timestamp-query"
if got := r.Header.Get("Content-Type"); got != wantContentType {
t.Fatalf("TimeStampRequest.ContentType = %v, want %v", err, wantContentType)
}
if got, err := io.ReadAll(r.Body); err != nil {
t.Fatalf("TimeStampRequest.Body read error = %v", err)
} else if !bytes.Equal(got, testRequest) {
t.Fatalf("TimeStampRequest.Body = %v, want %v", got, testRequest)
}
// write reply
w.Header().Set("Content-Type", "application/timestamp-reply")
if _, err := w.Write(testResp); err != nil {
t.Error("failed to write response:", err)
}
}))
defer ts.Close()
// do timestamp
tsa, err := NewHTTPTimestamper(nil, ts.URL)
if err != nil {
t.Fatalf("NewHTTPTimestamper() error = %v", err)
}
message := []byte("notation")
req, err := NewRequestFromBytes(message)
if err != nil {
t.Fatalf("NewRequestFromString() error = %v", err)
}
ctx := context.Background()
resp, err := tsa.Timestamp(ctx, req)
if err != nil {
t.Fatalf("httpTimestamper.Timestamp() error = %v", err)
}
wantStatus := pki.StatusRejection
if got := resp.Status.Status; got != wantStatus {
t.Fatalf("Response.Status = %v, want %v", got, wantStatus)
}
wantStatusString := []string{"request contains unknown algorithm"}
if got := resp.Status.StatusString; !reflect.DeepEqual(got, wantStatusString) {
t.Fatalf("Response.StatusString = %v, want %v", got, wantStatusString)
}
wantFailInfo := asn1.BitString{
Bytes: []byte{0x80},
BitLength: 1,
}
if got := resp.Status.FailInfo; !reflect.DeepEqual(got, wantFailInfo) {
t.Fatalf("Response.FailInfo = %v, want %v", got, wantFailInfo)
}
}
func TestHTTPTimestampBadEndpoint(t *testing.T) {
// setup test server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// write reply
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if _, err := w.Write([]byte("{}")); err != nil {
t.Error("failed to write response:", err)
}
}))
defer ts.Close()
// do timestamp
tsa, err := NewHTTPTimestamper(nil, ts.URL)
if err != nil {
t.Fatalf("NewHTTPTimestamper() error = %v", err)
}
req, err := NewRequestFromString("notation")
if err != nil {
t.Fatalf("NewRequestFromString() error = %v", err)
}
ctx := context.Background()
_, err = tsa.Timestamp(ctx, req)
if err == nil {
t.Fatalf("httpTimestamper.Timestamp() error = %v, wantErr %v", err, true)
}
}
func TestHTTPTimestampEndpointNotFound(t *testing.T) {
// setup test server
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
defer ts.Close()
// do timestamp
tsa, err := NewHTTPTimestamper(nil, ts.URL)
if err != nil {
t.Fatalf("NewHTTPTimestamper() error = %v", err)
}
req, err := NewRequestFromString("notation")
if err != nil {
t.Fatalf("NewRequestFromString() error = %v", err)
}
ctx := context.Background()
_, err = tsa.Timestamp(ctx, req)
if err == nil {
t.Fatalf("httpTimestamper.Timestamp() error = %v, wantErr %v", err, true)
}
}

View File

@ -0,0 +1,99 @@
package timestamp
import (
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"errors"
"math/big"
"github.com/notaryproject/notation-go/internal/crypto/oid"
digest "github.com/opencontainers/go-digest"
)
// MessageImprint contains the hash of the datum to be time-stamped.
// MessageImprint ::= SEQUENCE {
// hashAlgorithm AlgorithmIdentifier,
// hashedMessage OCTET STRING }
type MessageImprint struct {
HashAlgorithm pkix.AlgorithmIdentifier
HashedMessage []byte
}
// Request is a time-stamping request.
// TimeStampReq ::= SEQUENCE {
// version INTEGER { v1(1) },
// messageImprint MessageImprint,
// reqPolicy TSAPolicyID OPTIONAL,
// nonce INTEGER OPTIONAL,
// certReq BOOLEAN DEFAULT FALSE,
// extensions [0] IMPLICIT Extensions OPTIONAL }
type Request struct {
Version int // fixed to 1 as defined in RFC 3161 2.4.1 Request Format
MessageImprint MessageImprint
ReqPolicy asn1.ObjectIdentifier `asn1:"optional"`
Nonce *big.Int `asn1:"optional"`
CertReq bool `asn1:"optional,default:false"`
Extensions []pkix.Extension `asn1:"optional,tag:0"`
}
// NewRequest creates a request based on the given digest.
func NewRequest(contentDigest digest.Digest) (*Request, error) {
hashAlgorithm, err := getOIDFromDigestAlgorithm(contentDigest.Algorithm())
if err != nil {
return nil, err
}
hashedMessage, err := hex.DecodeString(contentDigest.Encoded())
if err != nil {
return nil, err
}
return &Request{
Version: 1,
MessageImprint: MessageImprint{
HashAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: hashAlgorithm,
},
HashedMessage: hashedMessage,
},
CertReq: true,
}, nil
}
// NewRequestFromBytes creates a request based on the given byte slice.
func NewRequestFromBytes(content []byte) (*Request, error) {
return NewRequest(digest.FromBytes(content))
}
// NewRequestFromString creates a request based on the given string.
func NewRequestFromString(content string) (*Request, error) {
return NewRequest(digest.FromString(content))
}
// MarshalBinary encodes the request to binary form.
// This method implements encoding.BinaryMarshaler
func (r *Request) MarshalBinary() ([]byte, error) {
if r == nil {
return nil, errors.New("nil request")
}
return asn1.Marshal(*r)
}
// UnmarshalBinary decodes the request from binary form.
// This method implements encoding.BinaryUnmarshaler
func (r *Request) UnmarshalBinary(data []byte) error {
_, err := asn1.Unmarshal(data, r)
return err
}
// getOIDFromDigestAlgorithm returns corresponding ASN.1 OID for the given digest algorithm.
func getOIDFromDigestAlgorithm(alg digest.Algorithm) (asn1.ObjectIdentifier, error) {
switch alg {
case digest.SHA256:
return oid.SHA256, nil
case digest.SHA384:
return oid.SHA384, nil
case digest.SHA512:
return oid.SHA512, nil
}
return nil, digest.ErrDigestUnsupported
}

View File

@ -0,0 +1,44 @@
package timestamp
import (
"encoding/asn1"
"errors"
"github.com/notaryproject/notation-go/internal/crypto/pki"
)
// Response is a time-stamping response.
// TimeStampResp ::= SEQUENCE {
// status PKIStatusInfo,
// timeStampToken TimeStampToken OPTIONAL }
type Response struct {
Status pki.StatusInfo
TimeStampToken asn1.RawValue `asn1:"optional"`
}
// MarshalBinary encodes the response to binary form.
// This method implements encoding.BinaryMarshaler
func (r *Response) MarshalBinary() ([]byte, error) {
if r == nil {
return nil, errors.New("nil response")
}
return asn1.Marshal(r)
}
// UnmarshalBinary decodes the response from binary form.
// This method implements encoding.BinaryUnmarshaler
func (r *Response) UnmarshalBinary(data []byte) error {
_, err := asn1.Unmarshal(data, r)
return err
}
// TokenBytes returns the bytes of the timestamp token.
func (r *Response) TokenBytes() []byte {
return r.TimeStampToken.FullBytes
}
// SignedToken returns the timestamp token with signatures.
// Callers should invoke Verify to verify the content before comsumption.
func (r *Response) SignedToken() (*SignedToken, error) {
return ParseSignedToken(r.TokenBytes())
}

View File

@ -0,0 +1 @@
0/0-0$ "request contains unknown algorithm

View File

@ -0,0 +1,11 @@
// Package timestamp generates timestamping requests to TSA servers,
// and fetches the responses according to RFC 3161.
package timestamp
import "context"
// Timestamper stamps the time.
type Timestamper interface {
// Timestamp stamps the time with the given request.
Timestamp(context.Context, *Request) (*Response, error)
}

View File

@ -0,0 +1,269 @@
// Package timestamptest provides utilities for timestamp testing
package timestamptest
import (
"context"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"math"
"math/big"
"time"
"github.com/notaryproject/notation-go/crypto/timestamp"
"github.com/notaryproject/notation-go/internal/crypto/cms"
"github.com/notaryproject/notation-go/internal/crypto/hashutil"
"github.com/notaryproject/notation-go/internal/crypto/oid"
"github.com/notaryproject/notation-go/internal/crypto/pki"
)
// responseRejection is a general response for request rejection.
var responseRejection = &timestamp.Response{
Status: pki.StatusInfo{
Status: pki.StatusRejection,
},
}
// TSA is a Timestamping Authority for testing purpose.
type TSA struct {
// key is the TSA signing key.
key *rsa.PrivateKey
// cert is the self-signed certificate by the TSA signing key.
cert *x509.Certificate
// NowFunc provides the current time. time.Now() is used if nil.
NowFunc func() time.Time
}
// NewTSA creates a TSA with random credentials.
func NewTSA() (*TSA, error) {
// generate key
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
// generate certificate
serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
return nil, err
}
now := time.Now()
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: "timestamp test",
},
NotBefore: now,
NotAfter: now.Add(365 * 24 * time.Hour), // 1 year
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping},
BasicConstraintsValid: true,
}
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key)
if err != nil {
return nil, err
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
return nil, err
}
return &TSA{
key: key,
cert: cert,
}, nil
}
// Certificate returns the certificate used by the server.
func (tsa *TSA) Certificate() *x509.Certificate {
return tsa.cert
}
// Timestamp stamps the time with the given request.
func (tsa *TSA) Timestamp(_ context.Context, req *timestamp.Request) (*timestamp.Response, error) {
// validate request
if req.Version != 1 {
return responseRejection, nil
}
hash, ok := oid.ConvertToHash(req.MessageImprint.HashAlgorithm.Algorithm)
if !ok {
return responseRejection, nil
}
if hashedMessage := req.MessageImprint.HashedMessage; len(hashedMessage) != hash.Size() {
return responseRejection, nil
}
// generate token info
policy := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 4146, 2} // time-stamp-policies
switch hash {
case crypto.SHA1:
policy = append(policy, 2)
case crypto.SHA256, crypto.SHA384, crypto.SHA512:
policy = append(policy, 3)
default:
return responseRejection, nil
}
infoBytes, err := tsa.generateTokenInfo(req, policy)
if err != nil {
return nil, err
}
// generate signed data
signed, err := tsa.generateSignedData(infoBytes, req.CertReq)
if err != nil {
return nil, err
}
content, err := convertToRawASN1(signed, "explicit,tag:0")
if err != nil {
return nil, err
}
// generate content info
contentInfo := cms.ContentInfo{
ContentType: oid.SignedData,
Content: content,
}
token, err := convertToRawASN1(contentInfo, "")
if err != nil {
return nil, err
}
// generate response
return &timestamp.Response{
Status: pki.StatusInfo{
Status: pki.StatusGranted,
},
TimeStampToken: token,
}, nil
}
// generateTokenInfo generate timestamp token info.
func (tsa *TSA) generateTokenInfo(req *timestamp.Request, policy asn1.ObjectIdentifier) ([]byte, error) {
serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
return nil, err
}
nowFunc := tsa.NowFunc
if nowFunc == nil {
nowFunc = time.Now
}
info := timestamp.TSTInfo{
Version: 1,
Policy: policy,
MessageImprint: req.MessageImprint,
SerialNumber: serialNumber,
GenTime: nowFunc().UTC().Truncate(time.Second),
Accuracy: timestamp.Accuracy{
Seconds: 1,
},
}
return asn1.Marshal(info)
}
// generateSignedData generate signed data according to
func (tsa *TSA) generateSignedData(infoBytes []byte, requestCert bool) (cms.SignedData, error) {
var issuer asn1.RawValue
_, err := asn1.Unmarshal(tsa.cert.RawIssuer, &issuer)
if err != nil {
return cms.SignedData{}, err
}
contentType, err := convertToRawASN1([]interface{}{oid.TSTInfo}, "set")
if err != nil {
return cms.SignedData{}, err
}
infoDigest, err := hashutil.ComputeHash(crypto.SHA256, infoBytes)
if err != nil {
return cms.SignedData{}, err
}
messageDigest, err := convertToRawASN1([]interface{}{infoDigest}, "set")
if err != nil {
return cms.SignedData{}, err
}
signingTime, err := convertToRawASN1([]interface{}{time.Now().UTC()}, "set")
if err != nil {
return cms.SignedData{}, err
}
signed := cms.SignedData{
Version: 3,
DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{
{
Algorithm: oid.SHA256,
},
},
EncapsulatedContentInfo: cms.EncapsulatedContentInfo{
ContentType: oid.TSTInfo,
Content: infoBytes,
},
SignerInfos: []cms.SignerInfo{
{
Version: 1,
SignerIdentifier: cms.IssuerAndSerialNumber{
Issuer: issuer,
SerialNumber: tsa.cert.SerialNumber,
},
DigestAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: oid.SHA256,
},
SignedAttributes: cms.Attributes{
{
Type: oid.ContentType,
Values: contentType,
},
{
Type: oid.MessageDigest,
Values: messageDigest,
},
{
Type: oid.SigningTime,
Values: signingTime,
},
},
SignatureAlgorithm: pkix.AlgorithmIdentifier{
Algorithm: oid.SHA256WithRSA,
},
},
},
}
if requestCert {
certs, err := convertToRawASN1(tsa.cert.Raw, "tag:0")
if err != nil {
return cms.SignedData{}, err
}
signed.Certificates = certs
}
// sign data
signer := &signed.SignerInfos[0]
encodedAttributes, err := asn1.MarshalWithParams(signer.SignedAttributes, "set")
if err != nil {
return cms.SignedData{}, err
}
hashedAttributes, err := hashutil.ComputeHash(crypto.SHA256, encodedAttributes)
if err != nil {
return cms.SignedData{}, err
}
signer.Signature, err = rsa.SignPKCS1v15(rand.Reader, tsa.key, crypto.SHA256, hashedAttributes)
if err != nil {
return cms.SignedData{}, err
}
return signed, nil
}
// convertToRawASN1 convert any data ASN.1 data structure to asn1.RawValue.
func convertToRawASN1(val interface{}, params string) (asn1.RawValue, error) {
b, err := asn1.MarshalWithParams(val, params)
if err != nil {
return asn1.NullRawValue, err
}
var raw asn1.RawValue
_, err = asn1.UnmarshalWithParams(b, &raw, params)
if err != nil {
return asn1.NullRawValue, err
}
return raw, nil
}

View File

@ -0,0 +1,95 @@
package timestamptest
import (
"context"
"crypto/x509"
"testing"
"time"
"github.com/notaryproject/notation-go/crypto/timestamp"
"github.com/notaryproject/notation-go/internal/crypto/oid"
"github.com/notaryproject/notation-go/internal/crypto/pki"
)
func TestTSATimestampGranted(t *testing.T) {
// prepare TSA
now := time.Date(2021, 9, 18, 11, 54, 34, 0, time.UTC)
tsa, err := NewTSA()
if err != nil {
t.Fatalf("NewTSA() error = %v", err)
}
tsa.NowFunc = func() time.Time {
return now
}
// do timestamp
message := []byte("notation")
req, err := timestamp.NewRequestFromBytes(message)
if err != nil {
t.Fatalf("NewRequestFromString() error = %v", err)
}
ctx := context.Background()
resp, err := tsa.Timestamp(ctx, req)
if err != nil {
t.Fatalf("TSA.Timestamp() error = %v", err)
}
wantStatus := pki.StatusGranted
if got := resp.Status.Status; got != wantStatus {
t.Fatalf("Response.Status = %v, want %v", got, wantStatus)
}
// verify timestamp token
token, err := resp.SignedToken()
if err != nil {
t.Fatalf("Response.SignedToken() error = %v", err)
}
roots := x509.NewCertPool()
roots.AddCert(tsa.Certificate())
opts := x509.VerifyOptions{
Roots: roots,
}
if _, err := token.Verify(opts); err != nil {
t.Fatal("SignedToken.Verify() error =", err)
}
info, err := token.Info()
if err != nil {
t.Fatal("SignedToken.Info() error =", err)
}
if err := info.Verify(message); err != nil {
t.Errorf("TSTInfo.Verify() error = %v", err)
}
timestamp, accuracy := info.Timestamp()
wantTimestamp := now
if timestamp != wantTimestamp {
t.Errorf("TSTInfo.Timestamp() Timestamp = %v, want %v", timestamp, wantTimestamp)
}
wantAccuracy := time.Second
if accuracy != wantAccuracy {
t.Errorf("TSTInfo.Timestamp() Accuracy = %v, want %v", accuracy, wantAccuracy)
}
}
func TestTSATimestampRejection(t *testing.T) {
// prepare TSA
tsa, err := NewTSA()
if err != nil {
t.Fatalf("NewTSA() error = %v", err)
}
// do timestamp
message := []byte("notation")
req, err := timestamp.NewRequestFromBytes(message)
if err != nil {
t.Fatalf("NewRequestFromString() error = %v", err)
}
req.MessageImprint.HashAlgorithm.Algorithm = oid.SHA1WithRSA // set bad algorithm
ctx := context.Background()
resp, err := tsa.Timestamp(ctx, req)
if err != nil {
t.Fatalf("TSA.Timestamp() error = %v", err)
}
wantStatus := pki.StatusRejection
if got := resp.Status.Status; got != wantStatus {
t.Fatalf("Response.Status = %v, want %v", got, wantStatus)
}
}

132
crypto/timestamp/token.go Normal file
View File

@ -0,0 +1,132 @@
package timestamp
import (
"bytes"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"math/big"
"time"
"github.com/notaryproject/notation-go/internal/crypto/cms"
"github.com/notaryproject/notation-go/internal/crypto/hashutil"
"github.com/notaryproject/notation-go/internal/crypto/oid"
asn1util "github.com/notaryproject/notation-go/internal/encoding/asn1"
)
// SignedToken is a parsed timestamp token with signatures.
type SignedToken cms.ParsedSignedData
// ParseSignedToken parses ASN.1 BER-encoded structure to SignedToken
// without verification.
// Callers should invoke Verify to verify the content before comsumption.
func ParseSignedToken(data []byte) (*SignedToken, error) {
data, err := asn1util.ConvertToDER(data)
if err != nil {
return nil, err
}
signed, err := cms.ParseSignedData(data)
if err != nil {
return nil, err
}
if !oid.TSTInfo.Equal(signed.ContentType) {
return nil, fmt.Errorf("unexpected content type: %v", signed.ContentType)
}
return (*SignedToken)(signed), nil
}
// Verify verifies the signed token as CMS SignedData.
// An empty list of KeyUsages in VerifyOptions implies ExtKeyUsageTimeStamping.
func (t *SignedToken) Verify(opts x509.VerifyOptions) ([]*x509.Certificate, error) {
if len(opts.KeyUsages) == 0 {
opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}
}
signed := (*cms.ParsedSignedData)(t)
certs, err := signed.Verify(opts)
if err != nil {
return nil, err
}
// RFC 3161 2.3: The corresponding certificate MUST contain only one instance of
// the extended key usage field extension.
verifiedCerts := make([]*x509.Certificate, 0, len(certs))
for _, cert := range certs {
if len(cert.ExtKeyUsage) == 1 && len(cert.UnknownExtKeyUsage) == 0 {
verifiedCerts = append(verifiedCerts, cert)
}
}
if len(verifiedCerts) == 0 {
return nil, errors.New("unexpected number of extended key usages")
}
return verifiedCerts, nil
}
// Info returns the timestamping information.
func (t *SignedToken) Info() (*TSTInfo, error) {
var info TSTInfo
if _, err := asn1.Unmarshal(t.Content, &info); err != nil {
return nil, err
}
return &info, nil
}
// Accuracy ::= SEQUENCE {
// seconds INTEGER OPTIONAL,
// millis [0] INTEGER (1..999) OPTIONAL,
// micros [1] INTEGER (1..999) OPTIONAL }
type Accuracy struct {
Seconds int `asn1:"optional"`
Milliseconds int `asn1:"optional,tag:0"`
Microseconds int `asn1:"optional,tag:1"`
}
// TSTInfo ::= SEQUENCE {
// version INTEGER { v1(1) },
// policy TSAPolicyId,
// messageImprint MessageImprint,
// serialNumber INTEGER,
// genTime GeneralizedTime,
// accuracy Accuracy OPTIONAL,
// ordering BOOLEAN DEFAULT FALSE,
// nonce INTEGER OPTIONAL,
// tsa [0] GeneralName OPTIONAL,
// extensions [1] IMPLICIT Extensions OPTIONAL }
type TSTInfo struct {
Version int // fixed to 1 as defined in RFC 3161 2.4.2 Response Format
Policy asn1.ObjectIdentifier
MessageImprint MessageImprint
SerialNumber *big.Int
GenTime time.Time `asn1:"generalized"`
Accuracy Accuracy `asn1:"optional"`
Ordering bool `asn1:"optional,default:false"`
Nonce *big.Int `asn1:"optional"`
TSA asn1.RawValue `asn1:"optional,tag:0"`
Extensions []pkix.Extension `asn1:"optional,tag:1"`
}
// Verify verifies the message against the timestamp token information.
func (tst *TSTInfo) Verify(message []byte) error {
hashAlg := tst.MessageImprint.HashAlgorithm.Algorithm
hash, ok := oid.ConvertToHash(hashAlg)
if !ok {
return fmt.Errorf("unrecognized hash algorithm: %v", hashAlg)
}
messageDigest, err := hashutil.ComputeHash(hash, message)
if err != nil {
return err
}
if !bytes.Equal(tst.MessageImprint.HashedMessage, messageDigest) {
return errors.New("mismatch message digest")
}
return nil
}
// Timestamp returns the timestamp by TSA and its accuracy.
func (tst *TSTInfo) Timestamp() (time.Time, time.Duration) {
accuracy := time.Duration(tst.Accuracy.Seconds)*time.Second +
time.Duration(tst.Accuracy.Milliseconds)*time.Millisecond +
time.Duration(tst.Accuracy.Microseconds)*time.Microsecond
return tst.GenTime, accuracy
}

View File

@ -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 dir
import (
"io/fs"
"os"
"path/filepath"
)
// SysFS is virtual file system interface that support fs.FS and SysPath method.
type SysFS interface {
fs.FS
// SysPath returns the real system path of the given path items in the SysFS.
SysPath(items ...string) (string, error)
}
type sysFS struct {
fs.FS
root string
}
// SysPath returns the real system path of the given name in the SysFS.
func (s sysFS) SysPath(items ...string) (string, error) {
pathItems := []string{s.root}
pathItems = append(pathItems, items...)
return filepath.Join(pathItems...), nil
}
// NewSysFS returns the SysFS for the given root directory.
//
// Support one root directory for rc.1, and may support union directories FS
// after rc.1.
func NewSysFS(root string) SysFS {
return sysFS{
FS: os.DirFS(root),
root: root}
}
// ConfigFS is the config SysFS
func ConfigFS() SysFS {
return NewSysFS(userConfigDirPath())
}
// 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())
}

View File

@ -1,84 +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 dir
import (
"bytes"
"path/filepath"
"testing"
)
func TestSysFS_SysPath(t *testing.T) {
wantPath := filepath.FromSlash("/path/notation/config.json")
fsys := NewSysFS("/path/notation")
path, err := fsys.SysPath(PathConfigFile)
if err != nil {
t.Fatalf("SysPath() failed. err = %v", err)
}
if path != wantPath {
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, wantPath)
}
}
func TestOsFs(t *testing.T) {
wantData := []byte("data")
fsys := NewSysFS("./testdata")
// read test file
path, err := fsys.Open("data.txt")
if err != nil {
t.Fatalf("Open() failed. err = %v", err)
}
data := make([]byte, 4)
_, err = path.Read(data)
if err != nil {
t.Fatalf("Read() failed. err = %v", err)
}
if !bytes.Equal(data, wantData) {
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)
}
}

View File

@ -1,153 +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 dir implements Notation directory structure.
// [directory spec]: https://notaryproject.dev/docs/user-guides/how-to/directory-structure/
//
// Example:
//
// - Read config.json:
// file, err := dir.ConfigFS().Open(dir.PathConfigFile)
//
// - Get the path of config.json:
// path, err := dir.ConfigFS().SysPath(dir.PathConfigFile)
//
// - Read trustpolicy.json:
// file, err := dir.ConfigFS().Open(dir.PathTrustPolicy)
//
// - Get the path of trustpolicy.json:
// path, err := dir.ConfigFS().SysPath(dir.PathTrustPolicy)
//
// - Set custom configurations directory:
// dir.UserConfigDir = '/path/to/configurations/'
//
// Only user level directory is supported, and system level directory
// may be added later.
package dir
import (
"os"
"path"
"path/filepath"
)
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 (
// notation is the directory name for notation configurations.
notation = "notation"
)
// The relative path to {NOTATION_CONFIG}
const (
// PathConfigFile is the config.json file relative path.
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 = "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"
// LocalKeysDir is the directory name for local key relative path.
LocalKeysDir = "localkeys"
// LocalCertificateExtension defines the extension of the certificate files.
LocalCertificateExtension = ".crt"
// LocalKeyExtension defines the extension of the key files.
LocalKeyExtension = ".key"
// TrustStoreDir is the directory name of trust store.
TrustStoreDir = "truststore"
)
// The relative path to {NOTATION_LIBEXEC}
const (
// PathPlugins is the plugins directory relative path.
PathPlugins = "plugins"
)
// 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
}
// userLibexecDirPath returns the user level {NOTATION_LIBEXEC} path.
func userLibexecDirPath() string {
if UserLibexecDir == "" {
// set user libexec
UserLibexecDir = userConfigDirPath()
}
return UserLibexecDir
}
// 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
}
// LocalKeyPath returns the local key and local cert relative paths.
func LocalKeyPath(name string) (keyPath, certPath string) {
basePath := path.Join(LocalKeysDir, name)
return basePath + LocalKeyExtension, basePath + LocalCertificateExtension
}
// X509TrustStoreDir returns the trust store relative path.
//
// items includes named-store and cert-file names.
// the directory follows the pattern of
// {NOTATION_CONFIG}/truststore/x509/{store-type}/{named-store}/{cert-file}
func X509TrustStoreDir(items ...string) string {
pathItems := []string{TrustStoreDir, "x509"}
pathItems = append(pathItems, items...)
return path.Join(pathItems...)
}

View File

@ -1,98 +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 dir
import (
"os"
"path/filepath"
"testing"
)
func mockUserPath() (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_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)
}
}
func TestLocalKeyPath(t *testing.T) {
userConfigDir = mockUserPath
setup()
_ = userConfigDirPath()
_ = userLibexecDirPath()
gotKeyPath, gotCertPath := LocalKeyPath("web")
if gotKeyPath != "localkeys/web.key" {
t.Fatalf(`LocalKeyPath() gotKeyPath = %q, want "localkeys/web.key"`, gotKeyPath)
}
if gotCertPath != "localkeys/web.crt" {
t.Fatalf(`LocalKeyPath() gotCertPath = %q, want "localkeys/web.crt"`, gotCertPath)
}
}
func TestX509TrustStoreDir(t *testing.T) {
userConfigDir = mockUserPath
setup()
_ = userConfigDirPath()
_ = userLibexecDirPath()
if got := X509TrustStoreDir("ca", "web"); got != "truststore/x509/ca/web" {
t.Fatalf(`X509TrustStoreDir() = %q, want "truststore/x509/ca/web"`, got)
}
}

View File

@ -1 +0,0 @@
data

130
errors.go
View File

@ -1,128 +1,8 @@
// 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
// ErrorPushSignatureFailed is used when failed to push signature to the
// target registry.
//
// Deprecated: Use PushSignatureFailedError instead.
type ErrorPushSignatureFailed = PushSignatureFailedError
import "errors"
// PushSignatureFailedError is used when failed to push signature to the
// target registry.
type PushSignatureFailedError struct {
Msg string
}
func (e PushSignatureFailedError) Error() string {
if e.Msg != "" {
return "failed to push signature to registry with error: " + e.Msg
}
return "failed to push signature to registry"
}
// 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 {
Msg string
}
func (e VerificationInconclusiveError) Error() string {
if e.Msg != "" {
return e.Msg
}
return "signature verification was inclusive due to an unexpected error"
}
// 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 {
Msg string
}
func (e NoApplicableTrustPolicyError) Error() string {
if e.Msg != "" {
return e.Msg
}
return "there is no applicable trust policy for the given artifact"
}
// 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 {
Msg string
}
func (e SignatureRetrievalFailedError) Error() string {
if e.Msg != "" {
return e.Msg
}
return "unable to retrieve the digital signature from the registry"
}
// 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 {
Msg string
}
func (e VerificationFailedError) Error() string {
if e.Msg != "" {
return e.Msg
}
return "signature verification failed"
}
// 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 {
Msg string
}
func (e UserMetadataVerificationFailedError) Error() string {
if e.Msg != "" {
return e.Msg
}
return "unable to find specified metadata in the signature"
}
// SignOptions errors
var (
ErrExpiryNotSpecified = errors.New("expiry not specified")
)

View File

@ -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)
}
})
}
}

View File

@ -1,94 +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"
"fmt"
"github.com/notaryproject/notation-core-go/signature"
"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/signer"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
var (
// exampleDesc is an example of the target manifest descriptor.
exampleDesc = ocispec.Descriptor{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: "sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c",
Size: 528,
}
// 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}
)
// ExampleLocalSign demonstrates how to use signer.Sign to sign an artifact
// at local (without using a registry.Repository).
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)
if err != nil {
panic(err) // Handle error
}
// Both COSE ("application/cose") and JWS ("application/jose+json")
// signature mediaTypes are supported.
exampleSignatureMediaType := cose.MediaTypeEnvelope
// exampleSignOptions is an example of notation.SignerSignOptions.
exampleSignOptions := notation.SignerSignOptions{
SignatureMediaType: exampleSignatureMediaType,
SigningAgent: "example signing agent",
}
// local sign core process
// upon successful signing, signature envelope and signerInfo are returned.
// signatureEnvelope can be used in a verification process later on.
signatureEnvelope, signerInfo, err := exampleSigner.Sign(context.Background(), exampleDesc, exampleSignOptions)
if err != nil {
panic(err) // Handle error
}
fmt.Println("Successfully signed")
// a peek of the signature envelope generated from Sign
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":{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c","size":528}}
// signerInfo SigningAgent: example signing agent
}

View File

@ -1,203 +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"
"encoding/pem"
"errors"
"fmt"
"os"
"github.com/notaryproject/notation-core-go/signature/cose"
"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"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// 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{
Version: "1.0",
TrustPolicies: []trustpolicy.OCITrustPolicy{
{
Name: "test-statement-name",
RegistryScopes: []string{"example/software"},
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{"*"},
},
},
}
// ExampleLocalVerify demonstrates how to use verifier.Verify to verify a
// signature of the target artifact at local (without using a
// registry.Repository).
func Example_localVerify() {
// exampleArtifactReference is an example of the target artifact reference
exampleArtifactReference := "example/software@sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c"
// Both COSE ("application/cose") and JWS ("application/jose+json")
// signature mediaTypes are supported.
exampleSignatureMediaType := cose.MediaTypeEnvelope
// exampleTargetDescriptor is an example of the target manifest descriptor.
exampleTargetDescriptor := ocispec.Descriptor{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: "sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c",
Size: 528,
}
// exampleSignatureEnvelope is a valid signature envelope.
exampleSignatureEnvelope := generateExampleSignatureEnvelope()
// exampleVerifyOptions is an example of notation.VerifierVerifyOptions
exampleVerifyOptions := notation.VerifierVerifyOptions{
ArtifactReference: exampleArtifactReference,
SignatureMediaType: exampleSignatureMediaType,
}
// 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
if err := createTrustStore(); 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
}
// local verify core process
// upon successful verification, the signature verification outcome is
// returned.
outcome, err := exampleVerifier.Verify(context.Background(), exampleTargetDescriptor, 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":{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c","size":528}}
}
func generateExampleSignatureEnvelope() []byte {
// exampleSignatureEnvelopePem is a valid signature envelope in COSE format
// wrapped by a PEM block.
// The signature envelope is generated in a previous Sign process.
// Users should replace it with their own signature envelope.
// Regarding how to generate such signature envelopes, users could refer to
// `example_localSign_test.go`.
exampleSignatureEnvelopePem := `-----BEGIN EXAMPLE SIGNATURE ENVELOPE-----
0oRYnqUBOCQCgXgcaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZQN4K2FwcGxp
Y2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS5wYXlsb2FkLnYxK2pzb254GmlvLmNuY2Yu
bm90YXJ5LnNpZ25pbmdUaW1lwRpju22GeBxpby5jbmNmLm5vdGFyeS5zaWduaW5n
U2NoZW1la25vdGFyeS54NTA5ohghgVkDRDCCA0AwggIooAMCAQICAVEwDQYJKoZI
hvcNAQELBQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdT
ZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxDzANBgNVBAMTBmFscGluZTAgFw0wMDA4
MjkxMzUwMDBaGA8yMTIzMDgyOTEzNTAwMFowTjELMAkGA1UEBhMCVVMxCzAJBgNV
BAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxDzANBgNV
BAMTBmFscGluZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKHIN6hL
MjQwy3wfDhw+HYYvjNTTytQLMuTV/OHPoL2nGbqDy08JqTn5upz7exjzwfRu0usc
YRTW0cU2H2FIyvpGgo9/F4wUX+ZRnsG0iSlZMUPNv2sVO9HHkltyaWs62oHjfSVE
2fyX4uDH54qSE8HJjKIoo/Hhqx7JI8lcmWyXdFjDCkQ1lYSz1IjzFVrf+He2LWSL
c2nGkCrV5l4LEwk1qSKJbN4H7TWI60KDLFHpVHQ/LzgFjfSdvP0sgvrkofytSn8l
JW6rn5+HYvAxAcZ7T+cJ12GyS9Y7Y7FIBMQFY0MU9cyOfV9+pt7d2CqgkIdXLndN
i+aJzm2Os4+ezekCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
CCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAvAIwS8uxLEaYuqILgnRUm+p0R
o7xdezfm+pg297AEZwfE9acb8009usmvgMlkfo46HRMGzCEvtd5Vfvak9i8KpWzl
DWOdfT2thkUWO9iY3qMFiN4ipCmjM32VAe5UUxl3RLmQKS20zv9yVXEfX37tNDdV
GgD/WBhJUreCQyWAPTPnf0LaPh4iLBNCo/o3Z8CGKLzzzpa8iji3xW/69RhKu5+t
8RWc/N4sljWmXbCeTd2B8XTqZGwWwmpThAQyU40CqngGAS6ADTVNDgbJZqhwkgUx
J4W6iRzekCshdPUnDpeS8DNULE5dFGObIhiwH4/40n/Th/VS0zxzkvPzdCmueBtp
by5jbmNmLm5vdGFyeS5zaWduaW5nQWdlbnRuTm90YXRpb24vMS4wLjBYtXsidGFy
Z2V0QXJ0aWZhY3QiOnsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tl
ci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsImRpZ2VzdCI6InNoYTI1
NjpjMGQ0ODhhODAwZTQxMjdjMzM0YWQyMGQ2MWQ3YmMyMWI0MDk3NTQwMzI3MjE3
ZGZhYjUyMjYyYWRjMDIzODBjIiwic2l6ZSI6NTI4fX1ZAQBUIYPA45B/iFylmloW
s/NpTVsheuedJb6nnXK0XR46BYs4AeCXVVYSRDK2Bq+tA9C7BXHoeCMcqnAHa8qs
ZR/fcMa9FrEPI6Pl8QVE/6QRMkT+Drn+9CSFxzHk6CU9S1vRsUVYNcibyejnuVEE
RPYaORrnfTc5wIs4XxeqprmrLimMMNn+u82Uadtc57tbHbY8Vh4XEKP++hBJJNvQ
E60X5aWKIS2RnOEc4n9T7LdN0bOL1OoM1lW4iTFMhzfcy/VmF8PrOStFS9LllX3J
69V0WwHbmD33cjtVBDCF44UXRWgLQGbE6yaaVmdxEUBGKqSUeHf8Gp7WoZ/YaFmz
xQr/
-----END EXAMPLE SIGNATURE ENVELOPE-----`
block, _ := pem.Decode([]byte(exampleSignatureEnvelopePem))
if block == nil {
panic(errors.New("invalid signature envelope pem block"))
}
// block.Bytes contains the binary of the signature envelope.
return block.Bytes
}
func createTrustStore() 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/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
}
return os.WriteFile("tmp/truststore/x509/ca/valid-trust-store/NotationLocalExample.pem", []byte(exampleX509Certificate), 0600)
}

View File

@ -1,85 +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"
"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"
)
// Both COSE ("application/cose") and JWS ("application/jose+json")
// signature mediaTypes are supported.
var exampleSignatureMediaType = cose.MediaTypeEnvelope
// ExampleRemoteSign demonstrates how to use notation.Sign to sign an artifact
// in the remote registry and push the signature to the remote.
func Example_remoteSign() {
// 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)
// exampleSignOptions is an example of notation.SignOptions.
exampleSignOptions := notation.SignOptions{
SignerSignOptions: notation.SignerSignOptions{
SignatureMediaType: exampleSignatureMediaType,
},
ArtifactReference: exampleArtifactReference,
}
// 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)
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)
}

View File

@ -1,132 +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"
)
// ExampleRemoteVerify demonstrates how to use notation.Verify to verify
// signatures of an artifact in the remote registry.
func Example_remoteVerify() {
// 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.
// 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},
TrustStores: []string{"ca:valid-trust-store"},
TrustedIdentities: []string{"*"},
},
},
}
// 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
if err := generateTrustStore(); 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 generateTrustStore() 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
}
return os.WriteFile("tmp/truststore/x509/ca/valid-trust-store/NotationExample.pem", []byte(exampleX509Certificate), 0600)
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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"}`
}

View File

@ -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)
}

22
go.mod
View File

@ -1,26 +1,8 @@
module github.com/notaryproject/notation-go
go 1.23.0
go 1.17
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/golang-jwt/jwt/v4 v4.3.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
)
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/x448/float16 v0.8.4 // indirect
golang.org/x/sync v0.14.0 // indirect
)

60
go.sum
View File

@ -1,60 +1,4 @@
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/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/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog=
github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
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/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/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=
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=

View File

@ -1,39 +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 set
// Set is a map as a set data structure.
type Set[T comparable] map[T]struct{}
// Add adds the element of type T into the Set.
func (s Set[T]) Add(elem T) {
s[elem] = struct{}{}
}
// Contains checks if element exists in the Set.
func (s Set[T]) Contains(elem T) bool {
_, ok := s[elem]
return ok
}
// New creates an empty Set for elements of type T.
func New[T comparable]() Set[T] {
return make(map[T]struct{})
}
// NewWithSize creates an empty Set of fixed size for elements of type T.
func NewWithSize[T comparable](size int) Set[T] {
return make(map[T]struct{}, size)
}

View File

@ -0,0 +1,92 @@
// Package cms verifies signatures in Cryptographic Message Syntax (CMS) / PKCS7
// defined in RFC 5652.
package cms
import (
"crypto/x509/pkix"
"encoding/asn1"
"math/big"
)
// ContentInfo ::= SEQUENCE {
// contentType ContentType,
// content [0] EXPLICIT ANY DEFINED BY contentType }
type ContentInfo struct {
ContentType asn1.ObjectIdentifier
Content asn1.RawValue `asn1:"explicit,tag:0"`
}
// SignedData ::= SEQUENCE {
// version CMSVersion,
// digestAlgorithms DigestAlgorithmIdentifiers,
// encapContentInfo EncapsulatedContentInfo,
// certificates [0] IMPLICIT CertificateSet OPTIONAL,
// crls [1] IMPLICIT CertificateRevocationLists OPTIONAL,
// signerInfos SignerInfos }
type SignedData struct {
Version int
DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"`
EncapsulatedContentInfo EncapsulatedContentInfo
Certificates asn1.RawValue `asn1:"optional,tag:0"`
CRLs []pkix.CertificateList `asn1:"optional,tag:1"`
SignerInfos []SignerInfo `asn1:"set"`
}
// EncapsulatedContentInfo ::= SEQUENCE {
// eContentType ContentType,
// eContent [0] EXPLICIT OCTET STRING OPTIONAL }
type EncapsulatedContentInfo struct {
ContentType asn1.ObjectIdentifier
Content []byte `asn1:"explicit,optional,tag:0"`
}
// SignerInfo ::= SEQUENCE {
// version CMSVersion,
// sid SignerIdentifier,
// digestAlgorithm DigestAlgorithmIdentifier,
// signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL,
// signatureAlgorithm SignatureAlgorithmIdentifier,
// signature SignatureValue,
// unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL }
// Only version 1 is supported. As defined in RFC 5652 5.3, SignerIdentifier
// is IssuerAndSerialNumber when version is 1.
type SignerInfo struct {
Version int
SignerIdentifier IssuerAndSerialNumber
DigestAlgorithm pkix.AlgorithmIdentifier
SignedAttributes Attributes `asn1:"optional,tag:0"`
SignatureAlgorithm pkix.AlgorithmIdentifier
Signature []byte
UnsignedAttributes Attributes `asn1:"optional,tag:1"`
}
// IssuerAndSerialNumber ::= SEQUENCE {
// issuer Name,
// serialNumber CertificateSerialNumber }
type IssuerAndSerialNumber struct {
Issuer asn1.RawValue
SerialNumber *big.Int
}
// Attribute ::= SEQUENCE {
// attrType OBJECT IDENTIFIER,
// attrValues SET OF AttributeValue }
type Attribute struct {
Type asn1.ObjectIdentifier
Values asn1.RawValue `asn1:"set"`
}
// Attribute ::= SET SIZE (1..MAX) OF Attribute
type Attributes []Attribute
// TryGet tries to find the attribute by the given identifier, parse and store
// the result in the value pointed to by out.
func (a Attributes) TryGet(identifier asn1.ObjectIdentifier, out interface{}) error {
for _, attribute := range a {
if identifier.Equal(attribute.Type) {
_, err := asn1.Unmarshal(attribute.Values.Bytes, out)
return err
}
}
return ErrAttributeNotFound
}

View File

@ -0,0 +1,62 @@
package cms
import "errors"
// ErrExpectSignedData is returned if wrong content is provided when signed
// data is expected.
var ErrExpectSignedData = errors.New("cms: signed data expected")
// ErrAttributeNotFound is returned if attribute is not found in a given set.
var ErrAttributeNotFound = errors.New("attribute not found")
// Verification errors
var (
ErrSignerNotFound = VerificationError{Message: "signer not found"}
ErrCertificateNotFound = VerificationError{Message: "certificate not found"}
)
// SyntaxError indicates that the ASN.1 data is invalid.
type SyntaxError struct {
Message string
Detail error
}
// Error returns error message.
func (e SyntaxError) Error() string {
msg := "cms: syntax error"
if e.Message != "" {
msg += ": " + e.Message
}
if e.Detail != nil {
msg += ": " + e.Detail.Error()
}
return msg
}
// Unwrap returns the internal error.
func (e SyntaxError) Unwrap() error {
return e.Detail
}
// VerificationError indicates verification failures.
type VerificationError struct {
Message string
Detail error
}
// Error returns error message.
func (e VerificationError) Error() string {
msg := "cms: verification failure"
if e.Message != "" {
msg += ": " + e.Message
}
if e.Detail != nil {
msg += ": " + e.Detail.Error()
}
return msg
}
// Unwrap returns the internal error.
func (e VerificationError) Unwrap() error {
return e.Detail
}

View File

@ -0,0 +1,238 @@
package cms
import (
"bytes"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/hex"
"time"
"github.com/notaryproject/notation-go/internal/crypto/hashutil"
"github.com/notaryproject/notation-go/internal/crypto/oid"
)
// ParsedSignedData is a parsed SignedData structure for golang friendly types.
type ParsedSignedData struct {
Content []byte
ContentType asn1.ObjectIdentifier
Certificates []*x509.Certificate
CRLs []pkix.CertificateList
Signers []SignerInfo
}
// ParseSignedData parses ASN.1 DER-encoded SignedData structure to golang friendly types.
func ParseSignedData(data []byte) (*ParsedSignedData, error) {
var contentInfo ContentInfo
if _, err := asn1.Unmarshal(data, &contentInfo); err != nil {
return nil, SyntaxError{Message: "invalid content info", Detail: err}
}
if !oid.SignedData.Equal(contentInfo.ContentType) {
return nil, ErrExpectSignedData
}
var signedData SignedData
if _, err := asn1.Unmarshal(contentInfo.Content.Bytes, &signedData); err != nil {
return nil, SyntaxError{Message: "invalid signed data", Detail: err}
}
certs, err := x509.ParseCertificates(signedData.Certificates.Bytes)
if err != nil {
return nil, SyntaxError{Message: "invalid signed data", Detail: err}
}
return &ParsedSignedData{
Content: signedData.EncapsulatedContentInfo.Content,
ContentType: signedData.EncapsulatedContentInfo.ContentType,
Certificates: certs,
CRLs: signedData.CRLs,
Signers: signedData.SignerInfos,
}, nil
}
// Verify attempts to verify the content in the parsed signed data against the signer
// information. The `Intermediates` in the verify options will be ignored and
// re-contrusted using the certificates in the parsed signed data.
// If more than one signature is present, the successful validation of any signature
// implies that the content in the parsed signed data is valid.
// On successful verification, the list of signing certificates that successfully
// verify is returned.
// If all signatures fail to verify, the last error is returned.
// References:
// - RFC 5652 5 Signed-data Content Type
// - RFC 5652 5.4 Message Digest Calculation Process
// - RFC 5652 5.6 Signature Verification Process
// WARNING: this function doesn't do any revocation checking.
func (d *ParsedSignedData) Verify(opts x509.VerifyOptions) ([]*x509.Certificate, error) {
if len(d.Signers) == 0 {
return nil, ErrSignerNotFound
}
if len(d.Certificates) == 0 {
return nil, ErrCertificateNotFound
}
intermediates := x509.NewCertPool()
for _, cert := range d.Certificates {
intermediates.AddCert(cert)
}
opts.Intermediates = intermediates
verifiedSignerMap := map[string]*x509.Certificate{}
var lastErr error
for _, signer := range d.Signers {
cert, err := d.verify(signer, opts)
if err != nil {
lastErr = err
continue
}
thumbprint, err := hashutil.ComputeHash(crypto.SHA256, cert.Raw)
if err != nil {
return nil, err
}
verifiedSignerMap[hex.EncodeToString(thumbprint)] = cert
}
if len(verifiedSignerMap) == 0 {
return nil, lastErr
}
verifiedSigners := make([]*x509.Certificate, 0, len(verifiedSignerMap))
for _, cert := range verifiedSignerMap {
verifiedSigners = append(verifiedSigners, cert)
}
return verifiedSigners, nil
}
// verify verifies the trust in a top-down manner.
// References:
// - RFC 5652 5.4 Message Digest Calculation Process
// - RFC 5652 5.6 Signature Verification Process
func (d *ParsedSignedData) verify(signer SignerInfo, opts x509.VerifyOptions) (*x509.Certificate, error) {
// find signer certificate
cert := d.getCertificate(signer.SignerIdentifier)
if cert == nil {
return nil, ErrCertificateNotFound
}
// verify signer certificate
if _, err := cert.Verify(opts); err != nil {
return cert, VerificationError{Detail: err}
}
// verify signature
return cert, d.verifySignature(signer, cert)
}
// verifySignature verifies the signature with a trusted certificate.
// References:
// - RFC 5652 5.4 Message Digest Calculation Process
// - RFC 5652 5.6 Signature Verification Process
func (d *ParsedSignedData) verifySignature(signer SignerInfo, cert *x509.Certificate) error {
// verify signature
algorithm := getSignatureAlgorithmFromOID(
signer.DigestAlgorithm.Algorithm,
signer.SignatureAlgorithm.Algorithm,
)
if algorithm == x509.UnknownSignatureAlgorithm {
return VerificationError{Message: "unknown signature algorithm"}
}
signed := d.Content
if len(signer.SignedAttributes) > 0 {
encoded, err := asn1.MarshalWithParams(signer.SignedAttributes, "set")
if err != nil {
return VerificationError{Message: "invalid signed attributes", Detail: err}
}
signed = encoded
}
if err := cert.CheckSignature(algorithm, signed, signer.Signature); err != nil {
return VerificationError{Detail: err}
}
// verify attributes if present
if len(signer.SignedAttributes) == 0 {
return nil
}
var contentType asn1.ObjectIdentifier
if err := signer.SignedAttributes.TryGet(oid.ContentType, &contentType); err != nil {
return VerificationError{Message: "invalid content type", Detail: err}
}
if !d.ContentType.Equal(contentType) {
return VerificationError{Message: "mismatch content type"}
}
var expectedDigest []byte
if err := signer.SignedAttributes.TryGet(oid.MessageDigest, &expectedDigest); err != nil {
return VerificationError{Message: "invalid message digest", Detail: err}
}
hash, ok := oid.ConvertToHash(signer.DigestAlgorithm.Algorithm)
if !ok {
return VerificationError{Message: "unsupported digest algorithm"}
}
actualDigest, err := hashutil.ComputeHash(hash, d.Content)
if err != nil {
return VerificationError{Message: "hash failure", Detail: err}
}
if !bytes.Equal(expectedDigest, actualDigest) {
return VerificationError{Message: "mismatch message digest"}
}
// sanity check on signing time
var signingTime time.Time
if err := signer.SignedAttributes.TryGet(oid.SigningTime, &signingTime); err != nil {
if err == ErrAttributeNotFound {
return nil
}
return VerificationError{Message: "invalid signing time", Detail: err}
}
if signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter) {
return VerificationError{Message: "signature signed when cert is inactive"}
}
return nil
}
// getCertificate finds the certificate by issuer name and issuer-specific
// serial number.
// Reference: RFC 5652 5 Signed-data Content Type
func (d *ParsedSignedData) getCertificate(ref IssuerAndSerialNumber) *x509.Certificate {
for _, cert := range d.Certificates {
if bytes.Equal(cert.RawIssuer, ref.Issuer.FullBytes) && cert.SerialNumber.Cmp(ref.SerialNumber) == 0 {
return cert
}
}
return nil
}
// getSignatureAlgorithmFromOID converts ASN.1 digest and signature algorithm identifiers
// to golang signature algorithms.
func getSignatureAlgorithmFromOID(digestAlg, sigAlg asn1.ObjectIdentifier) x509.SignatureAlgorithm {
switch {
case oid.RSA.Equal(sigAlg):
switch {
case oid.SHA1.Equal(digestAlg):
return x509.SHA1WithRSA
case oid.SHA256.Equal(digestAlg):
return x509.SHA256WithRSA
case oid.SHA384.Equal(digestAlg):
return x509.SHA384WithRSA
case oid.SHA512.Equal(digestAlg):
return x509.SHA512WithRSA
}
case oid.SHA1WithRSA.Equal(sigAlg):
return x509.SHA1WithRSA
case oid.SHA256WithRSA.Equal(sigAlg):
return x509.SHA256WithRSA
case oid.SHA384WithRSA.Equal(sigAlg):
return x509.SHA384WithRSA
case oid.SHA512WithRSA.Equal(sigAlg):
return x509.SHA512WithRSA
case oid.ECDSAWithSHA1.Equal(sigAlg):
return x509.ECDSAWithSHA1
case oid.ECDSAWithSHA256.Equal(sigAlg):
return x509.ECDSAWithSHA256
case oid.ECDSAWithSHA384.Equal(sigAlg):
return x509.ECDSAWithSHA384
case oid.ECDSAWithSHA512.Equal(sigAlg):
return x509.ECDSAWithSHA512
}
return x509.UnknownSignatureAlgorithm
}

View File

@ -0,0 +1,95 @@
package cms
import (
"crypto/x509"
"os"
"reflect"
"testing"
"time"
)
func TestVerifySignedData(t *testing.T) {
// parse signed data
sigBytes, err := os.ReadFile("testdata/TimeStampToken.p7s")
if err != nil {
t.Fatal("failed to read test signature:", err)
}
signed, err := ParseSignedData(sigBytes)
if err != nil {
t.Fatal("ParseSignedData() error =", err)
}
// basic check on parsed signed data
if got := len(signed.Certificates); got != 4 {
t.Fatalf("len(Certificates) = %v, want %v", got, 4)
}
if got := len(signed.Signers); got != 1 {
t.Fatalf("len(Signers) = %v, want %v", got, 1)
}
// verify with no root CAs and should fail
roots := x509.NewCertPool()
opts := x509.VerifyOptions{
Roots: roots,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping},
CurrentTime: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
}
if _, err := signed.Verify(opts); err == nil {
t.Errorf("ParseSignedData.Verify() error = %v, wantErr %v", err, true)
} else if vErr, ok := err.(VerificationError); !ok {
t.Errorf("ParseSignedData.Verify() error = %v, want VerificationError", err)
} else if _, ok := vErr.Detail.(x509.UnknownAuthorityError); !ok {
t.Errorf("ParseSignedData.Verify() VerificationError.Detail = %v, want UnknownAuthorityError", err)
}
// verify with proper root CA
rootCABytes, err := os.ReadFile("testdata/GlobalSignRootCA.crt")
if err != nil {
t.Fatal("failed to read root CA certificate:", err)
}
if ok := roots.AppendCertsFromPEM(rootCABytes); !ok {
t.Fatal("failed to load root CA certificate")
}
verifiedSigners, err := signed.Verify(opts)
if err != nil {
t.Fatal("ParseSignedData.Verify() error =", err)
}
if !reflect.DeepEqual(verifiedSigners, signed.Certificates[:1]) {
t.Fatalf("ParseSignedData.Verify() = %v, want %v", verifiedSigners, signed.Certificates[:1])
}
}
func TestVerifyCorruptedSignedData(t *testing.T) {
// parse signed data
sigBytes, err := os.ReadFile("testdata/TimeStampToken.p7s")
if err != nil {
t.Fatal("failed to read test signature:", err)
}
signed, err := ParseSignedData(sigBytes)
if err != nil {
t.Fatal("ParseSignedData() error =", err)
}
// corrupt the content
signed.Content = []byte("corrupted data")
// verify with no root CAs and should fail
roots := x509.NewCertPool()
rootCABytes, err := os.ReadFile("testdata/GlobalSignRootCA.crt")
if err != nil {
t.Fatal("failed to read root CA certificate:", err)
}
if ok := roots.AppendCertsFromPEM(rootCABytes); !ok {
t.Fatal("failed to load root CA certificate")
}
opts := x509.VerifyOptions{
Roots: roots,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping},
CurrentTime: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
}
if _, err := signed.Verify(opts); err == nil {
t.Errorf("ParseSignedData.Verify() error = %v, wantErr %v", err, true)
} else if _, ok := err.(VerificationError); !ok {
t.Errorf("ParseSignedData.Verify() error = %v, want VerificationError", err)
}
}

View File

@ -0,0 +1,15 @@
// Package hashutil provides utilities for hash.
package hashutil
import "crypto"
// ComputeHash computes the digest of the message with the given hash algorithm.
// Callers should check the availability of the hash algorithm before invoking.
func ComputeHash(hash crypto.Hash, message []byte) ([]byte, error) {
h := hash.New()
_, err := h.Write(message)
if err != nil {
return nil, err
}
return h.Sum(nil), nil
}

View File

@ -0,0 +1,25 @@
package oid
import (
"crypto"
"encoding/asn1"
)
// ConvertToHash converts ASN.1 digest algorithm identifier to golang crypto hash
// if it is available.
func ConvertToHash(alg asn1.ObjectIdentifier) (crypto.Hash, bool) {
var hash crypto.Hash
switch {
case SHA1.Equal(alg):
hash = crypto.SHA1
case SHA256.Equal(alg):
hash = crypto.SHA256
case SHA384.Equal(alg):
hash = crypto.SHA384
case SHA512.Equal(alg):
hash = crypto.SHA512
default:
return hash, false
}
return hash, hash.Available()
}

View File

@ -0,0 +1,67 @@
// Package oid collects object identifiers for crypto algorithms.
package oid
import "encoding/asn1"
// OIDs for hash algorithms
var (
// SHA1 (id-sha1) is defined in RFC 8017 B.1 Hash Functions
SHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}
// SHA256 (id-sha256) is defined in RFC 8017 B.1 Hash Functions
SHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}
// SHA384 (id-sha384) is defined in RFC 8017 B.1 Hash Functions
SHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2}
// SHA512 (id-sha512) is defined in RFC 8017 B.1 Hash Functions
SHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3}
)
// OIDs for signature algorithms
var (
// RSA is defined in RFC 8017 C ASN.1 Module
RSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
// SHA1WithRSA is defined in RFC 8017 C ASN.1 Module
SHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5}
// SHA256WithRSA is defined in RFC 8017 C ASN.1 Module
SHA256WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}
// SHA384WithRSA is defined in RFC 8017 C ASN.1 Module
SHA384WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12}
// SHA512WithRSA is defined in RFC 8017 C ASN.1 Module
SHA512WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13}
// ECDSAWithSHA1 is defined in ANSI X9.62
ECDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1}
// ECDSAWithSHA256 is defined in RFC 5758 3.2 ECDSA Signature Algorithm
ECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2}
// ECDSAWithSHA384 is defined in RFC 5758 3.2 ECDSA Signature Algorithm
ECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3}
// ECDSAWithSHA512 is defined in RFC 5758 3.2 ECDSA Signature Algorithm
ECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4}
)
// OIDs defined in RFC 5652 Cryptographic Message Syntax (CMS)
var (
// SignedData (id-signedData) is defined in RFC 5652 5.1 SignedData Type
SignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
// ContentType (id-ct-contentType) is defined in RFC 5652 3 General Syntax
ContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3}
// MessageDigest (id-messageDigest) is defined in RFC 5652 11.2 Message Digest
MessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4}
// SigningTime (id-signingTime) is defined in RFC 5652 11.3 Signing Time
SigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5}
)
// TSTInfo (id-ct-TSTInfo) is defined in RFC 3161 2.4.2 Response Format
var TSTInfo = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4}

View File

@ -0,0 +1,50 @@
// Package pki contains certificate management protocol structures
// defined in RFC 2510.
package pki
import "encoding/asn1"
// PKIStatus is defined in RFC 2510 3.2.3.
const (
StatusGranted = 0 // you got exactly what you asked for
StatusGrantedWithMods = 1 // you got something like what you asked for
StatusRejection = 2 // you don't get it, more information elsewhere in the message
StatusWaiting = 3 // the request body part has not yet been processed, expect to hear more later
StatusRevocationWarning = 4 // this message contains a warning that a revocation is imminent
StatusRevocationNotification = 5 // notification that a revocation has occurred
StatusKeyUpdateWarning = 6 // update already done for the oldCertId specified in the key update request message
)
// PKIFailureInfo is defined in RFC 2510 3.2.3 and RFC 3161 2.4.2.
const (
FailureInfoBadAlg = 0 // unrecognized or unsupported Algorithm Identifier
FailureInfoBadMessageCheck = 1 // integrity check failed (e.g., signature did not verify)
FailureInfoBadRequest = 2 // transaction not permitted or supported
FailureInfoBadTime = 3 // messageTime was not sufficiently close to the system time, as defined by local policy
FailureInfoBadCertID = 4 // no certificate could be found matching the provided criteria
FailureInfoBadDataFormat = 5 // the data submitted has the wrong format
FailureInfoWrongAuthority = 6 // the authority indicated in the request is different from the one creating the response token
FailureInfoIncorrectData = 7 // the requester's data is incorrect (used for notary services)
FailureInfoMissingTimeStamp = 8 // the timestamp is missing but should be there (by policy)
FailureInfoBadPOP = 9 // the proof-of-possession failed
FailureInfoTimeNotAvailable = 14 // the TSA's time source is not available
FailureInfoUnacceptedPolicy = 15 // the requested TSA policy is not supported by the TSA.
FailureInfoUnacceptedExtension = 16 // the requested extension is not supported by the TSA.
FailureInfoAddInfoNotAvailable = 17 // the additional information requested could not be understood or is not available
FailureInfoSystemFailure = 25 // the request cannot be handled due to system failure
)
// StatusInfo contains status codes and failure information for PKI messages.
// PKIStatusInfo ::= SEQUENCE {
// status PKIStatus,
// statusString PKIFreeText OPTIONAL,
// failInfo PKIFailureInfo OPTIONAL }
// PKIStatus ::= INTEGER
// PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String
// PKIFailureInfo ::= BIT STRING
// Reference: RFC 2510 3.2.3 Status codes and Failure Information for PKI messages.
type StatusInfo struct {
Status int
StatusString []string `asn1:"optional,utf8"`
FailInfo asn1.BitString `asn1:"optional"`
}

View File

@ -0,0 +1,54 @@
// Package asn1 decodes BER-encoded ASN.1 data structures and encodes in DER.
// Note: DER is a subset of BER.
// Reference: http://luca.ntop.org/Teaching/Appunti/asn1.html
package asn1
import (
"bytes"
"encoding/asn1"
)
// Common errors
var (
ErrEarlyEOF = asn1.SyntaxError{Msg: "early EOF"}
ErrExpectConstructed = asn1.SyntaxError{Msg: "constructed value expected"}
ErrExpectPrimitive = asn1.SyntaxError{Msg: "primitive value expected"}
ErrUnsupportedLength = asn1.StructuralError{Msg: "length method not supported"}
)
// Value represents an ASN.1 value.
type Value interface {
// Encode encodes the value to the value writer in DER.
Encode(ValueWriter) error
// EncodedLen returns the length in bytes of the encoded data.
EncodedLen() int
}
// Decode decodes BER-encoded ASN.1 data structures.
func Decode(r ValueReader) (Value, error) {
peekIdentifier, err := r.ReadByte()
if err != nil {
return nil, err
}
if err = r.UnreadByte(); err != nil {
return nil, err
}
if isPrimitive(peekIdentifier) {
return DecodePrimitive(r)
}
return DecodeConstructed(r)
}
// ConvertToDER converts BER-encoded ASN.1 data structures to DER-encoded.
func ConvertToDER(ber []byte) ([]byte, error) {
v, err := Decode(bytes.NewReader(ber))
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(make([]byte, 0, v.EncodedLen()))
if err = v.Encode(buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,68 @@
package asn1
import (
"encoding/asn1"
"reflect"
"testing"
)
func TestConvertToDER(t *testing.T) {
type data struct {
Type asn1.ObjectIdentifier
Value []byte
}
want := data{
Type: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
Value: []byte{
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
},
}
ber := []byte{
// Constructed value
0x30,
// Constructed value length
0x2e,
// Type identifier
0x06,
// Type length
0x09,
// Type content
0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
// Value identifier
0x04,
// Value length in BER
0x81, 0x20,
// Value content
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
}
der, err := ConvertToDER(ber)
if err != nil {
t.Errorf("ConvertToDER() error = %v", err)
return
}
var got data
rest, err := asn1.Unmarshal(der, &got)
if err != nil {
t.Errorf("Failed to decode converted data: %v", err)
return
}
if len(rest) > 0 {
t.Errorf("Unexpected rest data: %v", rest)
return
}
if !reflect.DeepEqual(got, want) {
t.Errorf("got = %v, want %v", got, want)
}
}

View File

@ -0,0 +1,116 @@
package asn1
import "io"
// isPrimitive checks the primitive flag in the identifier.
// Returns true if the value is primitive.
func isPrimitive(identifier byte) bool {
return identifier&0x20 == 0
}
// encodedLengthSize gives the number of octets used for encoding the length.
func encodedLengthSize(length int) int {
if length < 0x80 {
return 1
}
lengthSize := 1
for ; length > 0; lengthSize++ {
length >>= 8
}
return lengthSize
}
// encodeLength encodes length octets in DER.
func encodeLength(w io.ByteWriter, length int) error {
// DER restriction: short form must be used for length less than 128
if length < 0x80 {
return w.WriteByte(byte(length))
}
// DER restriction: long form must be encoded in the minimum number of octets
lengthSize := encodedLengthSize(length)
err := w.WriteByte(0x80 | byte(lengthSize-1))
if err != nil {
return err
}
for i := lengthSize - 1; i > 0; i-- {
if err = w.WriteByte(byte(length >> (8 * (i - 1)))); err != nil {
return err
}
}
return nil
}
// decodeIdentifier decodes identifier octets.
func decodeIdentifier(r io.ByteReader) ([]byte, error) {
b, err := r.ReadByte()
if err != nil {
return nil, err
}
// low-tag-number form
identifier := []byte{b}
// high-tag-number form
if b&0x1f == 0x1f {
for {
b, err = r.ReadByte()
if err != nil {
if err == io.EOF {
return nil, ErrEarlyEOF
}
return nil, err
}
identifier = append(identifier, b)
if b&0x80 != 0 {
break
}
}
}
return identifier, nil
}
// decodeLength decodes length octets.
// Indefinite length is not supported
func decodeLength(r io.ByteReader) (int, error) {
b, err := r.ReadByte()
if err != nil {
if err == io.EOF {
return 0, ErrEarlyEOF
}
return 0, err
}
switch {
case b < 0x80:
// short form
return int(b), nil
case b == 0x80:
// Indefinite-length method is not supported.
return 0, ErrUnsupportedLength
}
// long form
n := int(b & 0x7f)
if n > 4 {
// length must fit the memory space of the int type.
return 0, ErrUnsupportedLength
}
var length int
for i := 0; i < n; i++ {
b, err = r.ReadByte()
if err != nil {
if err == io.EOF {
return 0, ErrEarlyEOF
}
return 0, err
}
length = (length << 8) | int(b)
}
if length < 0 {
// double check in case that length is over 31 bits.
return 0, ErrUnsupportedLength
}
return length, nil
}

View File

@ -0,0 +1,172 @@
package asn1
import (
"bytes"
"testing"
)
func Test_encodeLength(t *testing.T) {
tests := []struct {
name string
length int
want []byte
wantErr bool
}{
{
name: "zero length",
length: 0,
want: []byte{0x00},
},
{
name: "short form",
length: 42,
want: []byte{0x2a},
},
{
name: "short form in max",
length: 127,
want: []byte{0x7f},
},
{
name: "long form in min",
length: 128,
want: []byte{0x81, 0x80},
},
{
name: "long form",
length: 1234567890,
want: []byte{0x84, 0x49, 0x96, 0x02, 0xd2},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
buf := bytes.NewBuffer(nil)
if err := encodeLength(buf, tt.length); (err != nil) != tt.wantErr {
t.Errorf("encodeLength() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got := buf.Bytes(); !bytes.Equal(got, tt.want) {
t.Errorf("encoded length = %v, want %v", got, tt.want)
}
})
}
}
func Test_decodeIdentifier(t *testing.T) {
tests := []struct {
name string
encoded []byte
want []byte
wantErr bool
}{
{
name: "empty identifier",
wantErr: true,
},
{
name: "low-tag-number form",
encoded: []byte{0x0b},
want: []byte{0x0b},
},
{
name: "no extra read in low-tag-number form",
encoded: []byte{0x0b, 0x42},
want: []byte{0x0b},
},
{
name: "high-tag-number form",
encoded: []byte{0x1f, 0x17, 0xdf},
want: []byte{0x1f, 0x17, 0xdf}, // tag: 0x012345
},
{
name: "no extra read in high-tag-number form",
encoded: []byte{0x1f, 0x17, 0xdf, 0x42},
want: []byte{0x1f, 0x17, 0xdf}, // tag: 0x012345
},
{
name: "high-tag-number form (no termination)",
encoded: []byte{0x1f, 0x17, 0x5f},
wantErr: true,
},
{
name: "high-tag-number form (EOF)",
encoded: []byte{0x1f, 0x17},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := bytes.NewReader(tt.encoded)
got, err := decodeIdentifier(r)
if (err != nil) != tt.wantErr {
t.Errorf("decodeIdentifier() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !bytes.Equal(got, tt.want) {
t.Errorf("decodeIdentifier() = %v, want %v", got, tt.want)
}
})
}
}
func Test_decodeLength(t *testing.T) {
tests := []struct {
name string
encoded []byte
want int
wantErr bool
}{
{
name: "empty length",
wantErr: true,
},
{
name: "short form",
encoded: []byte{0x2a},
want: 42,
},
{
name: "no extra read in short form",
encoded: []byte{0x2a, 0x42},
want: 42,
},
{
name: "long form",
encoded: []byte{0x84, 0x49, 0x96, 0x02, 0xd2},
want: 1234567890,
},
{
name: "long form in BER",
encoded: []byte{0x81, 0x2a},
want: 42,
},
{
name: "no extra read in long form",
encoded: []byte{0x84, 0x49, 0x96, 0x02, 0xd2, 0x42},
want: 1234567890,
},
{
name: "long form (indefinite)",
encoded: []byte{0x80, 0x42, 0x00, 0x00},
wantErr: true,
},
{
name: "long form (EOF)",
encoded: []byte{0x84, 0x49, 0x96, 0x02},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := bytes.NewReader(tt.encoded)
got, err := decodeLength(r)
if (err != nil) != tt.wantErr {
t.Errorf("decodeLength() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("decodeLength() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -0,0 +1,68 @@
package asn1
import "io"
// ConstructedValue represents a value in constructed encoding.
type ConstructedValue struct {
identifier []byte
length int
members []Value
}
// Encode encodes the constructed value to the value writer in DER.
func (v ConstructedValue) Encode(w ValueWriter) error {
_, err := w.Write(v.identifier)
if err != nil {
return err
}
if err = encodeLength(w, v.length); err != nil {
return err
}
for _, value := range v.members {
if err = value.Encode(w); err != nil {
return err
}
}
return nil
}
// EncodedLen returns the length in bytes of the encoded data.
func (v ConstructedValue) EncodedLen() int {
return len(v.identifier) + encodedLengthSize(v.length) + v.length
}
// DecodeConstructed decodes a constructed value in BER.
func DecodeConstructed(r ValueReader) (Value, error) {
identifier, err := decodeIdentifier(r)
if err != nil {
return nil, err
}
if isPrimitive(identifier[0]) {
return nil, ErrExpectConstructed
}
expectedLength, err := decodeLength(r)
if err != nil {
return nil, err
}
var members []Value
encodedLength := 0
r = LimitValueReader(r, int64(expectedLength))
for {
value, err := Decode(r)
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
members = append(members, value)
encodedLength += value.EncodedLen()
}
return ConstructedValue{
identifier: identifier,
length: encodedLength,
members: members,
}, nil
}

View File

@ -0,0 +1,51 @@
package asn1
import "io"
// ValueReader is the interface for reading a value.
type ValueReader interface {
io.Reader
io.ByteScanner
}
// ValueWriter is the interface for writing a value.
type ValueWriter interface {
io.Writer
io.ByteWriter
}
// limitedValueReader limits the amount of data returned.
type limitedValueReader struct {
io.LimitedReader
S io.ByteScanner
}
// LimitValueReader returns a ValueReader, which limits the amount of data returned.
func LimitValueReader(r ValueReader, n int64) ValueReader {
return &limitedValueReader{
LimitedReader: io.LimitedReader{
R: r,
N: n,
},
S: r,
}
}
func (l *limitedValueReader) ReadByte() (c byte, err error) {
if l.N <= 0 {
return 0, io.EOF
}
c, err = l.S.ReadByte()
if err == nil {
l.N--
}
return
}
func (l *limitedValueReader) UnreadByte() (err error) {
err = l.S.UnreadByte()
if err == nil {
l.N++
}
return
}

View File

@ -0,0 +1,55 @@
package asn1
import "io"
// PrimitiveValue represents a value in primitive encoding.
type PrimitiveValue struct {
identifier []byte
content []byte
}
// Encode encodes the primitive value to the value writer in DER.
func (v PrimitiveValue) Encode(w ValueWriter) error {
_, err := w.Write(v.identifier)
if err != nil {
return err
}
if err = encodeLength(w, len(v.content)); err != nil {
return err
}
_, err = w.Write(v.content)
return err
}
// EncodedLen returns the length in bytes of the encoded data.
func (v PrimitiveValue) EncodedLen() int {
return len(v.identifier) + encodedLengthSize(len(v.content)) + len(v.content)
}
// DecodePrimitive decodes a primitive value in BER.
func DecodePrimitive(r ValueReader) (Value, error) {
identifier, err := decodeIdentifier(r)
if err != nil {
return nil, err
}
if !isPrimitive(identifier[0]) {
return nil, ErrExpectPrimitive
}
length, err := decodeLength(r)
if err != nil {
return nil, err
}
content := make([]byte, length)
_, err = io.ReadFull(r, content)
if err != nil {
if err == io.EOF {
return nil, ErrEarlyEOF
}
return nil, err
}
return PrimitiveValue{
identifier: identifier,
content: content,
}, nil
}

View File

@ -1,68 +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 envelope
import (
"errors"
"fmt"
"time"
"github.com/notaryproject/notation-core-go/signature"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// MediaTypePayloadV1 is the supported content type for signature's payload.
const (
MediaTypePayloadV1 = "application/vnd.cncf.notary.payload.v1+json"
AnnotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256"
)
// Payload describes the content that gets signed.
type Payload struct {
TargetArtifact ocispec.Descriptor `json:"targetArtifact"`
}
// ValidatePayloadContentType validates signature payload's content type.
func ValidatePayloadContentType(payload *signature.Payload) error {
switch payload.ContentType {
case MediaTypePayloadV1:
return nil
default:
return fmt.Errorf("payload content type %q not supported", payload.ContentType)
}
}
// SanitizeTargetArtifact filters out unrelated ocispec.Descriptor fields based
// on notation spec (https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#payload).
func SanitizeTargetArtifact(targetArtifact ocispec.Descriptor) ocispec.Descriptor {
return ocispec.Descriptor{
MediaType: targetArtifact.MediaType,
Digest: targetArtifact.Digest,
Size: targetArtifact.Size,
Annotations: targetArtifact.Annotations,
}
}
// SigningTime returns the signing time of a signature envelope blob
func SigningTime(signerInfo *signature.SignerInfo) (time.Time, error) {
// sanity check
if signerInfo == nil {
return time.Time{}, errors.New("failed to generate annotations: signerInfo cannot be nil")
}
signingTime := signerInfo.SignedAttributes.SigningTime
if signingTime.IsZero() {
return time.Time{}, errors.New("signing time is missing")
}
return signingTime.UTC(), nil
}

View File

@ -1,141 +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 envelope
import (
"errors"
"testing"
"time"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-core-go/signature/cose"
"github.com/notaryproject/notation-core-go/signature/jws"
gcose "github.com/veraison/go-cose"
)
var (
validCoseSignatureEnvelope []byte
)
func init() {
msg := gcose.Sign1Message{
Headers: gcose.NewSign1Message().Headers,
Payload: []byte("valid"),
Signature: []byte("valid"),
}
validCoseSignatureEnvelope, _ = msg.MarshalCBOR()
}
const invalidMediaType = "invalid"
func checkErrorEqual(expected, got error) bool {
if expected == nil && got == nil {
return true
}
if expected != nil && got != nil {
return expected.Error() == got.Error()
}
return false
}
func TestValidateEnvelopeMediaType(t *testing.T) {
tests := []struct {
name string
mediaType string
expectedErr error
}{
{
name: "jws signature media type",
mediaType: jws.MediaTypeEnvelope,
expectedErr: nil,
},
{
name: "cose signature media type",
mediaType: cose.MediaTypeEnvelope,
expectedErr: nil,
},
{
name: "invalid media type",
mediaType: invalidMediaType,
expectedErr: errors.New("invalid envelope media type"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validateEnvelopeMediaType(tt.mediaType); !checkErrorEqual(tt.expectedErr, err) {
t.Fatalf("expected validate envelope media type err: %v, got: %v", tt.expectedErr, err)
}
})
}
}
func TestValidatePayloadContentType(t *testing.T) {
payload := &signature.Payload{
ContentType: MediaTypePayloadV1,
}
err := ValidatePayloadContentType(payload)
if !isErrEqual(nil, err) {
t.Fatalf("ValidatePayloadContentType() expects error: %v, but got: %v.", nil, err)
}
payload = &signature.Payload{
ContentType: "invalid",
}
err = ValidatePayloadContentType(payload)
expect := errors.New("payload content type \"invalid\" not supported")
if !isErrEqual(expect, err) {
t.Fatalf("ValidatePayloadContentType() expects error: %v, but got: %v.", expect, err)
}
}
func TestSigningTime(t *testing.T) {
testTime, err := time.Parse(time.RFC3339, "2023-03-14T04:45:22Z")
if err != nil {
t.Fatal("failed to generate time")
}
signerInfo := signature.SignerInfo{
SignedAttributes: signature.SignedAttributes{
SigningTime: testTime,
},
}
signingTime, err := SigningTime(&signerInfo)
if err != nil {
t.Fatalf("failed to get signing time from signature: %v", err)
}
expectedSigningTime := "2023-03-14T04:45:22Z"
if signingTime.Format(time.RFC3339) != expectedSigningTime {
t.Fatalf("expected signing time: %q, got: %q", expectedSigningTime, signingTime.Format(time.RFC3339))
}
}
func isErrEqual(wanted, got error) bool {
if wanted == nil && got == nil {
return true
}
if wanted != nil && got != nil {
return wanted.Error() == got.Error()
}
return false
}
// validateEnvelopeMediaType validetes envelope media type is supported by
// notation-core-go.
func validateEnvelopeMediaType(mediaType string) error {
for _, types := range signature.RegisteredEnvelopeTypes() {
if mediaType == types {
return nil
}
}
return errors.New("invalid envelope media type")
}

View File

@ -1,153 +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 (
"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")
// 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)
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -1,48 +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 mockfs
import (
"io/fs"
"path/filepath"
"github.com/notaryproject/notation-go/dir"
)
type sysFSMock struct {
fs.FS
root string
}
// SysPath returns the system path of the FS.
func (s sysFSMock) SysPath(items ...string) (string, error) {
pathItems := []string{s.root}
pathItems = append(pathItems, items...)
return filepath.Join(pathItems...), nil
}
// NewSysFSMock returns a SysFS mock of the given FS.
func NewSysFSMock(fsys fs.FS) dir.SysFS {
return sysFSMock{
FS: fsys,
root: ""}
}
// NewSysFSWithRootMock returns a SysFS mock of the given fs and
// a root directory
func NewSysFSWithRootMock(fsys fs.FS, root string) dir.SysFS {
return sysFSMock{
FS: fsys,
root: root}
}

View File

@ -1,228 +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 mock
import (
"context"
_ "embed"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
//go:embed testdata/ca_valid_sig_env.json
var MockCaValidSigEnv []byte
//go:embed testdata/ca_invalid_sig_env.json
var MockCaInvalidSigEnv []byte
//go:embed testdata/sa_valid_sig_env.json
var MockSaValidSigEnv []byte
//go:embed testdata/ca_plugin_sig_env.json
var MockCaPluginSigEnv []byte // extended attributes are "SomeKey":"SomeValue", "io.cncf.notary.verificationPlugin":"plugin-name"
//go:embed testdata/sa_invalid_sig_env.json
var MockSaInvalidSigEnv []byte
//go:embed testdata/ca_expired_sig_env.json
var MockCaExpiredSigEnv []byte
//go:embed testdata/sa_expired_sig_env.json
var MockSaExpiredSigEnv []byte
//go:embed testdata/sa_plugin_sig_env.json
var MockSaPluginSigEnv []byte // extended attributes are "SomeKey":"SomeValue", "io.cncf.notary.verificationPlugin":"plugin-name"
//go:embed testdata/sig_env_with_metadata.json
var MockSigEnvWithMetadata []byte
//go:embed testdata/ca_incompatible_pluginver_sig_env_1.0.9.json
var MockCaIncompatiblePluginVerSigEnv_1_0_9 []byte
//go:embed testdata/ca_incompatible_pluginver_sig_env_1.0.1.json
var MockCaIncompatiblePluginVerSigEnv_1_0_1 []byte
//go:embed testdata/ca_incompatible_pluginver_sig_env_1.2.3.json
var MockCaIncompatiblePluginVerSigEnv_1_2_3 []byte
//go:embed testdata/ca_incompatible_pluginver_sig_env_1.1.0-alpha.json
var MockCaIncompatiblePluginVerSigEnv_1_1_0_alpha []byte
//go:embed testdata/ca_compatible_pluginver_sig_env_0.0.9.json
var MockCaCompatiblePluginVerSigEnv_0_0_9 []byte
//go:embed testdata/ca_compatible_pluginver_sig_env_1.0.0-alpha.json
var MockCaCompatiblePluginVerSigEnv_1_0_0_alpha []byte
//go:embed testdata/ca_compatible_pluginver_sig_env_1.0.0-alpha.beta.json
var MockCaCompatiblePluginVerSigEnv_1_0_0_alpha_beta []byte
//go:embed testdata/ca_compatible_pluginver_sig_env_1.0.0.json
var MockCaCompatiblePluginVerSigEnv_1_0_0 []byte
var (
SampleArtifactUri = "registry.acme-rockets.io/software/net-monitor@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e"
SampleDigest = digest.Digest("sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e")
ZeroDigest = digest.Digest("sha256:0000000000000000000000000000000000000000000000000000000000000000")
Annotations = map[string]string{"key": "value"}
ImageDescriptor = ocispec.Descriptor{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: SampleDigest,
Size: 528,
Annotations: Annotations,
}
SigManfiestDescriptor = ocispec.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: SampleDigest,
Size: 300,
Annotations: Annotations,
}
TestImageDescriptor = ocispec.Descriptor{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: digest.Digest("sha256:fe7e9333395060c2f5e63cf36a38fba10176f183b4163a5794e081a480abba5f"),
Size: 942,
Annotations: nil,
}
JwsSigEnvDescriptor = ocispec.Descriptor{
MediaType: "application/jose+json",
Digest: SampleDigest,
Size: 100,
Annotations: Annotations,
}
PluginExtendedCriticalAttribute = signature.Attribute{
Key: "SomeKey",
Critical: true,
Value: "SomeValue",
}
MetadataSigEnvDescriptor = ocispec.Descriptor{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: digest.Digest("sha256:5a07385af4e6b6af81b0ebfd435aedccdfa3507f0609c658209e1aba57159b2b"),
Size: 942,
Annotations: map[string]string{"io.wabbit-networks.buildId": "123", "io.wabbit-networks.buildTime": "1672944615"},
}
)
type Repository struct {
ResolveResponse ocispec.Descriptor
ResolveError error
ListSignaturesResponse []ocispec.Descriptor
ListSignaturesError error
FetchSignatureBlobResponse []byte
FetchSignatureBlobError error
MissMatchDigest bool
ExceededNumOfSignatures bool
PushSignatureError error
}
func NewRepository() Repository {
return Repository{
ResolveResponse: ImageDescriptor,
ListSignaturesResponse: []ocispec.Descriptor{SigManfiestDescriptor},
FetchSignatureBlobResponse: MockCaValidSigEnv,
}
}
func (t Repository) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
if t.MissMatchDigest {
return ocispec.Descriptor{
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
Digest: ZeroDigest,
Size: 528,
Annotations: Annotations,
}, nil
}
return t.ResolveResponse, t.ResolveError
}
func (t Repository) ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error {
if t.ExceededNumOfSignatures {
t.ListSignaturesResponse = []ocispec.Descriptor{SigManfiestDescriptor, SigManfiestDescriptor}
}
err := fn(t.ListSignaturesResponse)
if err != nil {
return err
}
return t.ListSignaturesError
}
func (t Repository) FetchSignatureBlob(ctx context.Context, desc ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) {
return t.FetchSignatureBlobResponse, JwsSigEnvDescriptor, t.FetchSignatureBlobError
}
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
ExecuteResponse interface{}
ExecuteError error
}
func (p *PluginMock) GetMetadata(ctx context.Context, req *plugin.GetMetadataRequest) (*plugin.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 {
return resp, nil
}
return nil, p.ExecuteError
}
func (p *PluginMock) DescribeKey(ctx context.Context, req *plugin.DescribeKeyRequest) (*plugin.DescribeKeyResponse, error) {
panic("not implemented") // TODO: Implement
}
func (p *PluginMock) GenerateSignature(ctx context.Context, req *plugin.GenerateSignatureRequest) (*plugin.GenerateSignatureResponse, error) {
panic("not implemented") // TODO: Implement
}
func (p *PluginMock) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) {
panic("not implemented") // TODO: Implement
}
type PluginManager struct {
PluginCapabilities []plugin.Capability
GetPluginError error
PluginRunnerLoadError error
PluginRunnerExecuteResponse interface{}
PluginRunnerExecuteError error
}
func (pm PluginManager) Get(ctx context.Context, name string) (plugin.Plugin, error) {
return &PluginMock{
Metadata: plugin.GetMetadataResponse{
Name: "plugin-name",
Description: "for mocking in unit tests",
Version: "1.0.0",
URL: ".",
SupportedContractVersions: []string{"1.0"},
Capabilities: pm.PluginCapabilities,
},
ExecuteResponse: pm.PluginRunnerExecuteResponse,
ExecuteError: pm.PluginRunnerExecuteError,
}, pm.GetPluginError
}
func (pm PluginManager) List(ctx context.Context) ([]string, error) {
panic("not implemented")
}

View File

@ -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
}

View File

@ -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)
}
})
}

View File

@ -1 +0,0 @@
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQwMDoyMTozNi0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjAuMC45In0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"VXWolnwKhCSYn1x1_0CUpvUxEihiuKCJ9Ae2Lm--gkW_tfbBlTzkq4TciXM4u4V9MBtbDczQ8k748tmEb7qbzPPT6CEPGMBX8WN7kDStqXGILmpIE5M7Z1nYVIYkgQPk_w6FyC291bluQQGu0yqNrAO3pT1Ym5DoHAyRHLROdDRChntI4Qrz5DGrjBsiibo_GAOxw1jY1ENvo5dlSTAgnZm9jkfbY0gsYTXuNGYk2atS0H1W_MVRdgDSI9gbQ6amLUf-qy_gcbl5UT8Pa5fWb_1KZPtAqoh4hA5PW4UKkxFE0Wz2pUAs9RpYI-xpw1B6KGtgiI9MuTYDFMmTXHBRig"}

View File

@ -1 +0,0 @@
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMzowMzoyMy0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMC4wLWFscGhhLmJldGEifQ","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"iJthtqbz0O5nFuo5Z9nRddEjyZp3RG-KOY6SSB3sc8AgDBdT5Fjp9yltIoqTl-BLZhrGOAFeO0T_1JVsPbZZMxzJq4fb3gPaIPItrendkpit1m2RaB8fK1D_I6Vqu1_rGiYaxDcNpaqn1T_isxr4MVRekcLSNQnG3iMdJ0k-Attf8JdCXE0EWKyLBStMVAfo0J39ShFcwyIMvO0vm2_TRDVbcKovpY0vFrfyE2pFIChnJECmivImdKmBMIW78vEtN6qBrKskI3HzA9N1XjxGY4GOAu30iqtNRanO65nZGng0lqpJd15bAwUaqj-KD_BAZIUT9T2qCf2COF9GKvc3NQ"}

View File

@ -1 +0,0 @@
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMjo1NzowNC0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMC4wLWFscGhhIn0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"xZqE2HZye4qPmG688z875mHySGV_MoWOb99wChu-hInU8-CyxMesxzVCo_boG3Oae6tj6MKwdJ-Dj2cKbI3S4aX2l6t5IRFLB5z4DuIsDhmKZj9iN5LjtP8ua5_fni9dBk4e9c9TAsMq1hjXyNEen2rC1dzP_bcNYnoOs1yRWpO4JAcsslMYeqUIKKf39kzlOxOKIsJ8YhZoNeRc3HnAu4hlX2XpXwArovvMZtg1Akp6qCjVQcQQUTb_M0JeytmR8R5tdr_ZYqh-rCWbIe5tNU4u9jCP8xvlXPdSjpHgpmPsEnNd4u4gnLFxuYAq5l3UkdGDLXUsGrTx_Bi_LoFHUQ"}

View File

@ -1 +0,0 @@
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMzowNTo1OC0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMC4wIn0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"JG7Gk8HwJbkUheyX9eRoBDPezynCmMetYATNsW0U4ERBiagKO-DxRMN4lqHxcVFf7HXVRCWPf3A6aIYo6Vox0fHNFDWyX7g4qcD0wy8mSIgt9FsN5EBFqkgUxfC2o_5OrlUEsbaN8vU3tH4jNoTjWEcT6cNVNv7gltzkTQDQFdgl7DC-Bf12p9HJsSQQlJqdS-BhDYp-ou7dwgd3jeomureLC6kOhaU3ssmSsn69cdCt9cZgZ9U9-5knjyicGUDaCpPHWpz3_R8JgyLq3L8nzEetPBHRShwMPUwV42F_9_C2-gXR7ZVaU3ENshViL0p0T70U4VElOb7IxqAMWRmIlw"}

View File

@ -1,12 +0,0 @@
{
"payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ",
"protected": "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmV4cGlyeSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjAyMi0wNy0yOVQyMzo1OTowMFoiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nVGltZSI6IjIwMjItMDctMjlUMDA6MDA6MDBaIn0",
"header": {
"x5c": [
"MIIEWDCCAsCgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MCAXDTIwMTAwOTA3MDAwMFoYDzIxMjIwODA2MjAzODQ1WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAwE8YkFUAA0R7aUkRYxHKYoVbFPx9xhuNovLKDy72/7X0+j4XdGP4C0aAX2KLfgy9OR1RIUwtpMyI7k7ZFRd+ljcMW/FgbirfhkY/8axjamOYMBO0Qg+w93oaI6HA1gvZ/WZem4PHu68LlZhLQ2BrQwCz/F/3Ft0IZ2S1aF6N6vajx2le8xTI5hQS+UZFPQGrBUqrjcYc6GkL8XqL+rLGZaKGfh3c7bF9cEbA1H2Tm6MDFnfoFemerbP3v19JoUH+EtOnvYmNZWEU51RaLsNGkC3E/unXAnIfXrNxHDcbehyfa5y3AT10Shiron6O4Bc9S0MvwtXyLT6qein3Nh0VKBFUMSdthu5ZrSR28T9wDWHMXngpa115VjHOQDY3gDPwfzZ0xitN3NpMnivxculGUCkEQpst957tqQNJpS/zipI5Mtej0YOAhVKGQMjDIJekZ2DXDNd1X3xfahrR5VEQF0gnRFhA3vhycDqFj4E6Hoc5y3SxnFqrhX3w2wyFt/xRAgMBAAGjJzAlMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0BAQsFAAOCAYEAAdONCAJxdB7H0uFDw6H+8Z5MtoRdJe6ZhlM2O5WMzkC1DLSyrF7arPnUMTeSyNS2Fx1BU38n5R1wvdgSfWtjm7o2ZyR8JQ+AngPklUCTNeL18kxNNXpmjDuMvsRlfHcr5hherjiQ49jWlpFqGRrNtZQWiVEI0r9Qz8DtZTw3GYF4MSuotA6wuUjolI1V2oMn/gdt8FFo0XUTDyiA12qpZzkUHY1rg3zJxKq3pIk04E7k6rFakHyZL91ipV2UeSbNq9vwLL7cglfPJ8+J+9AKvIPDstDF5k0ivUCYH5fIFZBGoceLiNfHSMcqA/qWfErqLBWAkACRUNyCWpAEv3DfDRbTHId0n6QQwOXj5d9YnDrmOLvQcn/sa+ZBfFMK7RdG9uVwMRyo+sRUnxo+v2lcvYwWymL7ONQqVWZbTJCxuG90Unxa3cQHZiKB5mgKweMft+vp6C3IQFhFfP8j1kvRTJq8ZqSEBADppUuBZJ1KWalwauK0AE4jpHlE0KsYDXiP",
"MIIEizCCAvOgAwIBAgIBATANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MCAXDTIwMDkwOTA3MDAwMFoYDzIxMjIwOTA1MjAzODQ1WjBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSTm90YXRpb24gVGVzdCBSb290MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAxxAZ8VZegqBUctz3BkwhObZKnW+KsN5/N1/u2vPLmEzHDj6xgd8Hn0JoughDaxeQCV66NC2obqPnPp4+68G/qZnxkXVXdFyqVodu4FgPUjiqcJjft7bh45BVgLFpOqSqDQ3ko30B7gdGfIIkoBj/8gz3tHnmIvl3MywtOhDeGnlLNzBY52wVmhPIdKOaW/7WkMrXKFCkLkNICGnIpWuyBtC+7RfM8hG6eRW1KCm5xrkRmn5ptonjxix/JTGj4me/NMkwdVkz6wcCSAJnqTgHi2oqk73qqNu0LHsEMFBF8IGqmVkn2MOHkFamPBokzQ6HXXfvR4nbcWQZCUgRinPTVg9CF0B6XSCEMCSH5kveZxTQtAFRB6NosbzuU5jDmJgpbDfauev7Eg/6bZzphcugRkVuwulymzsake5Jbvs9Kyw3CNPYH2G3Kli1FNhfc46ugXHbIfXgNQcou3xabcu+r6cFRqqK6NmV9ouMQRj8Ri95Gp2BUlpTEFhcvMb9d4nXAgMBAAGjWjBYMA4GA1UdDwEB/wQEAwICBDATBgNVHSUEDDAKBggrBgEFBQcDAzASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBS5FZjt9UsEPkcKrStrnjSpTq4kDTANBgkqhkiG9w0BAQsFAAOCAYEAKtxfv12LzM85bxOMp5++pIDa6eMcBaurYbAM2yC9B6LuHf0JGeFdNqt4Fw38Ajooj2vWMWBrARVEZRVqTC5+ZSN2meGBXBXlT4n8FdEdmv+05iwVYdmDFp8FKeoOZZZF23u+r2OrazJo1ufWmoSI2P0lEfZQQFQElltWu3QH+OLOWXJmB7KbLKyheelGK5XhtAYYapRdW4sKJ398ybpv5C1oALCcTwoSmvH8wW5J4/gjmhKICYh2goMauf0lesdxj+0His7E8blOWrUmfOB5dp73XawLKcd/UxHN8zAPC08LDL9NMcihn3ZHKi7/dtkiV2iSaDPD1ChSGdqfXIysYqOhYoktgAfBZ43CWnqQhgB8NezRKdOStYC3P2AGJW18irxxTRp2CO+gnXEcyhyr+cvyf0j8MkRSaHLXzjIrECu8BUitB6sKughdN13fs5t5SIiO6foeFdvIpZFFKO8s+4oTOSDCos2WFoC+8TZS6r583OtFLmywl1HRgQkobGgw"
],
"io.cncf.notary.SigningAgent": "Notation/1.0.0"
},
"signature": "RZtqCD4KGh5_CD8wjG69TJIzzB4Cr-cxQhKTvZJYsRVIJyl3s5D0215GhBrggomVk9-LGD2FdWd2VfuaLd4bmhW3rSV3ltmAext7DNQFg2xtMeYSeCL2U_ygN2j4bc80RDaX8w_zOTVOmuhW6i2jgwRjWXdDaJeYTbZA2syA5R38tYYewVcZJ6U057Wsflt5yPWJCdxZLuTago5CkbLASL8HHnmlUkDvKKB1Y9SNDOQ3AmGP4-XJykcX_MfPo5RGRvZE-zHUJOEKj3ryfC0UTUT7V1ISTagqOt7zOa1BEzgQ-1GQk1MbaPPZWkiOZX4RqMXMV3hVqtDuZxlpT25KzZPm1USwWwJkycv7YB69fc2aoHJAPo-39uEV9fdAz_03whnrQSpfJbmHHTXMJkWKrZ5ozU-8zlEttWyL5D85zAouSMVXWm22zMrDW-XxST9QoeV4b1_BedW1PwJDbeU6P1hhobnQh3jHmSueVl_WZ5_g8_iVepSmSBcR1e4WpoPi"
}

View File

@ -1 +0,0 @@
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMjo0OTowMi0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMC4xIn0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"tb2xdd03j1ATBoG1K-9QmCjNeTnK-LKLHdZS44NJ0G5MfrzSFv56w3_FDqnS1jki8FTmGVUMdPAOciTuyoP_nREMBMr9QYn-qOAHisVrvxAcqmWEL-4Uoa_VIzmPvq-_wJKw9L_oZ2m-b9dx93tl2t2z0gxQaAgtVWJP6ap47lKlri6IFeFIXDq6jpdC9sy3q_wifnxFaZ9LM3892Pp7aMLvnT_TdTPxT1AHSq6ZOvddPbStvSUVICXZLmsglFym2c8RzatxulrnGlZ1fKKS0gR7W96-L1JsqIV5KeBMXq8vFnG-rK4fsqa0FeBBkmOOV6ZKKIruvfm7Z-SJ-nJLZw"}

View File

@ -1 +0,0 @@
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQwMDo0NzoxOC0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMC45In0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"zdA6RgbS3-QUdrhJuhAz-4wi55PZjy0CezOyTpg0UP1zxRfigVefPne86GEhGmiC-m-QlJC6bWSYFdkF3EoBL1CpGo46zUeaGKhQXM0Db1I8VKhJE20o1T83yXm-_ZVgDEe3_LUhu_KYs-jvkfJu_DGl6DJdBp_lkEpc9Br3tYUvgkxtF2LlvSUNYuc4oILnidj2sYFO5o7IBKdDoBVlQ3Z29s2Z6NUzy48ab9mxZCq0T9-uGj8636GJ3yJ78086GI_lt-0_mXdJ592WguWb3WBogCz9NvLm-byPIC7cP4RpHRqJQRsvYp6txgsrDqy2T1I0BEsf-Fp1FSxBdWMwXA"}

View File

@ -1 +0,0 @@
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMjo0MDozNC0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMS4wLWFscGhhIn0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"In4X5VH7wiFuGRaop36jSxFMvgAmbnZ7Pwhl1iqhSFItCGpwCCq7Sb9fWn79fiRyxI9F6JuJSTnTtHnjmZXfShAe5KRlSUktPwRcGg6LAMG9YTvd1JayNdjAGyPvZw7PGqeKF_syNgSrw-UzLsR0YXqck639affiVlKRTMNeZla2iXb8gRa8LGGiGoizKMrwV3Ywf3QilWy4CR5NK9TUj-OmdpaBfmE3T--LDpaOt7fjzhCFMXDGq27I_7NfzhrIJ_LpS7f2R5dG6eVRIgmSOVKEkCVM0n38lJ0H1E2uwwYmhns5wzDWJeBVEem8ycFrQkEvsGHWJ1Ru9YYNXhfr9Q"}

Some files were not shown because too many files have changed in this diff Show More