Compare commits
164 Commits
v1.0.0-rc.
...
main
Author | SHA1 | Date |
---|---|---|
|
6063ebe30f | |
|
f457e85917 | |
|
8c17f1cfc2 | |
|
2bc67e7695 | |
|
9d3ac1c22d | |
|
e1a37eb756 | |
|
626ac1d9c0 | |
|
de3655adc0 | |
|
02cc632b8b | |
|
287b8785c6 | |
|
81056bd695 | |
|
0caf40c9ae | |
|
dbbeca1679 | |
|
3bd0ac92b2 | |
|
02ff112615 | |
|
d6e291d617 | |
|
fd2a749d85 | |
|
fcc1ce32f8 | |
|
fdcf9cc476 | |
|
4b058c99ef | |
|
bb2ee7a8a7 | |
|
752832c674 | |
|
93b25a3e46 | |
|
c2b5474983 | |
|
6eb53a50d6 | |
|
96b7133718 | |
|
851cbabbc4 | |
|
b40566d885 | |
|
26ce0894a6 | |
|
e5eef5e899 | |
|
9fe530b5fd | |
|
3eeef95a40 | |
|
cefd007065 | |
|
d1d64e7041 | |
|
57c5e0dadf | |
|
95bac00829 | |
|
e99be1954a | |
|
240181a5eb | |
|
5a323330d0 | |
|
7b9636e239 | |
|
8797d86735 | |
|
cf70e77099 | |
|
0191e75373 | |
|
f332ed9212 | |
|
f855f25526 | |
|
c6c8cc7f66 | |
|
1dc55d0add | |
|
e1b80e2f2e | |
|
7d11fa2346 | |
|
82014a953f | |
|
11866a54a0 | |
|
4c2d986035 | |
|
a86f8da6ea | |
|
84c2ec0762 | |
|
9faa6e2747 | |
|
7c20eba012 | |
|
694e3a0314 | |
|
4d76f9a415 | |
|
115509e289 | |
|
4403efa431 | |
|
974c2916fa | |
|
8e2131d192 | |
|
aaadf0b342 | |
|
3c5a659c1d | |
|
c3b2f51140 | |
|
09e32d7940 | |
|
b3b8cbe0cc | |
|
8340920475 | |
|
8ada12a746 | |
|
b52583166f | |
|
1c3e3783d5 | |
|
c2cd70f095 | |
|
54b73d8a69 | |
|
a1825db13d | |
|
a6b0a8c2e5 | |
|
aba7ba74b2 | |
|
1a5b3e354f | |
|
254dfcde66 | |
|
b7fde5134d | |
|
b8508d04e9 | |
|
5e98995bd1 | |
|
378ee8371c | |
|
a901939275 | |
|
97a5a86d56 | |
|
442ece7b0d | |
|
2d65f6e3cb | |
|
fbf15e6c8c | |
|
57ff8e68a0 | |
|
b8136e2c80 | |
|
e686d8b995 | |
|
85df759836 | |
|
d9a44b5901 | |
|
ec42378613 | |
|
7fa8404e79 | |
|
2efb4a76bb | |
|
a86ca0d20c | |
|
345951e59c | |
|
4f3cb65cc0 | |
|
9ff189134f | |
|
4606472ebd | |
|
1752918878 | |
|
b7cd8a01fc | |
|
690448ee67 | |
|
d52ca7162a | |
|
66da1e313c | |
|
e56ee18161 | |
|
b315de42f9 | |
|
706eab815e | |
|
85a5bb9826 | |
|
966c6b7e42 | |
|
e8c8d224b2 | |
|
5de0d58b21 | |
|
ce0c457700 | |
|
1bc5a3f8c4 | |
|
765d02b5be | |
|
cb6f009f97 | |
|
36a0831e46 | |
|
effa7cb950 | |
|
18b3c680b4 | |
|
f89ec21bc8 | |
|
0ff7d26fb9 | |
|
60d9cdcc59 | |
|
c6bc5e0d38 | |
|
b684acb231 | |
|
f1706c2868 | |
|
553b866ed4 | |
|
f2cdfee211 | |
|
99bc2bc420 | |
|
3981f69fb5 | |
|
052e405b51 | |
|
abaaa0bc7c | |
|
757d01c8bb | |
|
2882bafab6 | |
|
432c931622 | |
|
983e97dc54 | |
|
a973c8b50a | |
|
d65bba32ae | |
|
6df5e38e05 | |
|
fa4ddc8f86 | |
|
69cf11bbbc | |
|
9b0e472c02 | |
|
eba60f5aed | |
|
c3f8c33305 | |
|
39c8ed050a | |
|
7de640bd5a | |
|
c8c1fbddec | |
|
15c8ff82d5 | |
|
1b9b7f2d80 | |
|
4e790cea6b | |
|
efa353a55e | |
|
f9c78f5340 | |
|
06d04cc0b2 | |
|
3dd11cbc7e | |
|
67a477f0c6 | |
|
cd1a135381 | |
|
5cf34ace71 | |
|
dfb41c5a7c | |
|
753b6b12cb | |
|
fb79e6df1a | |
|
7301607dce | |
|
1200ba5fb4 | |
|
9920fb7393 | |
|
8c3ed9217d | |
|
d872396066 |
|
@ -1,5 +1,21 @@
|
|||
# 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%
|
|
@ -0,0 +1,60 @@
|
|||
# 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.
|
|
@ -0,0 +1,18 @@
|
|||
# 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"
|
|
@ -0,0 +1,53 @@
|
|||
# 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.
|
|
@ -1,3 +1,16 @@
|
|||
# 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"
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
# 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
|
|
@ -1,3 +1,16 @@
|
|||
# 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:
|
||||
|
|
|
@ -1,11 +1,30 @@
|
|||
# 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
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
pull_request:
|
||||
branches: main
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: notaryproject/notation-core-go/.github/workflows/reusable-build.yml@main
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
|
|
@ -1,10 +1,27 @@
|
|||
# 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
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
pull_request:
|
||||
branches: main
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
schedule:
|
||||
- cron: '29 2 * * 5'
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# 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
|
|
@ -0,0 +1,33 @@
|
|||
# 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
|
|
@ -1,3 +1,16 @@
|
|||
# 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
|
||||
|
@ -5,4 +18,7 @@
|
|||
*.sublime-workspace
|
||||
|
||||
# Custom
|
||||
coverage.txt
|
||||
coverage.txt
|
||||
|
||||
# tmp directory was generated by example_remoteVerify_test.go
|
||||
tmp/
|
|
@ -1 +1,3 @@
|
|||
* @notaryproject/notation-maintainers
|
||||
# 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
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# 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)
|
15
Makefile
15
Makefile
|
@ -1,3 +1,16 @@
|
|||
# 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}'
|
||||
|
@ -16,7 +29,7 @@ clean:
|
|||
.PHONY: check-line-endings
|
||||
check-line-endings: ## check line endings
|
||||
! find . -name "*.go" -type f -exec file "{}" ";" | grep CRLF
|
||||
! find scripts -name "*.sh" -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
|
||||
|
|
24
README.md
24
README.md
|
@ -1,20 +1,28 @@
|
|||
# Notation
|
||||
# notation-go
|
||||
|
||||
[](https://github.com/notaryproject/notation-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain)
|
||||
[](https://codecov.io/gh/notaryproject/notation-go)
|
||||
[](https://codecov.io/gh/notaryproject/notation-go)
|
||||
[](https://pkg.go.dev/github.com/notaryproject/notation-go@main)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/notaryproject/notation-go)
|
||||
|
||||
A collection of libraries for supporting Notation sign, verify, push, pull of oci artifacts. Based on Notary V2 standard.
|
||||
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/).
|
||||
|
||||
## Table of Contents
|
||||
- [Core Documents](#core-documents)
|
||||
|
||||
- [Documentation](#documentation)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [License](#license)
|
||||
|
||||
## Documentation
|
||||
|
||||
## 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)
|
||||
Library documentation is available at [Go Reference](https://pkg.go.dev/github.com/notaryproject/notation-go).
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# 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.
|
|
@ -1,7 +1,22 @@
|
|||
// 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"
|
||||
|
||||
|
@ -28,7 +43,22 @@ func save(filePath string, cfg interface{}) error {
|
|||
|
||||
// load reads file, parses json and stores in cfg struct
|
||||
func load(filePath string, cfg interface{}) error {
|
||||
file, err := dir.ConfigFS().Open(filePath)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// 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
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -38,3 +51,15 @@ func TestSaveFile(t *testing.T) {
|
|||
t.Fatal("save config file failed.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNonExistedConfig(t *testing.T) {
|
||||
dir.UserConfigDir = "./testdata/non-existed"
|
||||
got, err := LoadConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error. err = %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, NewConfig()) {
|
||||
t.Errorf("loadFile() = %v, want %v", got, NewConfig())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// 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"
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
// 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")
|
||||
}
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -37,9 +50,6 @@ type KeySuite struct {
|
|||
*ExternalKey
|
||||
}
|
||||
|
||||
var errorKeyNameEmpty = errors.New("key name cannot be empty")
|
||||
var errKeyNotFound = errors.New("signing key not found")
|
||||
|
||||
// SigningKeys reflects the signingkeys.json file.
|
||||
type SigningKeys struct {
|
||||
Default *string `json:"default,omitempty"`
|
||||
|
@ -54,13 +64,12 @@ func NewSigningKeys() *SigningKeys {
|
|||
// Add adds new signing key
|
||||
func (s *SigningKeys) Add(name, keyPath, certPath string, markDefault bool) error {
|
||||
if name == "" {
|
||||
return errorKeyNameEmpty
|
||||
return ErrKeyNameEmpty
|
||||
}
|
||||
_, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ks := KeySuite{
|
||||
Name: name,
|
||||
X509KeyPair: &X509KeyPair{
|
||||
|
@ -75,25 +84,20 @@ func (s *SigningKeys) Add(name, keyPath, certPath string, markDefault bool) erro
|
|||
func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName string, pluginConfig map[string]string, markDefault bool) error {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debugf("Adding key with name %v and plugin name %v", keyName, pluginName)
|
||||
|
||||
if keyName == "" {
|
||||
return errorKeyNameEmpty
|
||||
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{
|
||||
|
@ -102,7 +106,6 @@ func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName str
|
|||
PluginConfig: pluginConfig,
|
||||
},
|
||||
}
|
||||
|
||||
if err = s.add(ks, markDefault); err != nil {
|
||||
logger.Error("Failed to add key with error: %v", err)
|
||||
return err
|
||||
|
@ -114,14 +117,12 @@ func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName str
|
|||
// Get returns signing key for the given name
|
||||
func (s *SigningKeys) Get(keyName string) (KeySuite, error) {
|
||||
if keyName == "" {
|
||||
return KeySuite{}, errorKeyNameEmpty
|
||||
return KeySuite{}, ErrKeyNameEmpty
|
||||
}
|
||||
|
||||
idx := slices.IndexIsser(s.Keys, keyName)
|
||||
if idx < 0 {
|
||||
return KeySuite{}, errKeyNotFound
|
||||
return KeySuite{}, KeyNotFoundError{KeyName: keyName}
|
||||
}
|
||||
|
||||
return s.Keys[idx], nil
|
||||
}
|
||||
|
||||
|
@ -129,9 +130,8 @@ func (s *SigningKeys) Get(keyName string) (KeySuite, error) {
|
|||
func (s *SigningKeys) GetDefault() (KeySuite, error) {
|
||||
if s.Default == nil {
|
||||
return KeySuite{}, errors.New("default signing key not set." +
|
||||
" Please set default singing key or specify a key name")
|
||||
" Please set default signing key or specify a key name")
|
||||
}
|
||||
|
||||
return s.Get(*s.Default)
|
||||
}
|
||||
|
||||
|
@ -140,12 +140,11 @@ func (s *SigningKeys) Remove(keyName ...string) ([]string, error) {
|
|||
var deletedNames []string
|
||||
for _, name := range keyName {
|
||||
if name == "" {
|
||||
return deletedNames, errorKeyNameEmpty
|
||||
return deletedNames, ErrKeyNameEmpty
|
||||
}
|
||||
|
||||
idx := slices.IndexIsser(s.Keys, name)
|
||||
if idx < 0 {
|
||||
return deletedNames, errors.New(name + ": not found")
|
||||
return deletedNames, KeyNotFoundError{KeyName: name}
|
||||
}
|
||||
s.Keys = slices.Delete(s.Keys, idx)
|
||||
deletedNames = append(deletedNames, name)
|
||||
|
@ -159,13 +158,11 @@ func (s *SigningKeys) Remove(keyName ...string) ([]string, error) {
|
|||
// UpdateDefault updates default signing key
|
||||
func (s *SigningKeys) UpdateDefault(keyName string) error {
|
||||
if keyName == "" {
|
||||
return errorKeyNameEmpty
|
||||
return ErrKeyNameEmpty
|
||||
}
|
||||
|
||||
if !slices.ContainsIsser(s.Keys, keyName) {
|
||||
return fmt.Errorf("key with name '%s' not found", keyName)
|
||||
return KeyNotFoundError{KeyName: keyName}
|
||||
}
|
||||
|
||||
s.Default = &keyName
|
||||
return nil
|
||||
}
|
||||
|
@ -176,11 +173,9 @@ func (s *SigningKeys) Save() error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateKeys(s); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return save(path, s)
|
||||
}
|
||||
|
||||
|
@ -195,11 +190,9 @@ func LoadSigningKeys() (*SigningKeys, error) {
|
|||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := validateKeys(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
|
@ -211,11 +204,9 @@ func LoadExecSaveSigningKeys(fn func(keys *SigningKeys) error) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fn(signingKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return signingKeys.Save()
|
||||
}
|
||||
|
||||
|
@ -228,12 +219,10 @@ 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
|
||||
}
|
||||
|
||||
|
@ -249,17 +238,14 @@ func validateKeys(config *SigningKeys) error {
|
|||
}
|
||||
uniqueKeyNames.Add(key.Name)
|
||||
}
|
||||
|
||||
if config.Default != nil {
|
||||
defaultKey := *config.Default
|
||||
if len(defaultKey) == 0 {
|
||||
return fmt.Errorf("malformed %s: default key name cannot be empty", dir.PathSigningKeys)
|
||||
}
|
||||
|
||||
if !uniqueKeyNames.Contains(defaultKey) {
|
||||
return fmt.Errorf("malformed %s: default key '%s' not found", dir.PathSigningKeys, defaultKey)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,9 +1,23 @@
|
|||
// 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"
|
||||
|
@ -54,11 +68,11 @@ func TestLoadSigningKeysInfo(t *testing.T) {
|
|||
}
|
||||
|
||||
if !reflect.DeepEqual(sampleSigningKeysInfo.Default, got.Default) {
|
||||
t.Fatal("singingKeysInfo test failed.")
|
||||
t.Fatal("signingKeysInfo test failed.")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(sampleSigningKeysInfo.Keys, got.Keys) {
|
||||
t.Fatal("singingKeysInfo test failed.")
|
||||
t.Fatal("signingKeysInfo test failed.")
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -297,14 +311,22 @@ func TestGet(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("NonExistent", func(t *testing.T) {
|
||||
if _, err := sampleSigningKeysInfo.Get("nonExistent"); err == nil {
|
||||
_, 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("InvalidName", func(t *testing.T) {
|
||||
if _, err := sampleSigningKeysInfo.Get(""); err == nil {
|
||||
t.Error("expected Get() to fail for invalid key name")
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -345,14 +367,22 @@ func TestUpdateDefault(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("NonExistent", func(t *testing.T) {
|
||||
if err := sampleSigningKeysInfo.UpdateDefault("nonExistent"); err == nil {
|
||||
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("InvalidName", func(t *testing.T) {
|
||||
if err := sampleSigningKeysInfo.UpdateDefault(""); err == nil {
|
||||
t.Error("expected Get() to fail for invalid key name")
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -369,21 +399,28 @@ func TestRemove(t *testing.T) {
|
|||
if _, err := testSigningKeysInfo.Get(testKeyName); err == nil {
|
||||
t.Error("Delete() filed to delete key")
|
||||
}
|
||||
|
||||
if keys[0] != testKeyName {
|
||||
t.Error("Delete() deleted key name mismatch")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NonExistent", func(t *testing.T) {
|
||||
if _, err := testSigningKeysInfo.Remove(testKeyName); err == nil {
|
||||
_, 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("InvalidName", func(t *testing.T) {
|
||||
if _, err := testSigningKeysInfo.Remove(""); err == nil {
|
||||
t.Error("expected Get() to fail for invalid key name")
|
||||
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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
24
dir/fs.go
24
dir/fs.go
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -38,10 +51,17 @@ func NewSysFS(root string) SysFS {
|
|||
|
||||
// ConfigFS is the config SysFS
|
||||
func ConfigFS() SysFS {
|
||||
return NewSysFS(UserConfigDir)
|
||||
return NewSysFS(userConfigDirPath())
|
||||
}
|
||||
|
||||
// PluginFS is the plugin SysFS
|
||||
func PluginFS() SysFS {
|
||||
return NewSysFS(filepath.Join(UserLibexecDir, PathPlugins))
|
||||
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())
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -6,7 +19,7 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func Test_sysFS_SysPath(t *testing.T) {
|
||||
func TestSysFS_SysPath(t *testing.T) {
|
||||
wantPath := filepath.FromSlash("/path/notation/config.json")
|
||||
fsys := NewSysFS("/path/notation")
|
||||
path, err := fsys.SysPath(PathConfigFile)
|
||||
|
@ -18,7 +31,7 @@ func Test_sysFS_SysPath(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_OsFs(t *testing.T) {
|
||||
func TestOsFs(t *testing.T) {
|
||||
wantData := []byte("data")
|
||||
fsys := NewSysFS("./testdata")
|
||||
|
||||
|
@ -36,3 +49,36 @@ func Test_OsFs(t *testing.T) {
|
|||
t.Fatalf("SysFS read failed. got data = %v, want %v", data, wantData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigFS(t *testing.T) {
|
||||
configFS := ConfigFS()
|
||||
path, err := configFS.SysPath(PathConfigFile)
|
||||
if err != nil {
|
||||
t.Fatalf("SysPath() failed. err = %v", err)
|
||||
}
|
||||
if path != filepath.Join(UserConfigDir, PathConfigFile) {
|
||||
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(UserConfigDir, PathConfigFile))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginFS(t *testing.T) {
|
||||
pluginFS := PluginFS()
|
||||
path, err := pluginFS.SysPath("plugin")
|
||||
if err != nil {
|
||||
t.Fatalf("SysPath() failed. err = %v", err)
|
||||
}
|
||||
if path != filepath.Join(userLibexecDirPath(), PathPlugins, "plugin") {
|
||||
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(userLibexecDirPath(), PathPlugins, "plugin"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRLFileCacheFS(t *testing.T) {
|
||||
cacheFS := CacheFS()
|
||||
path, err := cacheFS.SysPath(PathCRLCache)
|
||||
if err != nil {
|
||||
t.Fatalf("SysPath() failed. err = %v", err)
|
||||
}
|
||||
if path != filepath.Join(UserCacheDir, PathCRLCache) {
|
||||
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, UserConfigDir)
|
||||
}
|
||||
}
|
||||
|
|
98
dir/path.go
98
dir/path.go
|
@ -1,5 +1,18 @@
|
|||
// package dir implements Notation directory structure.
|
||||
// [directory spec]: https://github.com/notaryproject/notation/blob/main/specs/directory.md
|
||||
// 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:
|
||||
//
|
||||
|
@ -13,12 +26,12 @@
|
|||
// file, err := dir.ConfigFS().Open(dir.PathTrustPolicy)
|
||||
//
|
||||
// - Get the path of trustpolicy.json:
|
||||
// path, err := dir.ConfigFS().SysPath(dir.trustpolicy)
|
||||
// path, err := dir.ConfigFS().SysPath(dir.PathTrustPolicy)
|
||||
//
|
||||
// - Set custom configurations directory:
|
||||
// dir.UserConfigDir = '/path/to/configurations/'
|
||||
//
|
||||
// Only user level directory is supported for RC.1, and system level directory
|
||||
// Only user level directory is supported, and system level directory
|
||||
// may be added later.
|
||||
package dir
|
||||
|
||||
|
@ -31,6 +44,7 @@ import (
|
|||
var (
|
||||
UserConfigDir string // Absolute path of user level {NOTATION_CONFIG}
|
||||
UserLibexecDir string // Absolute path of user level {NOTATION_LIBEXEC}
|
||||
UserCacheDir string // Absolute path of user level {NOTATION_CACHE}
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -44,10 +58,15 @@ const (
|
|||
PathConfigFile = "config.json"
|
||||
// PathSigningKeys is the signingkeys file relative path.
|
||||
PathSigningKeys = "signingkeys.json"
|
||||
// PathTrustPolicy is the trust policy file relative path.
|
||||
// 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"
|
||||
// PathPlugins is the plugins directory relative path.
|
||||
PathPlugins = "plugins"
|
||||
// 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.
|
||||
|
@ -58,23 +77,62 @@ const (
|
|||
TrustStoreDir = "truststore"
|
||||
)
|
||||
|
||||
var userConfigDir = os.UserConfigDir // for unit test
|
||||
// The relative path to {NOTATION_LIBEXEC}
|
||||
const (
|
||||
// PathPlugins is the plugins directory relative path.
|
||||
PathPlugins = "plugins"
|
||||
)
|
||||
|
||||
func init() {
|
||||
loadUserPath()
|
||||
// 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
|
||||
}
|
||||
|
||||
// loadUserPath function defines UserConfigDir and UserLibexecDir.
|
||||
func loadUserPath() {
|
||||
// set user config
|
||||
userDir, err := userConfigDir()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
// userLibexecDirPath returns the user level {NOTATION_LIBEXEC} path.
|
||||
func userLibexecDirPath() string {
|
||||
if UserLibexecDir == "" {
|
||||
// set user libexec
|
||||
UserLibexecDir = userConfigDirPath()
|
||||
}
|
||||
UserConfigDir = filepath.Join(userDir, notation)
|
||||
return UserLibexecDir
|
||||
}
|
||||
|
||||
// set user libexec
|
||||
UserLibexecDir = UserConfigDir
|
||||
// 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.
|
||||
|
@ -87,7 +145,7 @@ func LocalKeyPath(name string) (keyPath, certPath string) {
|
|||
//
|
||||
// items includes named-store and cert-file names.
|
||||
// the directory follows the pattern of
|
||||
// {NOTATION_CONFIG}/truststore/x509/{named-store}/{cert-file}
|
||||
// {NOTATION_CONFIG}/truststore/x509/{store-type}/{named-store}/{cert-file}
|
||||
func X509TrustStoreDir(items ...string) string {
|
||||
pathItems := []string{TrustStoreDir, "x509"}
|
||||
pathItems = append(pathItems, items...)
|
||||
|
|
|
@ -1,30 +1,83 @@
|
|||
// 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 mockGetUserConfig() (string, error) {
|
||||
func mockUserPath() (string, error) {
|
||||
return "/path/", nil
|
||||
}
|
||||
|
||||
func Test_loadPath(t *testing.T) {
|
||||
wantDir := filepath.FromSlash("/path/notation")
|
||||
userConfigDir = mockGetUserConfig
|
||||
loadUserPath()
|
||||
if UserConfigDir != wantDir {
|
||||
t.Fatalf(`loadPath() UserConfigDir is incorrect. got: %q, want: %q`, UserConfigDir, wantDir)
|
||||
}
|
||||
func setup() {
|
||||
UserConfigDir = ""
|
||||
UserLibexecDir = ""
|
||||
UserCacheDir = ""
|
||||
}
|
||||
|
||||
if UserLibexecDir != UserConfigDir {
|
||||
t.Fatalf(`loadPath() UserLibexecDir is incorrect. got: %q, want: %q`, UserLibexecDir, wantDir)
|
||||
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 = mockGetUserConfig
|
||||
loadUserPath()
|
||||
userConfigDir = mockUserPath
|
||||
setup()
|
||||
_ = userConfigDirPath()
|
||||
_ = userLibexecDirPath()
|
||||
gotKeyPath, gotCertPath := LocalKeyPath("web")
|
||||
if gotKeyPath != "localkeys/web.key" {
|
||||
t.Fatalf(`LocalKeyPath() gotKeyPath = %q, want "localkeys/web.key"`, gotKeyPath)
|
||||
|
@ -35,8 +88,10 @@ func TestLocalKeyPath(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestX509TrustStoreDir(t *testing.T) {
|
||||
userConfigDir = mockGetUserConfig
|
||||
loadUserPath()
|
||||
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)
|
||||
}
|
||||
|
|
73
errors.go
73
errors.go
|
@ -1,12 +1,31 @@
|
|||
// 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.
|
||||
type ErrorPushSignatureFailed struct {
|
||||
//
|
||||
// Deprecated: Use PushSignatureFailedError instead.
|
||||
type ErrorPushSignatureFailed = PushSignatureFailedError
|
||||
|
||||
// PushSignatureFailedError is used when failed to push signature to the
|
||||
// target registry.
|
||||
type PushSignatureFailedError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e ErrorPushSignatureFailed) Error() string {
|
||||
func (e PushSignatureFailedError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return "failed to push signature to registry with error: " + e.Msg
|
||||
}
|
||||
|
@ -15,11 +34,17 @@ func (e ErrorPushSignatureFailed) Error() string {
|
|||
|
||||
// ErrorVerificationInconclusive is used when signature verification fails due
|
||||
// to a runtime error (e.g. a network error)
|
||||
type ErrorVerificationInconclusive struct {
|
||||
//
|
||||
// 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 ErrorVerificationInconclusive) Error() string {
|
||||
func (e VerificationInconclusiveError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
|
@ -28,11 +53,17 @@ func (e ErrorVerificationInconclusive) Error() string {
|
|||
|
||||
// ErrorNoApplicableTrustPolicy is used when there is no trust policy that
|
||||
// applies to the given artifact
|
||||
type ErrorNoApplicableTrustPolicy struct {
|
||||
//
|
||||
// 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 ErrorNoApplicableTrustPolicy) Error() string {
|
||||
func (e NoApplicableTrustPolicyError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
|
@ -41,11 +72,17 @@ func (e ErrorNoApplicableTrustPolicy) Error() string {
|
|||
|
||||
// ErrorSignatureRetrievalFailed is used when notation is unable to retrieve the
|
||||
// digital signature/s for the given artifact
|
||||
type ErrorSignatureRetrievalFailed struct {
|
||||
//
|
||||
// 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 ErrorSignatureRetrievalFailed) Error() string {
|
||||
func (e SignatureRetrievalFailedError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
|
@ -54,11 +91,17 @@ func (e ErrorSignatureRetrievalFailed) Error() string {
|
|||
|
||||
// ErrorVerificationFailed is used when it is determined that the digital
|
||||
// signature/s is not valid for the given artifact
|
||||
type ErrorVerificationFailed struct {
|
||||
//
|
||||
// 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 ErrorVerificationFailed) Error() string {
|
||||
func (e VerificationFailedError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
|
@ -67,11 +110,17 @@ func (e ErrorVerificationFailed) Error() string {
|
|||
|
||||
// ErrorUserMetadataVerificationFailed is used when the signature does not
|
||||
// contain the user specified metadata
|
||||
type ErrorUserMetadataVerificationFailed struct {
|
||||
//
|
||||
// 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 ErrorUserMetadataVerificationFailed) Error() string {
|
||||
func (e UserMetadataVerificationFailedError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -14,7 +27,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
// exampleDesc is an example of the target OCI artifact manifest descriptor.
|
||||
// exampleDesc is an example of the target manifest descriptor.
|
||||
exampleDesc = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||
Digest: "sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c",
|
||||
|
@ -33,9 +46,9 @@ func Example_localSign() {
|
|||
// exampleSigner is a notation.Signer given key and X509 certificate chain.
|
||||
// Users should replace `exampleCertTuple.PrivateKey` with their own private
|
||||
// key and replace `exampleCerts` with the corresponding full certificate
|
||||
// chain, following the Notary certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
|
||||
exampleSigner, err := signer.New(exampleCertTuple.PrivateKey, exampleCerts)
|
||||
// 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
|
||||
}
|
||||
|
@ -44,8 +57,8 @@ func Example_localSign() {
|
|||
// signature mediaTypes are supported.
|
||||
exampleSignatureMediaType := cose.MediaTypeEnvelope
|
||||
|
||||
// exampleSignOptions is an example of notation.SignOptions.
|
||||
exampleSignOptions := notation.SignOptions{
|
||||
// exampleSignOptions is an example of notation.SignerSignOptions.
|
||||
exampleSignOptions := notation.SignerSignOptions{
|
||||
SignatureMediaType: exampleSignatureMediaType,
|
||||
SigningAgent: "example signing agent",
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -18,14 +31,14 @@ import (
|
|||
|
||||
// examplePolicyDocument is an example of a valid trust policy document.
|
||||
// trust policy document should follow this spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-policy
|
||||
var examplePolicyDocument = trustpolicy.Document{
|
||||
// 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.TrustPolicy{
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"example/software"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelStrict.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{"*"},
|
||||
},
|
||||
|
@ -43,8 +56,7 @@ func Example_localVerify() {
|
|||
// signature mediaTypes are supported.
|
||||
exampleSignatureMediaType := cose.MediaTypeEnvelope
|
||||
|
||||
// exampleTargetDescriptor is an example of the target OCI artifact manifest
|
||||
// descriptor.
|
||||
// exampleTargetDescriptor is an example of the target manifest descriptor.
|
||||
exampleTargetDescriptor := ocispec.Descriptor{
|
||||
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||
Digest: "sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c",
|
||||
|
@ -54,16 +66,16 @@ func Example_localVerify() {
|
|||
// exampleSignatureEnvelope is a valid signature envelope.
|
||||
exampleSignatureEnvelope := generateExampleSignatureEnvelope()
|
||||
|
||||
// exampleVerifyOptions is an example of notation.VerifyOptions
|
||||
exampleVerifyOptions := notation.VerifyOptions{
|
||||
// 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 and add trusted
|
||||
// certificates into it following the trust store spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-store
|
||||
// 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
|
||||
}
|
||||
|
@ -160,8 +172,8 @@ func createTrustStore() error {
|
|||
// generate the `exampleSignatureEnvelopePem` above.)
|
||||
// Users should replace `exampleX509Certificate` with their own trusted
|
||||
// certificate and add to the trust store, following the
|
||||
// Notary certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -5,12 +18,13 @@ import (
|
|||
"crypto/x509"
|
||||
"fmt"
|
||||
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature/cose"
|
||||
"github.com/notaryproject/notation-core-go/testhelper"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/signer"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
)
|
||||
|
||||
// Both COSE ("application/cose") and JWS ("application/jose+json")
|
||||
|
@ -31,9 +45,9 @@ func Example_remoteSign() {
|
|||
// exampleSigner is a notation.Signer given key and X509 certificate chain.
|
||||
// Users should replace `exampleCertTuple.PrivateKey` with their own private
|
||||
// key and replace `exampleCerts` with the corresponding full certificate
|
||||
// chain, following the Notary certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
|
||||
exampleSigner, err := signer.New(exampleCertTuple.PrivateKey, exampleCerts)
|
||||
// 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
|
||||
}
|
||||
|
@ -46,23 +60,26 @@ func Example_remoteSign() {
|
|||
exampleRepo := registry.NewRepository(remoteRepo)
|
||||
|
||||
// exampleSignOptions is an example of notation.SignOptions.
|
||||
exampleSignOptions := notation.RemoteSignOptions{
|
||||
SignOptions: notation.SignOptions{
|
||||
ArtifactReference: exampleArtifactReference,
|
||||
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.
|
||||
targetDesc, err := notation.Sign(context.Background(), exampleSigner, exampleRepo, exampleSignOptions)
|
||||
targetManifestDesc, sigManifestDesc, err := notation.SignOCI(context.Background(), exampleSigner, exampleRepo, exampleSignOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully signed")
|
||||
fmt.Println("targetDesc MediaType:", targetDesc.MediaType)
|
||||
fmt.Println("targetDesc Digest:", targetDesc.Digest)
|
||||
fmt.Println("targetDesc Size:", targetDesc.Size)
|
||||
fmt.Println("targetManifestDesc.MediaType:", targetManifestDesc.MediaType)
|
||||
fmt.Println("targetManifestDesc.Digest:", targetManifestDesc.Digest)
|
||||
fmt.Println("targetManifestDesc.Size:", targetManifestDesc.Size)
|
||||
fmt.Println("sigManifestDesc.MediaType:", sigManifestDesc.MediaType)
|
||||
fmt.Println("sigManifestDesc.Digest:", sigManifestDesc.Digest)
|
||||
fmt.Println("sigManifestDesc.Size:", sigManifestDesc.Size)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -24,10 +37,10 @@ func Example_remoteVerify() {
|
|||
|
||||
// examplePolicyDocument is an example of a valid trust policy document.
|
||||
// trust policy document should follow this spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-policy
|
||||
examplePolicyDocument := trustpolicy.Document{
|
||||
// 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.TrustPolicy{
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
|
@ -39,9 +52,9 @@ func Example_remoteVerify() {
|
|||
}
|
||||
|
||||
// generateTrustStore generates a trust store directory for demo purpose.
|
||||
// Users could use the default trust store from Notary and add trusted
|
||||
// certificates into it following the trust store spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-store
|
||||
// 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
|
||||
}
|
||||
|
@ -60,16 +73,16 @@ func Example_remoteVerify() {
|
|||
}
|
||||
exampleRepo := registry.NewRepository(remoteRepo)
|
||||
|
||||
// exampleRemoteVerifyOptions is an example of notation.RemoteVerifyOptions.
|
||||
exampleRemoteVerifyOptions := notation.RemoteVerifyOptions{
|
||||
// exampleVerifyOptions is an example of notation.VerifyOptions.
|
||||
exampleVerifyOptions := notation.VerifyOptions{
|
||||
ArtifactReference: exampleArtifactReference,
|
||||
MaxSignatureAttempts: 50,
|
||||
}
|
||||
|
||||
// remote verify core process
|
||||
// upon successful verification, the target OCI artifact manifest descriptor
|
||||
// upon successful verification, the target manifest descriptor
|
||||
// and signature verification outcome are returned.
|
||||
targetDesc, _, err := notation.Verify(context.Background(), exampleVerifier, exampleRepo, exampleRemoteVerifyOptions)
|
||||
targetDesc, _, err := notation.Verify(context.Background(), exampleVerifier, exampleRepo, exampleVerifyOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
@ -88,8 +101,8 @@ func generateTrustStore() error {
|
|||
// an example of a valid X509 self-signed certificate for demo purpose ONLY.
|
||||
// Users should replace `exampleX509Certificate` with their own trusted
|
||||
// certificate and add to the trust store, following the
|
||||
// Notary certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
// 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
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
// 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)
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
// 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"}`
|
||||
}
|
|
@ -0,0 +1,192 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "github.com/notaryproject/notation-core-go/signature/cose"
|
||||
_ "github.com/notaryproject/notation-core-go/signature/jws"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/verifier"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
)
|
||||
|
||||
// Example_verifyWithTimestamp demonstrates how to use notation.Verify to verify
|
||||
// signature of an artifact including RFC 3161 compliant timestamp countersignature
|
||||
func Example_verifyWithTimestamp() {
|
||||
// exampleArtifactReference is an example of the target artifact reference
|
||||
exampleArtifactReference := "localhost:5000/software@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e"
|
||||
|
||||
// examplePolicyDocument is an example of a valid trust policy document with
|
||||
// timestamping configurations.
|
||||
// trust policy document should follow this spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy
|
||||
examplePolicyDocument := trustpolicy.OCIDocument{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{
|
||||
VerificationLevel: trustpolicy.LevelStrict.Name,
|
||||
|
||||
// verify timestamp countersignature only if the signing
|
||||
// certificate chain has expired.
|
||||
// Default: trustpolicy.OptionAlways
|
||||
VerifyTimestamp: trustpolicy.OptionAfterCertExpiry,
|
||||
},
|
||||
|
||||
// `tsa` trust store type MUST be configured to enable
|
||||
// timestamp verification
|
||||
TrustStores: []string{"ca:valid-trust-store", "tsa:valid-tsa"},
|
||||
|
||||
// TrustedIdentities only contains trusted identities of `ca`
|
||||
// and `signingAuthority`
|
||||
TrustedIdentities: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// generateTrustStoreWithTimestamp generates a trust store directory for demo purpose.
|
||||
// Users should configure their own trust store and add trusted certificates
|
||||
// into it following the trust store spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
|
||||
if err := generateTrustStoreWithTimestamp(); err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleVerifier is an example of notation.Verifier given
|
||||
// trust policy document and X509 trust store.
|
||||
exampleVerifier, err := verifier.New(&examplePolicyDocument, truststore.NewX509TrustStore(dir.ConfigFS()), nil)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleRepo is an example of registry.Repository.
|
||||
remoteRepo, err := remote.NewRepository(exampleArtifactReference)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
exampleRepo := registry.NewRepository(remoteRepo)
|
||||
|
||||
// exampleVerifyOptions is an example of notation.VerifyOptions.
|
||||
exampleVerifyOptions := notation.VerifyOptions{
|
||||
ArtifactReference: exampleArtifactReference,
|
||||
MaxSignatureAttempts: 50,
|
||||
}
|
||||
|
||||
// remote verify core process
|
||||
// upon successful verification, the target manifest descriptor
|
||||
// and signature verification outcome are returned.
|
||||
targetDesc, _, err := notation.Verify(context.Background(), exampleVerifier, exampleRepo, exampleVerifyOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully verified")
|
||||
fmt.Println("targetDesc MediaType:", targetDesc.MediaType)
|
||||
fmt.Println("targetDesc Digest:", targetDesc.Digest)
|
||||
fmt.Println("targetDesc Size:", targetDesc.Size)
|
||||
}
|
||||
|
||||
func generateTrustStoreWithTimestamp() error {
|
||||
// changing the path of the trust store for demo purpose.
|
||||
// Users could keep the default value, i.e. os.UserConfigDir.
|
||||
dir.UserConfigDir = "tmp"
|
||||
|
||||
// an example of a valid X509 self-signed certificate for demo purpose ONLY.
|
||||
// Users should replace `exampleX509Certificate` with their own trusted
|
||||
// certificate and add to the trust store, following the
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
|
||||
MA0GA1UEAxMGYWxwaW5lMCAXDTAwMDgyOTEzNTAwMFoYDzIxMjMwODI5MTM1MDAw
|
||||
WjBOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUx
|
||||
DzANBgNVBAoTBk5vdGFyeTEPMA0GA1UEAxMGYWxwaW5lMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEAocg3qEsyNDDLfB8OHD4dhi+M1NPK1Asy5NX84c+g
|
||||
vacZuoPLTwmpOfm6nPt7GPPB9G7S6xxhFNbRxTYfYUjK+kaCj38XjBRf5lGewbSJ
|
||||
KVkxQ82/axU70ceSW3JpazrageN9JUTZ/Jfi4MfnipITwcmMoiij8eGrHskjyVyZ
|
||||
bJd0WMMKRDWVhLPUiPMVWt/4d7YtZItzacaQKtXmXgsTCTWpIols3gftNYjrQoMs
|
||||
UelUdD8vOAWN9J28/SyC+uSh/K1KfyUlbqufn4di8DEBxntP5wnXYbJL1jtjsUgE
|
||||
xAVjQxT1zI59X36m3t3YKqCQh1cud02L5onObY6zj57N6QIDAQABoycwJTAOBgNV
|
||||
HQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQAD
|
||||
ggEBAC8AjBLy7EsRpi6oguCdFSb6nRGjvF17N+b6mDb3sARnB8T1pxvzTT26ya+A
|
||||
yWR+jjodEwbMIS+13lV+9qT2LwqlbOUNY519Pa2GRRY72JjeowWI3iKkKaMzfZUB
|
||||
7lRTGXdEuZApLbTO/3JVcR9ffu00N1UaAP9YGElSt4JDJYA9M+d/Qto+HiIsE0Kj
|
||||
+jdnwIYovPPOlryKOLfFb/r1GEq7n63xFZz83iyWNaZdsJ5N3YHxdOpkbBbCalOE
|
||||
BDJTjQKqeAYBLoANNU0OBslmqHCSBTEnhbqJHN6QKyF09ScOl5LwM1QsTl0UY5si
|
||||
GLAfj/jSf9OH9VLTPHOS8/N0Ka4=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// Adding the certificate into the trust store.
|
||||
if err := os.MkdirAll("tmp/truststore/x509/ca/valid-trust-store", 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile("tmp/truststore/x509/ca/valid-trust-store/NotationExample.pem", []byte(exampleX509Certificate), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// an example of a valid TSA root certificate for demo purpose ONLY.
|
||||
// Users should replace `exampleTSARootCertificate` with their own trusted
|
||||
// TSA root certificate and add to the trust store, following the
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleTSARootCertificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi
|
||||
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
|
||||
d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
|
||||
RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV
|
||||
UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
|
||||
Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG
|
||||
SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y
|
||||
ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If
|
||||
xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV
|
||||
ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO
|
||||
DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ
|
||||
jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/
|
||||
CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi
|
||||
EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM
|
||||
fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY
|
||||
uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK
|
||||
chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t
|
||||
9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
|
||||
hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD
|
||||
ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2
|
||||
SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd
|
||||
+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc
|
||||
fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa
|
||||
sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N
|
||||
cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N
|
||||
0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie
|
||||
4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI
|
||||
r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1
|
||||
/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm
|
||||
gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// Adding the tsa root certificate into the trust store.
|
||||
if err := os.MkdirAll("tmp/truststore/x509/tsa/valid-tsa", 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile("tmp/truststore/x509/tsa/valid-tsa/NotationTSAExample.pem", []byte(exampleTSARootCertificate), 0600)
|
||||
}
|
27
go.mod
27
go.mod
|
@ -1,23 +1,26 @@
|
|||
module github.com/notaryproject/notation-go
|
||||
|
||||
go 1.18
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/go-ldap/ldap/v3 v3.4.4
|
||||
github.com/notaryproject/notation-core-go v1.0.0-rc.2
|
||||
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/opencontainers/go-digest v1.0.0
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2
|
||||
github.com/veraison/go-cose v1.0.0
|
||||
golang.org/x/mod v0.8.0
|
||||
oras.land/oras-go/v2 v2.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.4.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 // 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/crypto v0.5.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
)
|
||||
|
|
87
go.sum
87
go.sum
|
@ -1,49 +1,60 @@
|
|||
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
|
||||
github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3 h1:Hxl6lhQFj4AnOX6MLrsCb/+7tCj7DxP7VA+2rDIq5AU=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.3/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/notaryproject/notation-core-go v1.0.0-rc.1 h1:ACi0gr6mD1bzp9+gu3P0meJ/N6iWHlyM9zgtdnooNAA=
|
||||
github.com/notaryproject/notation-core-go v1.0.0-rc.1/go.mod h1:n8Gbvl9sKa00KptkKEL5XKUyMTIALe74QipKauE2rj4=
|
||||
github.com/notaryproject/notation-core-go v1.0.0-rc.2 h1:nNJuXa12jVNSSETjGNJEcZgv1NwY5ToYPo+c0P9syCI=
|
||||
github.com/notaryproject/notation-core-go v1.0.0-rc.2/go.mod h1:ASoc9KbJkSHLbKhO96lb0pIEWJRMZq9oprwBSZ0EAx0=
|
||||
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/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.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
|
||||
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/veraison/go-cose v1.0.0 h1:Jxirc0rl3gG7wUFgW+82tBQNeK8T8e2Bk1Vd298ob4A=
|
||||
github.com/veraison/go-cose v1.0.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4=
|
||||
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.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
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.0.0 h1:+LRAz92WF7AvYQsQjPEAIw3Xb2zPPhuydjpi4pIHmc0=
|
||||
oras.land/oras-go/v2 v2.0.0/go.mod h1:iVExH1NxrccIxjsiq17L91WCZ4KIw6jVQyCLsZsu1gc=
|
||||
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=
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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.
|
||||
|
|
|
@ -1,14 +1,32 @@
|
|||
// 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"
|
||||
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 {
|
||||
|
@ -35,3 +53,16 @@ func SanitizeTargetArtifact(targetArtifact ocispec.Descriptor) ocispec.Descripto
|
|||
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
|
||||
}
|
||||
|
|
|
@ -1,8 +1,22 @@
|
|||
// 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"
|
||||
|
@ -85,6 +99,26 @@ func TestValidatePayloadContentType(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -1,8 +1,153 @@
|
|||
// 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 "regexp"
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,214 @@
|
|||
// 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")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
// 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
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 (
|
||||
|
@ -5,8 +18,7 @@ import (
|
|||
_ "embed"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-go/plugin"
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
@ -65,6 +77,7 @@ 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",
|
||||
|
@ -73,7 +86,7 @@ var (
|
|||
Annotations: Annotations,
|
||||
}
|
||||
SigManfiestDescriptor = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.cncf.oras.artifact.manifest.v1+json",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: SampleDigest,
|
||||
Size: 300,
|
||||
Annotations: Annotations,
|
||||
|
@ -110,6 +123,9 @@ type Repository struct {
|
|||
ListSignaturesError error
|
||||
FetchSignatureBlobResponse []byte
|
||||
FetchSignatureBlobError error
|
||||
MissMatchDigest bool
|
||||
ExceededNumOfSignatures bool
|
||||
PushSignatureError error
|
||||
}
|
||||
|
||||
func NewRepository() Repository {
|
||||
|
@ -121,10 +137,21 @@ func NewRepository() Repository {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -137,40 +164,44 @@ func (t Repository) FetchSignatureBlob(ctx context.Context, desc ocispec.Descrip
|
|||
}
|
||||
|
||||
func (t Repository) PushSignature(ctx context.Context, mediaType string, blob []byte, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) {
|
||||
if t.PushSignatureError != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, t.PushSignatureError
|
||||
}
|
||||
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, nil
|
||||
}
|
||||
|
||||
type PluginMock struct {
|
||||
Metadata proto.GetMetadataResponse
|
||||
Metadata plugin.GetMetadataResponse
|
||||
ExecuteResponse interface{}
|
||||
ExecuteError error
|
||||
}
|
||||
|
||||
func (p *PluginMock) GetMetadata(ctx context.Context, req *proto.GetMetadataRequest) (*proto.GetMetadataResponse, 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 *proto.VerifySignatureRequest) (*proto.VerifySignatureResponse, error) {
|
||||
if resp, ok := p.ExecuteResponse.(*proto.VerifySignatureResponse); ok {
|
||||
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 *proto.DescribeKeyRequest) (*proto.DescribeKeyResponse, error) {
|
||||
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 *proto.GenerateSignatureRequest) (*proto.GenerateSignatureResponse, error) {
|
||||
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 *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error) {
|
||||
func (p *PluginMock) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
type PluginManager struct {
|
||||
PluginCapabilities []proto.Capability
|
||||
PluginCapabilities []plugin.Capability
|
||||
GetPluginError error
|
||||
PluginRunnerLoadError error
|
||||
PluginRunnerExecuteResponse interface{}
|
||||
|
@ -179,7 +210,7 @@ type PluginManager struct {
|
|||
|
||||
func (pm PluginManager) Get(ctx context.Context, name string) (plugin.Plugin, error) {
|
||||
return &PluginMock{
|
||||
Metadata: proto.GetMetadataResponse{
|
||||
Metadata: plugin.GetMetadataResponse{
|
||||
Name: "plugin-name",
|
||||
Description: "for mocking in unit tests",
|
||||
Version: "1.0.0",
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
// 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
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pkix
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func FuzzParseDistinguishedName(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, name string) {
|
||||
_, _ = ParseDistinguishedName(name)
|
||||
})
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pkix
|
||||
|
||||
import (
|
||||
|
@ -7,17 +20,16 @@ import (
|
|||
ldapv3 "github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
// ParseDistinguishedName parses a DN name and validates Notary V2 rules
|
||||
// ParseDistinguishedName parses a DN name and validates Notary Project rules
|
||||
func ParseDistinguishedName(name string) (map[string]string, error) {
|
||||
if strings.Contains(name, "=#") {
|
||||
return nil, fmt.Errorf("unsupported distinguished name (DN) %q: notation does not support x509.subject identities containing \"=#\"", name)
|
||||
}
|
||||
|
||||
mandatoryFields := []string{"C", "ST", "O"}
|
||||
attrKeyValue := make(map[string]string)
|
||||
dn, err := ldapv3.ParseDN(name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing distinguished name (DN) %q failed with err: %v. A valid DN must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name, err)
|
||||
return nil, fmt.Errorf("parsing distinguished name (DN) %q failed with err: %v. A valid DN must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name, err)
|
||||
}
|
||||
|
||||
for _, rdn := range dn.RDNs {
|
||||
|
@ -26,6 +38,10 @@ func ParseDistinguishedName(name string) (map[string]string, error) {
|
|||
return nil, fmt.Errorf("distinguished name (DN) %q has multi-valued RDN attributes, remove multi-valued RDN attributes as they are not supported", name)
|
||||
}
|
||||
for _, attribute := range rdn.Attributes {
|
||||
// stateOrProvince name 'S' is an alias for 'ST'
|
||||
if attribute.Type == "S" {
|
||||
attribute.Type = "ST"
|
||||
}
|
||||
if attrKeyValue[attribute.Type] == "" {
|
||||
attrKeyValue[attribute.Type] = attribute.Value
|
||||
} else {
|
||||
|
@ -35,11 +51,13 @@ func ParseDistinguishedName(name string) (map[string]string, error) {
|
|||
}
|
||||
|
||||
// Verify mandatory fields are present
|
||||
mandatoryFields := []string{"C", "ST", "O"}
|
||||
for _, field := range mandatoryFields {
|
||||
if attrKeyValue[field] == "" {
|
||||
return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum", name, field)
|
||||
return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum", name, field)
|
||||
}
|
||||
}
|
||||
|
||||
// No errors
|
||||
return attrKeyValue, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pkix
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseDistinguishedName(t *testing.T) {
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid DN",
|
||||
input: "C=US,ST=California,O=Notary Project",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid DN with State alias",
|
||||
input: "C=US,S=California,O=Notary Project",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid DN",
|
||||
input: "C=US,ST=California",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid DN without State",
|
||||
input: "C=US,O=Notary Project",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid DN without State",
|
||||
input: "invalid",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "duplicate RDN attribute",
|
||||
input: "C=US,ST=California,O=Notary Project,S=California",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unsupported DN =#",
|
||||
input: "C=US,ST=California,O=Notary Project=#",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "multi-valued RDN attributes",
|
||||
input: "OU=Sales+CN=J. Smith,DC=example,DC=net",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := ParseDistinguishedName(tt.input)
|
||||
if tt.wantErr != (err != nil) {
|
||||
t.Errorf("ParseDistinguishedName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSubsetDN(t *testing.T) {
|
||||
// Test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
dn1 map[string]string
|
||||
dn2 map[string]string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "subset DN",
|
||||
dn1: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project",
|
||||
},
|
||||
dn2: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project",
|
||||
"L": "Los Angeles",
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "not subset DN",
|
||||
dn1: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project",
|
||||
},
|
||||
dn2: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project 2",
|
||||
"L": "Los Angeles",
|
||||
"CN": "Notary",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "not subset DN 2",
|
||||
dn1: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project",
|
||||
"CN": "Notary",
|
||||
},
|
||||
dn2: map[string]string{
|
||||
"C": "US",
|
||||
"ST": "California",
|
||||
"O": "Notary Project",
|
||||
"L": "Los Angeles",
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
// Run tests
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsSubsetDN(tt.dn1, tt.dn2); got != tt.want {
|
||||
t.Errorf("IsSubsetDN() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package semver provides functions related to semanic version.
|
||||
// This package is based on "golang.org/x/mod/semver"
|
||||
package semver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// semVerRegEx is taken from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
||||
var semVerRegEx = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
|
||||
|
||||
// IsValid returns true if version is a valid semantic version
|
||||
func IsValid(version string) bool {
|
||||
return semVerRegEx.MatchString(version)
|
||||
}
|
||||
|
||||
// ComparePluginVersion validates and compares two plugin semantic versions.
|
||||
//
|
||||
// The result will be 0 if v == w, -1 if v < w, or +1 if v > w.
|
||||
func ComparePluginVersion(v, w string) (int, error) {
|
||||
// sanity check
|
||||
if !IsValid(v) {
|
||||
return 0, fmt.Errorf("%s is not a valid semantic version", v)
|
||||
}
|
||||
if !IsValid(w) {
|
||||
return 0, fmt.Errorf("%s is not a valid semantic version", w)
|
||||
}
|
||||
|
||||
// golang.org/x/mod/semver requires semantic version strings must begin
|
||||
// with a leading "v". Adding prefix "v" to the inputs.
|
||||
// Reference: https://pkg.go.dev/golang.org/x/mod/semver#pkg-overview
|
||||
return semver.Compare("v"+v, "v"+w), nil
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package semver
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestComparePluginVersion(t *testing.T) {
|
||||
t.Run("compare with lower version", func(t *testing.T) {
|
||||
comp, err := ComparePluginVersion("1.0.0", "1.0.1")
|
||||
if err != nil || comp >= 0 {
|
||||
t.Fatal("expected nil err and negative comp")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("compare with equal version", func(t *testing.T) {
|
||||
comp, err := ComparePluginVersion("1.0.1", "1.0.1")
|
||||
if err != nil || comp != 0 {
|
||||
t.Fatal("expected nil err and comp equal to 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("failed due to invalid semantic version", func(t *testing.T) {
|
||||
expectedErrMsg := "v1.0.0 is not a valid semantic version"
|
||||
_, err := ComparePluginVersion("v1.0.0", "1.0.1")
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected err %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// 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 slices
|
||||
|
||||
// Contains reports whether v is present in s.
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6MTlkYmQyZTQ4ZTkyMTQyNmVlOGFjZTRkYzg5MmVkZmIyZWNkYzFkMWE3MmQ1NDE2YzgzNjcwYzMwYWNlY2VmMCIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5vY2kuaW1hZ2UubWFuaWZlc3QudjEranNvbiIsInNpemUiOjQ4MX19","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDIzLTAzLTE0VDE2OjEwOjAyKzA4OjAwIn0","header":{"x5c":["MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEPMA0GA1UEAxMGYWxwaW5lMCAXDTAwMDgyOTEzNTAwMFoYDzIxMjMwODI5MTM1MDAwWjBOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEPMA0GA1UEAxMGYWxwaW5lMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAocg3qEsyNDDLfB8OHD4dhi+M1NPK1Asy5NX84c+gvacZuoPLTwmpOfm6nPt7GPPB9G7S6xxhFNbRxTYfYUjK+kaCj38XjBRf5lGewbSJKVkxQ82/axU70ceSW3JpazrageN9JUTZ/Jfi4MfnipITwcmMoiij8eGrHskjyVyZbJd0WMMKRDWVhLPUiPMVWt/4d7YtZItzacaQKtXmXgsTCTWpIols3gftNYjrQoMsUelUdD8vOAWN9J28/SyC+uSh/K1KfyUlbqufn4di8DEBxntP5wnXYbJL1jtjsUgExAVjQxT1zI59X36m3t3YKqCQh1cud02L5onObY6zj57N6QIDAQABoycwJTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAC8AjBLy7EsRpi6oguCdFSb6nRGjvF17N+b6mDb3sARnB8T1pxvzTT26ya+AyWR+jjodEwbMIS+13lV+9qT2LwqlbOUNY519Pa2GRRY72JjeowWI3iKkKaMzfZUB7lRTGXdEuZApLbTO/3JVcR9ffu00N1UaAP9YGElSt4JDJYA9M+d/Qto+HiIsE0Kj+jdnwIYovPPOlryKOLfFb/r1GEq7n63xFZz83iyWNaZdsJ5N3YHxdOpkbBbCalOEBDJTjQKqeAYBLoANNU0OBslmqHCSBTEnhbqJHN6QKyF09ScOl5LwM1QsTl0UY5siGLAfj/jSf9OH9VLTPHOS8/N0Ka4="],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"eac34SOR2yT0jJcqu2Kd_3TxOBLhRU06RW1ue39Yg_VJeB2v0hYMy-Ufb-q1edcmh9S6LwXX9yRe4xeWaH-rjO_34q3e3nhSYV2dMUx78uQs2Np_6QhdEr0RZwZw9Vw0Jxr-FuMD7gBGdIQlJKbA7HHzBV9B0Gyy6I_SWnQuXtoOBEsFVFHJrT6UeZd2LrUcNRtqvkwjP0Hydx1RwPJMiGHu-K2sCBMeZuRRMhOqDyC9ArqapcnHgu0Cemoiur1zADm2MdUBvqkUsfc6Ogh9gknfDEpO4z66Kogt4zA7hqCl2d_nKKY4rIIIsrGUDZ0C3d7eWLP_YRordU6Mbs2ozg"}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"schemaVersion": 2,
|
||||
"config": {
|
||||
"mediaType": "application/vnd.oci.image.config.v1+json",
|
||||
"digest": "sha256:572996c3caeacea40b947911a9dda21516c082b5a64af30048a02a6f5eb956d4",
|
||||
"size": 1035
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
|
||||
"digest": "sha256:63b65145d645c1250c391b2d16ebe53b3747c295ca8ba2fcb6b0cf064a4dc21c",
|
||||
"size": 3374446
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"architecture":"amd64","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/bash"],"ArgsEscaped":true,"OnBuild":null},"created":"2023-02-11T04:46:42.558343068Z","history":[{"created":"2023-02-11T04:46:42.449083344Z","created_by":"/bin/sh -c #(nop) ADD file:40887ab7c06977737e63c215c9bd297c0c74de8d12d16ebdf1c3d40ac392f62d in / "},{"created":"2023-02-11T04:46:42.558343068Z","created_by":"/bin/sh -c #(nop) CMD [\"/bin/sh\"]","empty_layer":true},{"created":"2023-02-11T04:46:42.558343068Z","created_by":"CMD [\"/bin/bash\"]","comment":"buildkit.dockerfile.v0","empty_layer":true}],"moby.buildkit.buildinfo.v1":"eyJmcm9udGVuZCI6ImRvY2tlcmZpbGUudjAiLCJzb3VyY2VzIjpbeyJ0eXBlIjoiZG9ja2VyLWltYWdlIiwicmVmIjoiZG9ja2VyLmlvL2xpYnJhcnkvYWxwaW5lOmxhdGVzdCIsInBpbiI6InNoYTI1Njo2OTY2NWQwMmNiMzIxOTJlNTJlMDc2NDRkNzZiYzZmMjVhYmViNTQxMGVkYzFjN2E4MWExMGJhM2YwZWZiOTBhIn1dfQ==","os":"linux","rootfs":{"type":"layers","diff_ids":["sha256:7cd52847ad775a5ddc4b58326cf884beee34544296402c6292ed76474c686d39"]}}
|
Binary file not shown.
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"schemaVersion": 2,
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"digest": "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0",
|
||||
"size": 481,
|
||||
"annotations": {
|
||||
"io.containerd.image.name": "docker.io/library/alpine:v2",
|
||||
"org.opencontainers.image.created": "2023-03-13T02:31:43Z",
|
||||
"org.opencontainers.image.ref.name": "v2"
|
||||
},
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
[
|
||||
{
|
||||
"Config": "blobs/sha256/572996c3caeacea40b947911a9dda21516c082b5a64af30048a02a6f5eb956d4",
|
||||
"RepoTags": null,
|
||||
"Layers": [
|
||||
"blobs/sha256/63b65145d645c1250c391b2d16ebe53b3747c295ca8ba2fcb6b0cf064a4dc21c"
|
||||
]
|
||||
}
|
||||
]
|
|
@ -0,0 +1 @@
|
|||
{"imageLayoutVersion":"1.0.0"}
|
|
@ -1,3 +1,16 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package trustpolicy
|
||||
|
||||
const (
|
||||
|
|
13
log/log.go
13
log/log.go
|
@ -1,3 +1,16 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package log provides logging functionality to notation.
|
||||
// Users who want to enable logging option in notation should implement the
|
||||
// log.Logger interface and include it in context by calling log.WithLogger.
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package log provides logging functionality to notation.
|
||||
// Users who want to enable logging option in notation should implement the
|
||||
// log.Logger interface and include it in context by calling log.WithLogger.
|
||||
// 3rd party loggers that implement log.Logger: github.com/uber-go/zap.SugaredLogger
|
||||
// and github.com/sirupsen/logrus.Logger.
|
||||
package log
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWithLoggerAndGetLogger(t *testing.T) {
|
||||
tl := &discardLogger{}
|
||||
ctx := WithLogger(context.Background(), tl)
|
||||
|
||||
if got := GetLogger(ctx); got != tl {
|
||||
t.Errorf("GetLogger() = %v, want %v", got, tl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetLoggerWithNoLogger(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
if got := GetLogger(ctx); got != Discard {
|
||||
t.Errorf("GetLogger() = %v, want Discard", got)
|
||||
}
|
||||
}
|
482
notation.go
482
notation.go
|
@ -1,39 +1,57 @@
|
|||
// 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 provides signer and verifier for notation Sign
|
||||
// and Verification.
|
||||
// and Verification. It supports both OCI artifact and arbitrary blob.
|
||||
package notation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
orasRegistry "oras.land/oras-go/v2/registry"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/revocation"
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-core-go/signature/cose"
|
||||
"github.com/notaryproject/notation-core-go/signature/jws"
|
||||
"github.com/notaryproject/notation-go/internal/envelope"
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
"github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/tspclient-go"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
orasRegistry "oras.land/oras-go/v2/registry"
|
||||
)
|
||||
|
||||
const annotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256"
|
||||
|
||||
var errDoneVerification = errors.New("done verification")
|
||||
|
||||
var reservedAnnotationPrefixes = [...]string{"io.cncf.notary"}
|
||||
|
||||
// SignOptions contains parameters for Signer.Sign.
|
||||
type SignOptions struct {
|
||||
// ArtifactReference sets the reference of the artifact that needs to be
|
||||
// signed.
|
||||
ArtifactReference string
|
||||
|
||||
// SignerSignOptions contains parameters for [Signer] and [BlobSigner].
|
||||
type SignerSignOptions struct {
|
||||
// SignatureMediaType is the envelope type of the signature.
|
||||
// Currently both `application/jose+json` and `application/cose` are
|
||||
// Currently, both `application/jose+json` and `application/cose` are
|
||||
// supported.
|
||||
SignatureMediaType string
|
||||
|
||||
|
@ -46,24 +64,58 @@ type SignOptions struct {
|
|||
|
||||
// SigningAgent sets the signing agent name
|
||||
SigningAgent string
|
||||
|
||||
// Timestamper denotes the timestamper for RFC 3161 timestamping
|
||||
Timestamper tspclient.Timestamper
|
||||
|
||||
// TSARootCAs is the cert pool holding caller's TSA trust anchor
|
||||
TSARootCAs *x509.CertPool
|
||||
|
||||
// TSARevocationValidator is used for validating revocation status of
|
||||
// timestamping certificate chain with context during signing.
|
||||
// When present, only used when timestamping is performed.
|
||||
TSARevocationValidator revocation.Validator
|
||||
}
|
||||
|
||||
// RemoteSignOptions contains parameters for notation.Sign.
|
||||
type RemoteSignOptions struct {
|
||||
SignOptions
|
||||
// Signer is a generic interface for signing an OCI artifact.
|
||||
// The interface allows signing with local or remote keys,
|
||||
// and packing in various signature formats.
|
||||
type Signer interface {
|
||||
// Sign signs the OCI artifact described by its descriptor,
|
||||
// and returns the signature and SignerInfo.
|
||||
Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error)
|
||||
}
|
||||
|
||||
// SignBlobOptions contains parameters for [notation.SignBlob].
|
||||
type SignBlobOptions struct {
|
||||
SignerSignOptions
|
||||
|
||||
// ContentMediaType is the media-type of the blob being signed.
|
||||
ContentMediaType string
|
||||
|
||||
// UserMetadata contains key-value pairs that are added to the signature
|
||||
// payload
|
||||
UserMetadata map[string]string
|
||||
}
|
||||
|
||||
// Signer is a generic interface for signing an artifact.
|
||||
// BlobDescriptorGenerator creates descriptor using the digest Algorithm.
|
||||
// Below is the example of minimal descriptor, it must contain mediatype,
|
||||
// digest and size of the artifact.
|
||||
//
|
||||
// {
|
||||
// "mediaType": "application/octet-stream",
|
||||
// "digest": "sha256:2f3a23b6373afb134ddcd864be8e037e34a662d090d33ee849471ff73c873345",
|
||||
// "size": 1024
|
||||
// }
|
||||
type BlobDescriptorGenerator func(digest.Algorithm) (ocispec.Descriptor, error)
|
||||
|
||||
// BlobSigner is a generic interface for signing arbitrary data.
|
||||
// The interface allows signing with local or remote keys,
|
||||
// and packing in various signature formats.
|
||||
type Signer interface {
|
||||
// Sign signs the artifact described by its descriptor,
|
||||
// and returns the signature and SignerInfo.
|
||||
Sign(ctx context.Context, desc ocispec.Descriptor, opts SignOptions) ([]byte, *signature.SignerInfo, error)
|
||||
type BlobSigner interface {
|
||||
// SignBlob signs the descriptor returned by genDesc, and returns the
|
||||
// signature and SignerInfo.
|
||||
SignBlob(ctx context.Context, genDesc BlobDescriptorGenerator, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error)
|
||||
}
|
||||
|
||||
// signerAnnotation facilitates return of manifest annotations by signers
|
||||
|
@ -73,92 +125,161 @@ type signerAnnotation interface {
|
|||
PluginAnnotations() map[string]string
|
||||
}
|
||||
|
||||
// Sign signs the artifact in the remote registry and push the signature to the
|
||||
// remote.
|
||||
// The descriptor of the sign content is returned upon sucessful signing.
|
||||
func Sign(ctx context.Context, signer Signer, repo registry.Repository, remoteOpts RemoteSignOptions) (ocispec.Descriptor, error) {
|
||||
// Input validation for expiry duration
|
||||
if remoteOpts.ExpiryDuration < 0 {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("expiry duration cannot be a negative value")
|
||||
}
|
||||
// SignOptions contains parameters for [notation.Sign].
|
||||
type SignOptions struct {
|
||||
SignerSignOptions
|
||||
|
||||
if remoteOpts.ExpiryDuration%time.Second != 0 {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("expiry duration supports minimum granularity of seconds")
|
||||
// ArtifactReference sets the reference of the artifact that needs to be
|
||||
// signed. It can be a tag, a digest or a full reference.
|
||||
ArtifactReference string
|
||||
|
||||
// UserMetadata contains key-value pairs that are added to the signature
|
||||
// payload
|
||||
UserMetadata map[string]string
|
||||
}
|
||||
|
||||
// Sign signs the OCI artifact and push the signature to the Repository.
|
||||
// The descriptor of the sign content is returned upon successful signing.
|
||||
//
|
||||
// Deprecated: use [SignOCI] instead.
|
||||
func Sign(ctx context.Context, signer Signer, repo registry.Repository, signOpts SignOptions) (ocispec.Descriptor, error) {
|
||||
artifactMenifestDesc, _, err := SignOCI(ctx, signer, repo, signOpts)
|
||||
return artifactMenifestDesc, err
|
||||
}
|
||||
|
||||
// SignOCI signs the OCI artifact and push the signature to the Repository.
|
||||
//
|
||||
// Both artifact and signature manifest descriptors are returned upon successful
|
||||
// signing.
|
||||
//
|
||||
// Note: If the error type is [remote.ReferrersError] and
|
||||
// referrerError.IsReferrersIndexDelete() returns true, the signature is
|
||||
// successfully pushed to the repository, but the referrers index deletion
|
||||
// failed. In this case, the artifact and signature manifest descriptors are
|
||||
// returned with the error.
|
||||
func SignOCI(ctx context.Context, signer Signer, repo registry.Repository, signOpts SignOptions) (artifactManifestDesc, sigManifestDesc ocispec.Descriptor, err error) {
|
||||
// sanity check
|
||||
if err := validateSignArguments(signer, signOpts.SignerSignOptions); err != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
|
||||
}
|
||||
if repo == nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, errors.New("repo cannot be nil")
|
||||
}
|
||||
|
||||
logger := log.GetLogger(ctx)
|
||||
artifactRef := remoteOpts.ArtifactReference
|
||||
ref, err := orasRegistry.ParseReference(artifactRef)
|
||||
artifactRef := signOpts.ArtifactReference
|
||||
if ref, err := orasRegistry.ParseReference(artifactRef); err == nil {
|
||||
// artifactRef is a valid full reference
|
||||
artifactRef = ref.Reference
|
||||
}
|
||||
artifactManifestDesc, err = repo.Resolve(ctx, artifactRef)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
if ref.Reference == "" {
|
||||
return ocispec.Descriptor{}, errors.New("reference is missing digest or tag")
|
||||
}
|
||||
targetDesc, err := repo.Resolve(ctx, artifactRef)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
if ref.ValidateReferenceAsDigest() != nil {
|
||||
// artifactRef is not a digest reference
|
||||
logger.Warnf("Always sign the artifact using digest(`@sha256:...`) rather than a tag(`:%s`) because tags are mutable and a tag reference can point to a different artifact than the one signed", ref.Reference)
|
||||
logger.Infof("Resolved artifact tag `%s` to digest `%s` before signing", ref.Reference, targetDesc.Digest.String())
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, fmt.Errorf("failed to resolve reference: %w", err)
|
||||
}
|
||||
|
||||
targetDesc, err = addUserMetadataToDescriptor(ctx, targetDesc, remoteOpts.UserMetadata)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
// artifactRef is a tag or a digest, if it's a digest it has to match
|
||||
// the resolved digest
|
||||
if artifactRef != artifactManifestDesc.Digest.String() {
|
||||
if _, err := digest.Parse(artifactRef); err == nil {
|
||||
// artifactRef is a digest, but does not match the resolved digest
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, fmt.Errorf("user input digest %s does not match the resolved digest %s", artifactRef, artifactManifestDesc.Digest.String())
|
||||
}
|
||||
|
||||
sig, signerInfo, err := signer.Sign(ctx, targetDesc, remoteOpts.SignOptions)
|
||||
// artifactRef is a tag
|
||||
logger.Warnf("Always sign the artifact using digest(`@sha256:...`) rather than a tag(`:%s`) because tags are mutable and a tag reference can point to a different artifact than the one signed", artifactRef)
|
||||
logger.Infof("Resolved artifact tag `%s` to digest `%v` before signing", artifactRef, artifactManifestDesc.Digest)
|
||||
}
|
||||
descToSign, err := addUserMetadataToDescriptor(ctx, artifactManifestDesc, signOpts.UserMetadata)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
|
||||
}
|
||||
sig, signerInfo, err := signer.Sign(ctx, descToSign, signOpts.SignerSignOptions)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
var pluginAnnotations map[string]string
|
||||
if signerAnts, ok := signer.(signerAnnotation); ok {
|
||||
pluginAnnotations = signerAnts.PluginAnnotations()
|
||||
}
|
||||
|
||||
logger.Debug("Generating annotation")
|
||||
annotations, err := generateAnnotations(signerInfo, pluginAnnotations)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
|
||||
}
|
||||
logger.Debugf("Generated annotations: %+v", annotations)
|
||||
logger.Debugf("Pushing signature of artifact descriptor: %+v, signature media type: %v", targetDesc, remoteOpts.SignatureMediaType)
|
||||
_, _, err = repo.PushSignature(ctx, remoteOpts.SignatureMediaType, sig, targetDesc, annotations)
|
||||
logger.Debugf("Pushing signature of artifact descriptor: %+v, signature media type: %v", artifactManifestDesc, signOpts.SignatureMediaType)
|
||||
_, sigManifestDesc, err = repo.PushSignature(ctx, signOpts.SignatureMediaType, sig, artifactManifestDesc, annotations)
|
||||
if err != nil {
|
||||
var referrerError *remote.ReferrersError
|
||||
if errors.As(err, &referrerError) && referrerError.IsReferrersIndexDelete() {
|
||||
// return the descriptors for referrersIndexDelete error as
|
||||
// the signature is successfully pushed to the repository
|
||||
return artifactManifestDesc, sigManifestDesc, err
|
||||
}
|
||||
logger.Error("Failed to push the signature")
|
||||
return ocispec.Descriptor{}, ErrorPushSignatureFailed{Msg: err.Error()}
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, ErrorPushSignatureFailed{Msg: err.Error()}
|
||||
}
|
||||
return artifactManifestDesc, sigManifestDesc, nil
|
||||
}
|
||||
|
||||
// SignBlob signs the arbitrary data from blobReader and returns
|
||||
// the signature and SignerInfo.
|
||||
func SignBlob(ctx context.Context, signer BlobSigner, blobReader io.Reader, signBlobOpts SignBlobOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
// sanity checks
|
||||
if err := validateSignArguments(signer, signBlobOpts.SignerSignOptions); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if blobReader == nil {
|
||||
return nil, nil, errors.New("blobReader cannot be nil")
|
||||
}
|
||||
if signBlobOpts.ContentMediaType == "" {
|
||||
return nil, nil, errors.New("content media-type cannot be empty")
|
||||
}
|
||||
if err := validateContentMediaType(signBlobOpts.ContentMediaType); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return targetDesc, nil
|
||||
getDescFunc := getDescriptorFunc(ctx, blobReader, signBlobOpts.ContentMediaType, signBlobOpts.UserMetadata)
|
||||
return signer.SignBlob(ctx, getDescFunc, signBlobOpts.SignerSignOptions)
|
||||
}
|
||||
|
||||
func validateSignArguments(signer any, signOpts SignerSignOptions) error {
|
||||
if signer == nil {
|
||||
return errors.New("signer cannot be nil")
|
||||
}
|
||||
if signOpts.ExpiryDuration < 0 {
|
||||
return errors.New("expiry duration cannot be a negative value")
|
||||
}
|
||||
if signOpts.ExpiryDuration%time.Second != 0 {
|
||||
return errors.New("expiry duration supports minimum granularity of seconds")
|
||||
}
|
||||
if signOpts.SignatureMediaType == "" {
|
||||
return errors.New("signature media-type cannot be empty")
|
||||
}
|
||||
if err := validateSigMediaType(signOpts.SignatureMediaType); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func addUserMetadataToDescriptor(ctx context.Context, desc ocispec.Descriptor, userMetadata map[string]string) (ocispec.Descriptor, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
|
||||
if desc.Annotations == nil && len(userMetadata) > 0 {
|
||||
desc.Annotations = map[string]string{}
|
||||
}
|
||||
|
||||
for k, v := range userMetadata {
|
||||
logger.Debugf("Adding metadata %v=%v to annotations", k, v)
|
||||
|
||||
for _, reservedPrefix := range reservedAnnotationPrefixes {
|
||||
if strings.HasPrefix(k, reservedPrefix) {
|
||||
return desc, fmt.Errorf("error adding user metadata: metadata key %v has reserved prefix %v", k, reservedPrefix)
|
||||
}
|
||||
}
|
||||
|
||||
if _, ok := desc.Annotations[k]; ok {
|
||||
return desc, fmt.Errorf("error adding user metadata: metadata key %v is already present in the target artifact", k)
|
||||
}
|
||||
|
||||
desc.Annotations[k] = v
|
||||
}
|
||||
|
||||
return desc, nil
|
||||
}
|
||||
|
||||
|
@ -177,7 +298,7 @@ type ValidationResult struct {
|
|||
Error error
|
||||
}
|
||||
|
||||
// VerificationOutcome encapsulates a signature blob's descriptor, its content,
|
||||
// VerificationOutcome encapsulates a signature envelope blob, its content,
|
||||
// the verification level and results for each verification type that was
|
||||
// performed.
|
||||
type VerificationOutcome struct {
|
||||
|
@ -200,6 +321,7 @@ type VerificationOutcome struct {
|
|||
Error error
|
||||
}
|
||||
|
||||
// UserMetadata returns the user metadata from the signature envelope.
|
||||
func (outcome *VerificationOutcome) UserMetadata() (map[string]string, error) {
|
||||
if outcome.EnvelopeContent == nil {
|
||||
return nil, errors.New("unable to find envelope content for verification outcome")
|
||||
|
@ -210,22 +332,21 @@ func (outcome *VerificationOutcome) UserMetadata() (map[string]string, error) {
|
|||
if err != nil {
|
||||
return nil, errors.New("failed to unmarshal the payload content in the signature blob to envelope.Payload")
|
||||
}
|
||||
|
||||
if payload.TargetArtifact.Annotations == nil {
|
||||
return map[string]string{}, nil
|
||||
}
|
||||
|
||||
return payload.TargetArtifact.Annotations, nil
|
||||
}
|
||||
|
||||
// VerifyOptions contains parameters for Verifier.Verify.
|
||||
type VerifyOptions struct {
|
||||
// ArtifactReference is the reference of the artifact that is been
|
||||
// verified against to.
|
||||
// VerifierVerifyOptions contains parameters for [Verifier.Verify] used for
|
||||
// verifying OCI artifact.
|
||||
type VerifierVerifyOptions struct {
|
||||
// ArtifactReference is the reference of the artifact that is being
|
||||
// verified against to. It must be a full reference.
|
||||
ArtifactReference string
|
||||
|
||||
// SignatureMediaType is the envelope type of the signature.
|
||||
// Currently both `application/jose+json` and `application/cose` are
|
||||
// Currently only `application/jose+json` and `application/cose` are
|
||||
// supported.
|
||||
SignatureMediaType string
|
||||
|
||||
|
@ -233,23 +354,55 @@ type VerifyOptions struct {
|
|||
PluginConfig map[string]string
|
||||
|
||||
// UserMetadata contains key-value pairs that must be present in the
|
||||
// signature
|
||||
// signature.
|
||||
UserMetadata map[string]string
|
||||
}
|
||||
|
||||
// Verifier is a generic interface for verifying an artifact.
|
||||
// Verifier is a generic interface for verifying an OCI artifact.
|
||||
type Verifier interface {
|
||||
// Verify verifies the signature blob `signature` against the target OCI
|
||||
// artifact with manifest descriptor `desc`, and returns the outcome upon
|
||||
// Verify verifies the `signature` associated with the target OCI artifact
|
||||
// with manifest descriptor `desc`, and returns the outcome upon
|
||||
// successful verification.
|
||||
// If nil signature is present and the verification level is not 'skip',
|
||||
// an error will be returned.
|
||||
Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifyOptions) (*VerificationOutcome, error)
|
||||
Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifierVerifyOptions) (*VerificationOutcome, error)
|
||||
}
|
||||
|
||||
// RemoteVerifyOptions contains parameters for notation.Verify.
|
||||
type RemoteVerifyOptions struct {
|
||||
// ArtifactReference is the reference of the artifact that is been
|
||||
// BlobVerifierVerifyOptions contains parameters for [BlobVerifier.Verify].
|
||||
type BlobVerifierVerifyOptions struct {
|
||||
// SignatureMediaType is the envelope type of the signature.
|
||||
// Currently only `application/jose+json` and `application/cose` are
|
||||
// supported.
|
||||
SignatureMediaType string
|
||||
|
||||
// PluginConfig is a map of plugin configs.
|
||||
PluginConfig map[string]string
|
||||
|
||||
// UserMetadata contains key-value pairs that must be present in the
|
||||
// signature.
|
||||
UserMetadata map[string]string
|
||||
|
||||
// TrustPolicyName is the name of trust policy picked by caller.
|
||||
// If empty, the global trust policy will be applied.
|
||||
TrustPolicyName string
|
||||
}
|
||||
|
||||
// BlobVerifier is a generic interface for verifying a blob.
|
||||
type BlobVerifier interface {
|
||||
// VerifyBlob verifies the `signature` against the target blob using the
|
||||
// descriptor returned by descGenFunc parameter and
|
||||
// returns the outcome upon successful verification.
|
||||
VerifyBlob(ctx context.Context, descGenFunc BlobDescriptorGenerator, signature []byte, opts BlobVerifierVerifyOptions) (*VerificationOutcome, error)
|
||||
}
|
||||
|
||||
type verifySkipper interface {
|
||||
// SkipVerify validates whether the verification level is skip.
|
||||
SkipVerify(ctx context.Context, opts VerifierVerifyOptions) (bool, *trustpolicy.VerificationLevel, error)
|
||||
}
|
||||
|
||||
// VerifyOptions contains parameters for [notation.Verify].
|
||||
type VerifyOptions struct {
|
||||
// ArtifactReference is the reference of the artifact that is being
|
||||
// verified against to.
|
||||
ArtifactReference string
|
||||
|
||||
|
@ -266,46 +419,89 @@ type RemoteVerifyOptions struct {
|
|||
UserMetadata map[string]string
|
||||
}
|
||||
|
||||
type skipVerifier interface {
|
||||
// SkipVerify validates whether the verification level is skip.
|
||||
SkipVerify(ctx context.Context, artifactRef string) (bool, *trustpolicy.VerificationLevel, error)
|
||||
// VerifyBlobOptions contains parameters for [notation.VerifyBlob].
|
||||
type VerifyBlobOptions struct {
|
||||
BlobVerifierVerifyOptions
|
||||
|
||||
// ContentMediaType is the media-type type of the content being verified.
|
||||
ContentMediaType string
|
||||
}
|
||||
|
||||
// VerifyBlob performs signature verification for a blob using notation supported
|
||||
// verification types (like integrity, authenticity, etc.) and returns the
|
||||
// successful signature verification outcome. The blob is read using blobReader,
|
||||
// and upon successful verification, it returns the descriptor of the blob.
|
||||
// For more details on signature verification, see
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification
|
||||
func VerifyBlob(ctx context.Context, blobVerifier BlobVerifier, blobReader io.Reader, signature []byte, verifyBlobOpts VerifyBlobOptions) (ocispec.Descriptor, *VerificationOutcome, error) {
|
||||
if blobVerifier == nil {
|
||||
return ocispec.Descriptor{}, nil, errors.New("blobVerifier cannot be nil")
|
||||
}
|
||||
if blobReader == nil {
|
||||
return ocispec.Descriptor{}, nil, errors.New("blobReader cannot be nil")
|
||||
}
|
||||
if len(signature) == 0 {
|
||||
return ocispec.Descriptor{}, nil, errors.New("signature cannot be nil or empty")
|
||||
}
|
||||
if err := validateContentMediaType(verifyBlobOpts.ContentMediaType); err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
if err := validateSigMediaType(verifyBlobOpts.SignatureMediaType); err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
getDescFunc := getDescriptorFunc(ctx, blobReader, verifyBlobOpts.ContentMediaType, verifyBlobOpts.UserMetadata)
|
||||
vo, err := blobVerifier.VerifyBlob(ctx, getDescFunc, signature, verifyBlobOpts.BlobVerifierVerifyOptions)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
|
||||
var desc ocispec.Descriptor
|
||||
if err = json.Unmarshal(vo.EnvelopeContent.Payload.Content, &desc); err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
return desc, vo, nil
|
||||
}
|
||||
|
||||
// Verify performs signature verification on each of the notation supported
|
||||
// verification types (like integrity, authenticity, etc.) and return the
|
||||
// verification types (like integrity, authenticity, etc.) and returns the
|
||||
// successful signature verification outcome.
|
||||
// For more details on signature verification, see
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification
|
||||
func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, remoteOpts RemoteVerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) {
|
||||
func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, verifyOpts VerifyOptions) (ocispec.Descriptor, []*VerificationOutcome, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
|
||||
// opts to be passed in verifier.Verify()
|
||||
opts := VerifyOptions{
|
||||
ArtifactReference: remoteOpts.ArtifactReference,
|
||||
PluginConfig: remoteOpts.PluginConfig,
|
||||
UserMetadata: remoteOpts.UserMetadata,
|
||||
// sanity check
|
||||
if verifier == nil {
|
||||
return ocispec.Descriptor{}, nil, errors.New("verifier cannot be nil")
|
||||
}
|
||||
if repo == nil {
|
||||
return ocispec.Descriptor{}, nil, errors.New("repo cannot be nil")
|
||||
}
|
||||
if verifyOpts.MaxSignatureAttempts <= 0 {
|
||||
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("verifyOptions.MaxSignatureAttempts expects a positive number, got %d", verifyOpts.MaxSignatureAttempts)}
|
||||
}
|
||||
|
||||
if skipChecker, ok := verifier.(skipVerifier); ok {
|
||||
// opts to be passed in verifier.Verify()
|
||||
opts := VerifierVerifyOptions{
|
||||
ArtifactReference: verifyOpts.ArtifactReference,
|
||||
PluginConfig: verifyOpts.PluginConfig,
|
||||
UserMetadata: verifyOpts.UserMetadata,
|
||||
}
|
||||
if skipChecker, ok := verifier.(verifySkipper); ok {
|
||||
logger.Info("Checking whether signature verification should be skipped or not")
|
||||
skip, verificationLevel, err := skipChecker.SkipVerify(ctx, opts.ArtifactReference)
|
||||
skip, verificationLevel, err := skipChecker.SkipVerify(ctx, opts)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, nil, err
|
||||
}
|
||||
if skip {
|
||||
logger.Infoln("Verification skipped for", remoteOpts.ArtifactReference)
|
||||
logger.Infoln("Signature verification skipped for", verifyOpts.ArtifactReference)
|
||||
return ocispec.Descriptor{}, []*VerificationOutcome{{VerificationLevel: verificationLevel}}, nil
|
||||
}
|
||||
logger.Info("Check over. Trust policy is not configured to skip signature verification")
|
||||
}
|
||||
|
||||
// check MaxSignatureAttempts
|
||||
if remoteOpts.MaxSignatureAttempts <= 0 {
|
||||
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("verifyOptions.MaxSignatureAttempts expects a positive number, got %d", remoteOpts.MaxSignatureAttempts)}
|
||||
logger.Info("Check over. The signature verification level is not set to 'skip' in the trust policy.")
|
||||
}
|
||||
|
||||
// get artifact descriptor
|
||||
artifactRef := remoteOpts.ArtifactReference
|
||||
artifactRef := verifyOpts.ArtifactReference
|
||||
ref, err := orasRegistry.ParseReference(artifactRef)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()}
|
||||
|
@ -313,28 +509,30 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, re
|
|||
if ref.Reference == "" {
|
||||
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: "reference is missing digest or tag"}
|
||||
}
|
||||
artifactDescriptor, err := repo.Resolve(ctx, artifactRef)
|
||||
artifactDescriptor, err := repo.Resolve(ctx, ref.Reference)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: err.Error()}
|
||||
}
|
||||
if ref.ValidateReferenceAsDigest() != nil {
|
||||
// artifactRef is not a digest reference
|
||||
logger.Infof("Resolved artifact tag `%s` to digest `%s` before verification", ref.Reference, artifactDescriptor.Digest.String())
|
||||
logger.Infof("Resolved artifact tag `%s` to digest `%v` before verification", ref.Reference, artifactDescriptor.Digest)
|
||||
logger.Warn("The resolved digest may not point to the same signed artifact, since tags are mutable")
|
||||
} else if ref.Reference != artifactDescriptor.Digest.String() {
|
||||
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("user input digest %s does not match the resolved digest %s", ref.Reference, artifactDescriptor.Digest.String())}
|
||||
}
|
||||
|
||||
var verificationSucceeded bool
|
||||
var verificationOutcomes []*VerificationOutcome
|
||||
errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("total number of signatures associated with an artifact should be less than: %d", remoteOpts.MaxSignatureAttempts)}
|
||||
var verificationFailedErrorArray = []error{ErrorVerificationFailed{}}
|
||||
errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("signature evaluation stopped. The configured limit of %d signatures to verify per artifact exceeded", verifyOpts.MaxSignatureAttempts)}
|
||||
numOfSignatureProcessed := 0
|
||||
|
||||
var verificationFailedErr error = ErrorVerificationFailed{}
|
||||
|
||||
// get signature manifests
|
||||
logger.Debug("Fetching signature manifests using referrers API")
|
||||
logger.Debug("Fetching signature manifests")
|
||||
err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error {
|
||||
// process signatures
|
||||
for _, sigManifestDesc := range signatureManifests {
|
||||
if numOfSignatureProcessed >= remoteOpts.MaxSignatureAttempts {
|
||||
if numOfSignatureProcessed >= verifyOpts.MaxSignatureAttempts {
|
||||
break
|
||||
}
|
||||
numOfSignatureProcessed++
|
||||
|
@ -342,7 +540,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, re
|
|||
// get signature envelope
|
||||
sigBlob, sigDesc, err := repo.FetchSignatureBlob(ctx, sigManifestDesc)
|
||||
if err != nil {
|
||||
return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : %v", sigManifestDesc.Digest, artifactRef, err.Error())}
|
||||
return ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the Repository, error : %v", sigManifestDesc.Digest, artifactRef, err.Error())}
|
||||
}
|
||||
|
||||
// using signature media type fetched from registry
|
||||
|
@ -356,29 +554,26 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, re
|
|||
logger.Error("Got nil outcome. Expecting non-nil outcome on verification failure")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := outcome.Error.(ErrorUserMetadataVerificationFailed); ok {
|
||||
verificationFailedErr = outcome.Error
|
||||
}
|
||||
|
||||
outcome.Error = fmt.Errorf("failed to verify signature with digest %v, %w", sigManifestDesc.Digest, outcome.Error)
|
||||
verificationFailedErrorArray = append(verificationFailedErrorArray, outcome.Error)
|
||||
continue
|
||||
}
|
||||
// at this point, the signature is verified successfully. Add
|
||||
// it to the verificationOutcomes.
|
||||
verificationOutcomes = append(verificationOutcomes, outcome)
|
||||
// at this point, the signature is verified successfully
|
||||
verificationSucceeded = true
|
||||
|
||||
// on success, verificationOutcomes only contains the
|
||||
// succeeded outcome
|
||||
verificationOutcomes = []*VerificationOutcome{outcome}
|
||||
logger.Debugf("Signature verification succeeded for artifact %v with signature digest %v", artifactDescriptor.Digest, sigManifestDesc.Digest)
|
||||
|
||||
// early break on success
|
||||
return errDoneVerification
|
||||
}
|
||||
|
||||
if numOfSignatureProcessed >= remoteOpts.MaxSignatureAttempts {
|
||||
if numOfSignatureProcessed >= verifyOpts.MaxSignatureAttempts {
|
||||
return errExceededMaxVerificationLimit
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil && !errors.Is(err, errDoneVerification) {
|
||||
if errors.Is(err, errExceededMaxVerificationLimit) {
|
||||
return ocispec.Descriptor{}, verificationOutcomes, err
|
||||
|
@ -388,13 +583,13 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, re
|
|||
|
||||
// If there's no signature associated with the reference
|
||||
if numOfSignatureProcessed == 0 {
|
||||
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signature is associated with %q, make sure the image was signed successfully", artifactRef)}
|
||||
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("no signature is associated with %q, make sure the artifact was signed successfully", artifactRef)}
|
||||
}
|
||||
|
||||
// Verification Failed
|
||||
if len(verificationOutcomes) == 0 {
|
||||
if !verificationSucceeded {
|
||||
logger.Debugf("Signature verification failed for all the signatures associated with artifact %v", artifactDescriptor.Digest)
|
||||
return ocispec.Descriptor{}, verificationOutcomes, verificationFailedErr
|
||||
return ocispec.Descriptor{}, verificationOutcomes, errors.Join(verificationFailedErrorArray...)
|
||||
}
|
||||
|
||||
// Verification Succeeded
|
||||
|
@ -402,6 +597,10 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, re
|
|||
}
|
||||
|
||||
func generateAnnotations(signerInfo *signature.SignerInfo, annotations map[string]string) (map[string]string, error) {
|
||||
// sanity check
|
||||
if signerInfo == nil {
|
||||
return nil, errors.New("failed to generate annotations: signerInfo cannot be nil")
|
||||
}
|
||||
var thumbprints []string
|
||||
for _, cert := range signerInfo.CertificateChain {
|
||||
checkSum := sha256.Sum256(cert.Raw)
|
||||
|
@ -411,11 +610,46 @@ func generateAnnotations(signerInfo *signature.SignerInfo, annotations map[strin
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if annotations == nil {
|
||||
annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
annotations[annotationX509ChainThumbprint] = string(val)
|
||||
annotations[envelope.AnnotationX509ChainThumbprint] = string(val)
|
||||
signingTime, err := envelope.SigningTime(signerInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
annotations[ocispec.AnnotationCreated] = signingTime.Format(time.RFC3339)
|
||||
return annotations, nil
|
||||
}
|
||||
|
||||
func getDescriptorFunc(ctx context.Context, reader io.Reader, contentMediaType string, userMetadata map[string]string) BlobDescriptorGenerator {
|
||||
return func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) {
|
||||
digester := hashAlgo.Digester()
|
||||
bytes, err := io.Copy(digester.Hash(), reader)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
targetDesc := ocispec.Descriptor{
|
||||
MediaType: contentMediaType,
|
||||
Digest: digester.Digest(),
|
||||
Size: bytes,
|
||||
}
|
||||
return addUserMetadataToDescriptor(ctx, targetDesc, userMetadata)
|
||||
}
|
||||
}
|
||||
|
||||
func validateContentMediaType(contentMediaType string) error {
|
||||
if contentMediaType != "" {
|
||||
if _, _, err := mime.ParseMediaType(contentMediaType); err != nil {
|
||||
return fmt.Errorf("invalid content media-type %q: %v", contentMediaType, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSigMediaType(sigMediaType string) error {
|
||||
if !(sigMediaType == jws.MediaTypeEnvelope || sigMediaType == cose.MediaTypeEnvelope) {
|
||||
return fmt.Errorf("invalid signature media-type %q", sigMediaType)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
669
notation_test.go
669
notation_test.go
|
@ -1,16 +1,43 @@
|
|||
// 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-core-go/signature/cose"
|
||||
"github.com/notaryproject/notation-core-go/signature/jws"
|
||||
"github.com/notaryproject/notation-go/internal/envelope"
|
||||
"github.com/notaryproject/notation-go/internal/mock"
|
||||
"github.com/notaryproject/notation-go/internal/mock/ocilayout"
|
||||
"github.com/notaryproject/notation-go/plugin"
|
||||
"github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
|
@ -28,7 +55,8 @@ func TestSignSuccess(t *testing.T) {
|
|||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(b *testing.T) {
|
||||
opts := RemoteSignOptions{}
|
||||
opts := SignOptions{}
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
opts.ExpiryDuration = tc.dur
|
||||
opts.ArtifactReference = mock.SampleArtifactUri
|
||||
|
||||
|
@ -40,11 +68,91 @@ func TestSignSuccess(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSignBlobSuccess(t *testing.T) {
|
||||
reader := strings.NewReader("some content")
|
||||
testCases := []struct {
|
||||
name string
|
||||
dur time.Duration
|
||||
mtype string
|
||||
agent string
|
||||
pConfig map[string]string
|
||||
metadata map[string]string
|
||||
}{
|
||||
{"expiryInHours", 24 * time.Hour, "video/mp4", "", nil, nil},
|
||||
{"oneSecondExpiry", 1 * time.Second, "video/mp4", "", nil, nil},
|
||||
{"zeroExpiry", 0, "video/mp4", "", nil, nil},
|
||||
{"validContentType", 1 * time.Second, "video/mp4", "", nil, nil},
|
||||
{"emptyContentType", 1 * time.Second, "video/mp4", "someDummyAgent", map[string]string{"hi": "hello"}, map[string]string{"bye": "tata"}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(b *testing.T) {
|
||||
opts := SignBlobOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: jws.MediaTypeEnvelope,
|
||||
ExpiryDuration: tc.dur,
|
||||
PluginConfig: tc.pConfig,
|
||||
SigningAgent: tc.agent,
|
||||
},
|
||||
UserMetadata: expectedMetadata,
|
||||
ContentMediaType: tc.mtype,
|
||||
}
|
||||
|
||||
_, _, err := SignBlob(context.Background(), &dummySigner{}, reader, opts)
|
||||
if err != nil {
|
||||
b.Fatalf("Sign failed with error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignBlobError(t *testing.T) {
|
||||
reader := strings.NewReader("some content")
|
||||
testCases := []struct {
|
||||
name string
|
||||
signer BlobSigner
|
||||
dur time.Duration
|
||||
rdr io.Reader
|
||||
sigMType string
|
||||
ctMType string
|
||||
errMsg string
|
||||
}{
|
||||
{"negativeExpiry", &dummySigner{}, -1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration cannot be a negative value"},
|
||||
{"milliSecExpiry", &dummySigner{}, 1 * time.Millisecond, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration supports minimum granularity of seconds"},
|
||||
{"invalidContentMediaType", &dummySigner{}, 1 * time.Second, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type \"video/mp4/zoping\": mime: unexpected content after media subtype"},
|
||||
{"emptyContentMediaType", &dummySigner{}, 1 * time.Second, reader, "", jws.MediaTypeEnvelope, "content media-type cannot be empty"},
|
||||
{"invalidSignatureMediaType", &dummySigner{}, 1 * time.Second, reader, "", "", "content media-type cannot be empty"},
|
||||
{"nilReader", &dummySigner{}, 1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"},
|
||||
{"nilSigner", nil, 1 * time.Second, reader, "video/mp4", jws.MediaTypeEnvelope, "signer cannot be nil"},
|
||||
{"signerError", &dummySigner{fail: true}, 1 * time.Second, reader, "video/mp4", jws.MediaTypeEnvelope, "expected SignBlob failure"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
opts := SignBlobOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: jws.MediaTypeEnvelope,
|
||||
ExpiryDuration: tc.dur,
|
||||
PluginConfig: nil,
|
||||
},
|
||||
ContentMediaType: tc.sigMType,
|
||||
}
|
||||
|
||||
_, _, err := SignBlob(context.Background(), tc.signer, tc.rdr, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error but didnt found")
|
||||
}
|
||||
if err.Error() != tc.errMsg {
|
||||
t.Fatalf("expected err message to be '%s' but found '%s'", tc.errMsg, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignSuccessWithUserMetadata(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
opts := RemoteSignOptions{}
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = mock.SampleArtifactUri
|
||||
opts.UserMetadata = expectedMetadata
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &verifyMetadataSigner{}, repo, opts)
|
||||
if err != nil {
|
||||
|
@ -52,6 +160,71 @@ func TestSignSuccessWithUserMetadata(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSignWithDanglingReferrersIndex(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
repo.PushSignatureError = &remote.ReferrersError{
|
||||
Op: "DeleteReferrersIndex",
|
||||
Err: errors.New("error"),
|
||||
}
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = mock.SampleArtifactUri
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("no error occurred, expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithNilRepo(t *testing.T) {
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = mock.SampleArtifactUri
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, nil, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("no error occurred, expected error: repo cannot be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignResolveFailed(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
repo.ResolveError = errors.New("resolve error")
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = mock.SampleArtifactUri
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("no error occurred, expected resolve error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignArtifactRefIsTag(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = "registry.acme-rockets.io/software/net-monitor:v1"
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithPushSignatureError(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
repo.PushSignatureError = errors.New("error")
|
||||
opts := SignOptions{}
|
||||
opts.ArtifactReference = mock.SampleArtifactUri
|
||||
opts.SignatureMediaType = jws.MediaTypeEnvelope
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("no error occurred, expected error: failed to delete dangling referrers index")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithInvalidExpiry(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
testCases := []struct {
|
||||
|
@ -63,7 +236,7 @@ func TestSignWithInvalidExpiry(t *testing.T) {
|
|||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(b *testing.T) {
|
||||
opts := RemoteSignOptions{}
|
||||
opts := SignOptions{}
|
||||
opts.ExpiryDuration = tc.dur
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
|
@ -85,7 +258,14 @@ func TestSignWithInvalidUserMetadata(t *testing.T) {
|
|||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(b *testing.T) {
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, RemoteSignOptions{UserMetadata: tc.metadata})
|
||||
opts := SignOptions{
|
||||
UserMetadata: tc.metadata,
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: jws.MediaTypeEnvelope,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
b.Fatalf("Expected error but not found")
|
||||
}
|
||||
|
@ -93,62 +273,129 @@ func TestSignWithInvalidUserMetadata(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRegistryResolveError(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
func TestSignOptsMissingSignatureMediaType(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
opts := SignOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: "",
|
||||
},
|
||||
ArtifactReference: mock.SampleArtifactUri,
|
||||
}
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error but not found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignOptsUnknownMediaType(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
opts := SignOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: "unknown",
|
||||
},
|
||||
ArtifactReference: mock.SampleArtifactUri,
|
||||
}
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error but not found")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRegistryResolveError(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
|
||||
errorMessage := "network error"
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
||||
// mock the repository
|
||||
repo.ResolveError = errors.New(errorMessage)
|
||||
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
repo.ResolveError = errors.New("network error")
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err == nil || !errors.Is(err, expectedErr) {
|
||||
if err == nil || err.Error() != errorMessage {
|
||||
t.Fatalf("RegistryResolve expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyEmptyReference(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
|
||||
errorMessage := "reference is missing digest or tag"
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
||||
// mock the repository
|
||||
opts := RemoteVerifyOptions{ArtifactReference: "localhost/test", MaxSignatureAttempts: 50}
|
||||
opts := VerifyOptions{ArtifactReference: "localhost/test", MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
if err == nil || !errors.Is(err, expectedErr) {
|
||||
if err == nil || err.Error() != errorMessage {
|
||||
t.Fatalf("VerifyTagReference expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTagReferenceFailed(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
|
||||
errorMessage := "invalid reference: invalid repository"
|
||||
errorMessage := "invalid reference: invalid repository \"UPPERCASE/test\""
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
||||
// mock the repository
|
||||
opts := RemoteVerifyOptions{ArtifactReference: "localhost/UPPERCASE/test", MaxSignatureAttempts: 50}
|
||||
opts := VerifyOptions{ArtifactReference: "localhost/UPPERCASE/test", MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
if err == nil || !errors.Is(err, expectedErr) {
|
||||
if err == nil || err.Error() != errorMessage {
|
||||
t.Fatalf("VerifyTagReference expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkippedSignatureVerification(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
func TestVerifyDigestNotMatchResolve(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelSkip}
|
||||
repo.MissMatchDigest = true
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
|
||||
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
errorMessage := fmt.Sprintf("user input digest %s does not match the resolved digest %s", mock.SampleDigest, mock.ZeroDigest)
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
if err == nil || err.Error() != errorMessage {
|
||||
t.Fatalf("VerifyDigestNotMatch expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignDigestNotMatchResolve(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
repo.MissMatchDigest = true
|
||||
signOpts := SignOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: jws.MediaTypeEnvelope,
|
||||
},
|
||||
ArtifactReference: mock.SampleArtifactUri,
|
||||
}
|
||||
|
||||
errorMessage := fmt.Sprintf("user input digest %s does not match the resolved digest %s", mock.SampleDigest, mock.ZeroDigest)
|
||||
expectedErr := errors.New(errorMessage)
|
||||
|
||||
_, err := Sign(context.Background(), &dummySigner{}, repo, signOpts)
|
||||
if err == nil || err.Error() != errorMessage {
|
||||
t.Fatalf("SignDigestNotMatch expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkippedSignatureVerification(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelSkip, false}
|
||||
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, outcomes, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err != nil || outcomes[0].VerificationLevel.Name != trustpolicy.LevelSkip.Name {
|
||||
|
@ -157,15 +404,15 @@ func TestSkippedSignatureVerification(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRegistryNoSignatureManifests(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
errorMessage := fmt.Sprintf("no signature is associated with %q, make sure the image was signed successfully", mock.SampleArtifactUri)
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
errorMessage := fmt.Sprintf("no signature is associated with %q, make sure the artifact was signed successfully", mock.SampleArtifactUri)
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
||||
// mock the repository
|
||||
repo.ListSignaturesResponse = []ocispec.Descriptor{}
|
||||
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err == nil || !errors.Is(err, expectedErr) {
|
||||
|
@ -174,15 +421,15 @@ func TestRegistryNoSignatureManifests(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestRegistryFetchSignatureBlobError(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
errorMessage := fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the registry, error : network error", mock.SampleDigest, mock.SampleArtifactUri)
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
errorMessage := fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the Repository, error : network error", mock.SampleDigest, mock.SampleArtifactUri)
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
|
||||
|
||||
// mock the repository
|
||||
repo.FetchSignatureBlobError = errors.New("network error")
|
||||
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err == nil || !errors.Is(err, expectedErr) {
|
||||
|
@ -191,27 +438,58 @@ func TestRegistryFetchSignatureBlobError(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVerifyValid(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
|
||||
// mock the repository
|
||||
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("SignaureMediaTypeMismatch expected: %v got: %v", nil, err)
|
||||
t.Fatalf("expected nil error, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifySkip(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, true}
|
||||
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxSignatureAttemptsMissing(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
expectedErr := ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("verifyOptions.MaxSignatureAttempts expects a positive number, got %d", 0)}
|
||||
|
||||
// mock the repository
|
||||
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri}
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err == nil || !errors.Is(err, expectedErr) {
|
||||
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExceededMaxSignatureAttempts(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
repo.ExceededNumOfSignatures = true
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict, false}
|
||||
|
||||
expectedErr := ErrorVerificationFailed{Msg: fmt.Sprintf("signature evaluation stopped. The configured limit of %d signatures to verify per artifact exceeded", 1)}
|
||||
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 1}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err == nil || !errors.Is(err, expectedErr) {
|
||||
|
@ -220,30 +498,116 @@ func TestMaxSignatureAttemptsMissing(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestVerifyFailed(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict}
|
||||
expectedErr := ErrorVerificationFailed{}
|
||||
t.Run("verification error", func(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
repo := mock.NewRepository()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict, false}
|
||||
expectedErr := ErrorVerificationFailed{}
|
||||
|
||||
// mock the repository
|
||||
opts := RemoteVerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, repo, opts)
|
||||
|
||||
if err == nil || !errors.Is(err, expectedErr) {
|
||||
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
|
||||
if err == nil || !errors.Is(err, expectedErr) {
|
||||
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verifier is nil", func(t *testing.T) {
|
||||
repo := mock.NewRepository()
|
||||
expectedErr := errors.New("verifier cannot be nil")
|
||||
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), nil, repo, opts)
|
||||
|
||||
if err == nil || err.Error() != expectedErr.Error() {
|
||||
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("repo is nil", func(t *testing.T) {
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
expectedErr := errors.New("repo cannot be nil")
|
||||
|
||||
// mock the repository
|
||||
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
|
||||
_, _, err := Verify(context.Background(), &verifier, nil, opts)
|
||||
|
||||
if err == nil || err.Error() != expectedErr.Error() {
|
||||
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestVerifyBlobError(t *testing.T) {
|
||||
reader := strings.NewReader("some content")
|
||||
sig := []byte("signature")
|
||||
testCases := []struct {
|
||||
name string
|
||||
verifier BlobVerifier
|
||||
sig []byte
|
||||
rdr io.Reader
|
||||
ctMType string
|
||||
sigMType string
|
||||
errMsg string
|
||||
}{
|
||||
{"nilVerifier", nil, sig, reader, "video/mp4", jws.MediaTypeEnvelope, "blobVerifier cannot be nil"},
|
||||
{"verifierError", &dummyVerifier{FailVerify: true}, sig, reader, "video/mp4", jws.MediaTypeEnvelope, "failed verify"},
|
||||
{"nilSignature", &dummyVerifier{}, nil, reader, "video/mp4", jws.MediaTypeEnvelope, "signature cannot be nil or empty"},
|
||||
{"emptySignature", &dummyVerifier{}, []byte{}, reader, "video/mp4", jws.MediaTypeEnvelope, "signature cannot be nil or empty"},
|
||||
{"nilReader", &dummyVerifier{}, sig, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"},
|
||||
{"invalidContentType", &dummyVerifier{}, sig, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type \"video/mp4/zoping\": mime: unexpected content after media subtype"},
|
||||
{"invalidSigType", &dummyVerifier{}, sig, reader, "video/mp4", "hola!", "invalid signature media-type \"hola!\""},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
opts := VerifyBlobOptions{
|
||||
BlobVerifierVerifyOptions: BlobVerifierVerifyOptions{
|
||||
SignatureMediaType: tc.sigMType,
|
||||
UserMetadata: nil,
|
||||
TrustPolicyName: "",
|
||||
},
|
||||
ContentMediaType: tc.ctMType,
|
||||
}
|
||||
|
||||
_, _, err := VerifyBlob(context.Background(), tc.verifier, tc.rdr, tc.sig, opts)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error but didnt found")
|
||||
}
|
||||
if err.Error() != tc.errMsg {
|
||||
t.Fatalf("expected err message to be '%s' but found '%s'", tc.errMsg, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func dummyPolicyDocument() (policyDoc trustpolicy.Document) {
|
||||
policyDoc = trustpolicy.Document{
|
||||
func TestVerifyBlobValid(t *testing.T) {
|
||||
opts := VerifyBlobOptions{
|
||||
BlobVerifierVerifyOptions: BlobVerifierVerifyOptions{
|
||||
SignatureMediaType: jws.MediaTypeEnvelope,
|
||||
UserMetadata: nil,
|
||||
TrustPolicyName: "",
|
||||
},
|
||||
}
|
||||
|
||||
_, _, err := VerifyBlob(context.Background(), &dummyVerifier{}, strings.NewReader("some content"), []byte("signature"), opts)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error, but got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func dummyPolicyDocument() (policyDoc trustpolicy.OCIDocument) {
|
||||
policyDoc = trustpolicy.OCIDocument{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.TrustPolicy{dummyPolicyStatement()},
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{dummyPolicyStatement()},
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
|
||||
policyStatement = trustpolicy.TrustPolicy{
|
||||
func dummyPolicyStatement() (policyStatement trustpolicy.OCITrustPolicy) {
|
||||
policyStatement = trustpolicy.OCITrustPolicy{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
|
||||
|
@ -253,31 +617,59 @@ func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
|
|||
return
|
||||
}
|
||||
|
||||
type dummySigner struct{}
|
||||
type dummySigner struct {
|
||||
fail bool
|
||||
}
|
||||
|
||||
func (s *dummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
return []byte("ABC"), &signature.SignerInfo{}, nil
|
||||
func (s *dummySigner) Sign(_ context.Context, _ ocispec.Descriptor, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
return []byte("ABC"), &signature.SignerInfo{
|
||||
SignedAttributes: signature.SignedAttributes{
|
||||
SigningTime: time.Now(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *dummySigner) SignBlob(_ context.Context, descGenFunc BlobDescriptorGenerator, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
if s.fail {
|
||||
return nil, nil, errors.New("expected SignBlob failure")
|
||||
}
|
||||
|
||||
_, err := descGenFunc(digest.SHA384)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return []byte("ABC"), &signature.SignerInfo{
|
||||
SignedAttributes: signature.SignedAttributes{
|
||||
SigningTime: time.Now(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type verifyMetadataSigner struct{}
|
||||
|
||||
func (s *verifyMetadataSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
func (s *verifyMetadataSigner) Sign(_ context.Context, desc ocispec.Descriptor, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
for k, v := range expectedMetadata {
|
||||
if desc.Annotations[k] != v {
|
||||
return nil, nil, errors.New("expected metadata not present in descriptor")
|
||||
}
|
||||
}
|
||||
return []byte("ABC"), &signature.SignerInfo{}, nil
|
||||
return []byte("ABC"), &signature.SignerInfo{
|
||||
SignedAttributes: signature.SignedAttributes{
|
||||
SigningTime: time.Now(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type dummyVerifier struct {
|
||||
TrustPolicyDoc *trustpolicy.Document
|
||||
TrustPolicyDoc *trustpolicy.OCIDocument
|
||||
PluginManager plugin.Manager
|
||||
FailVerify bool
|
||||
VerificationLevel trustpolicy.VerificationLevel
|
||||
SkipVerification bool
|
||||
}
|
||||
|
||||
func (v *dummyVerifier) Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifyOptions) (*VerificationOutcome, error) {
|
||||
func (v *dummyVerifier) Verify(_ context.Context, _ ocispec.Descriptor, _ []byte, _ VerifierVerifyOptions) (*VerificationOutcome, error) {
|
||||
outcome := &VerificationOutcome{
|
||||
VerificationResults: []*ValidationResult{},
|
||||
VerificationLevel: &v.VerificationLevel,
|
||||
|
@ -287,3 +679,162 @@ func (v *dummyVerifier) Verify(ctx context.Context, desc ocispec.Descriptor, sig
|
|||
}
|
||||
return outcome, nil
|
||||
}
|
||||
|
||||
func (v *dummyVerifier) SkipVerify(_ context.Context, _ VerifierVerifyOptions) (bool, *trustpolicy.VerificationLevel, error) {
|
||||
if v.SkipVerification {
|
||||
return true, nil, nil
|
||||
}
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
func (v *dummyVerifier) VerifyBlob(_ context.Context, _ BlobDescriptorGenerator, _ []byte, _ BlobVerifierVerifyOptions) (*VerificationOutcome, error) {
|
||||
if v.FailVerify {
|
||||
return nil, errors.New("failed verify")
|
||||
}
|
||||
|
||||
return &VerificationOutcome{
|
||||
VerificationResults: []*ValidationResult{},
|
||||
VerificationLevel: &v.VerificationLevel,
|
||||
EnvelopeContent: &signature.EnvelopeContent{
|
||||
Payload: signature.Payload{
|
||||
Content: []byte("{}"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
reference = "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
|
||||
artifactReference = "local/oci-layout@sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
|
||||
signaturePath = filepath.FromSlash("./internal/testdata/cose_signature.sig")
|
||||
)
|
||||
|
||||
type ociDummySigner struct{}
|
||||
|
||||
func (s *ociDummySigner) Sign(_ context.Context, _ ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
sigBlob, err := os.ReadFile(signaturePath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
sigEnv, err := signature.ParseEnvelope(opts.SignatureMediaType, sigBlob)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
content, err := sigEnv.Content()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return sigBlob, &content.SignerInfo, nil
|
||||
}
|
||||
|
||||
func TestLocalContent(t *testing.T) {
|
||||
// create a temp OCI layout
|
||||
ociLayoutTestDataPath, err := filepath.Abs(filepath.Join("internal", "testdata", "oci-layout"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get oci layout path: %v", err)
|
||||
}
|
||||
newOCILayoutPath := t.TempDir()
|
||||
if err := ocilayout.Copy(ociLayoutTestDataPath, newOCILayoutPath, "v2"); err != nil {
|
||||
t.Fatalf("failed to create temp oci layout: %v", err)
|
||||
}
|
||||
repo, err := registry.NewOCIRepository(newOCILayoutPath, registry.RepositoryOptions{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("sign the local content", func(t *testing.T) {
|
||||
// sign the artifact
|
||||
signOpts := SignOptions{
|
||||
SignerSignOptions: SignerSignOptions{
|
||||
SignatureMediaType: cose.MediaTypeEnvelope,
|
||||
},
|
||||
ArtifactReference: reference,
|
||||
}
|
||||
_, err = Sign(context.Background(), &ociDummySigner{}, repo, signOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to Sign: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("verify local content", func(t *testing.T) {
|
||||
// verify the artifact
|
||||
verifyOpts := VerifyOptions{
|
||||
ArtifactReference: artifactReference,
|
||||
MaxSignatureAttempts: math.MaxInt64,
|
||||
}
|
||||
policyDocument := dummyPolicyDocument()
|
||||
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
|
||||
// verify signatures inside the OCI layout folder
|
||||
_, _, err = Verify(context.Background(), &verifier, repo, verifyOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to verify local content: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUserMetadata(t *testing.T) {
|
||||
t.Run("EnvelopeContent is nil", func(t *testing.T) {
|
||||
outcome := &VerificationOutcome{}
|
||||
_, err := outcome.UserMetadata()
|
||||
if err == nil {
|
||||
t.Fatal("expected an error, got nil")
|
||||
}
|
||||
if err.Error() != "unable to find envelope content for verification outcome" {
|
||||
t.Fatalf("expected error message 'unable to find envelope content for verification outcome', got '%s'", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EnvelopeContent is valid", func(t *testing.T) {
|
||||
payload := envelope.Payload{
|
||||
TargetArtifact: ocispec.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error marshaling payload: %v", err)
|
||||
}
|
||||
|
||||
outcome := &VerificationOutcome{
|
||||
EnvelopeContent: &signature.EnvelopeContent{
|
||||
Payload: signature.Payload{
|
||||
Content: payloadBytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
metadata, err := outcome.UserMetadata()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting user metadata: %v", err)
|
||||
}
|
||||
if len(metadata) != 1 || metadata["key"] != "value" {
|
||||
t.Fatalf("expected metadata map[key]=value, got %v", metadata)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Annotation is nil", func(t *testing.T) {
|
||||
payload := envelope.Payload{
|
||||
TargetArtifact: ocispec.Descriptor{},
|
||||
}
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error marshaling payload: %v", err)
|
||||
}
|
||||
|
||||
outcome := &VerificationOutcome{
|
||||
EnvelopeContent: &signature.EnvelopeContent{
|
||||
Payload: signature.Payload{
|
||||
Content: payloadBytes,
|
||||
},
|
||||
},
|
||||
}
|
||||
metadata, err := outcome.UserMetadata()
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error getting user metadata: %v", err)
|
||||
}
|
||||
if len(metadata) != 0 {
|
||||
t.Fatalf("expected empty metadata, got %v", metadata)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plugin
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrNotCompliant is returned by plugin methods when the response is not
|
||||
// compliant.
|
||||
var ErrNotCompliant = errors.New("plugin not compliant")
|
||||
|
||||
// ErrNotRegularFile is returned when the plugin file is not an regular file.
|
||||
var ErrNotRegularFile = errors.New("plugin executable file is not a regular file")
|
||||
|
||||
// PluginDowngradeError is returned when installing a plugin with version
|
||||
// lower than the exisiting plugin version.
|
||||
type PluginDowngradeError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
// Error returns the error message.
|
||||
func (e PluginDowngradeError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return "installing plugin with version lower than the existing plugin version"
|
||||
}
|
||||
|
||||
// InstallEqualVersionError is returned when installing a plugin with version
|
||||
// equal to the exisiting plugin version.
|
||||
type InstallEqualVersionError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
// Error returns the error message.
|
||||
func (e InstallEqualVersionError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return "installing plugin with version equal to the existing plugin version"
|
||||
}
|
||||
|
||||
// PluginMalformedError is used when there is an issue with plugin and
|
||||
// should be fixed by plugin developers.
|
||||
type PluginMalformedError struct {
|
||||
Msg string
|
||||
InnerError error
|
||||
}
|
||||
|
||||
// Error returns the error message.
|
||||
func (e PluginMalformedError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return e.InnerError.Error()
|
||||
}
|
||||
|
||||
// Unwrap returns the inner error.
|
||||
func (e PluginMalformedError) Unwrap() error {
|
||||
return e.InnerError
|
||||
}
|
||||
|
||||
// PluginDirectoryWalkError is used when there is an issue with plugins directory
|
||||
// and should suggest user to check the permission of plugin directory.
|
||||
type PluginDirectoryWalkError error
|
||||
|
||||
// PluginExecutableFileError is used when there is an issue with plugin
|
||||
// executable file and should suggest user to check the existence, permission
|
||||
// and platform/arch compatibility of plugin.
|
||||
type PluginExecutableFileError struct {
|
||||
Msg string
|
||||
InnerError error
|
||||
}
|
||||
|
||||
// Error returns the error message.
|
||||
func (e PluginExecutableFileError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return e.InnerError.Error()
|
||||
}
|
||||
|
||||
// Unwrap returns the inner error.
|
||||
func (e PluginExecutableFileError) Unwrap() error {
|
||||
return e.InnerError
|
||||
}
|
|
@ -1,3 +1,16 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
|
@ -10,16 +23,16 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
var exampleMetadata = proto.GetMetadataResponse{
|
||||
var exampleMetadata = plugin.GetMetadataResponse{
|
||||
Name: "foo",
|
||||
Description: "friendly",
|
||||
Version: "1",
|
||||
URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"},
|
||||
Capabilities: []proto.Capability{"cap"}}
|
||||
Capabilities: []plugin.Capability{"cap"}}
|
||||
|
||||
func preparePlugin(t *testing.T) string {
|
||||
root := t.TempDir()
|
||||
|
@ -74,11 +87,11 @@ func TestIntegration(t *testing.T) {
|
|||
}
|
||||
|
||||
// validate and create
|
||||
plugin, err := mgr.Get(context.Background(), "foo")
|
||||
pl, err := mgr.Get(context.Background(), "foo")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
metadata, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
|
||||
metadata, err := pl.GetMetadata(context.Background(), &plugin.GetMetadataRequest{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
@ -1,28 +1,41 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-go/internal/file"
|
||||
"github.com/notaryproject/notation-go/internal/semver"
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
// ErrNotCompliant is returned by plugin methods when the response is not
|
||||
// compliant.
|
||||
var ErrNotCompliant = errors.New("plugin not compliant")
|
||||
|
||||
// ErrNotRegularFile is returned when the plugin file is not an regular file.
|
||||
var ErrNotRegularFile = errors.New("not regular file")
|
||||
|
||||
// Manager manages plugins installed on the system.
|
||||
type Manager interface {
|
||||
Get(ctx context.Context, name string) (Plugin, error)
|
||||
Get(ctx context.Context, name string) (plugin.Plugin, error)
|
||||
List(ctx context.Context) ([]string, error)
|
||||
}
|
||||
|
||||
// CLIManager implements Manager
|
||||
// CLIManager implements [Manager]
|
||||
type CLIManager struct {
|
||||
pluginFS dir.SysFS
|
||||
}
|
||||
|
@ -35,7 +48,7 @@ func NewCLIManager(pluginFS dir.SysFS) *CLIManager {
|
|||
// Get returns a plugin on the system by its name.
|
||||
//
|
||||
// If the plugin is not found, the error is of type os.ErrNotExist.
|
||||
func (m *CLIManager) Get(ctx context.Context, name string) (Plugin, error) {
|
||||
func (m *CLIManager) Get(ctx context.Context, name string) (plugin.Plugin, error) {
|
||||
pluginPath := path.Join(name, binName(name))
|
||||
path, err := m.pluginFS.SysPath(pluginPath)
|
||||
if err != nil {
|
||||
|
@ -49,8 +62,11 @@ func (m *CLIManager) Get(ctx context.Context, name string) (Plugin, error) {
|
|||
// List produces a list of the plugin names on the system.
|
||||
func (m *CLIManager) List(ctx context.Context) ([]string, error) {
|
||||
var plugins []string
|
||||
fs.WalkDir(m.pluginFS, ".", func(dir string, d fs.DirEntry, err error) error {
|
||||
if err := fs.WalkDir(m.pluginFS, ".", func(dir string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if dir == "." {
|
||||
|
@ -66,6 +82,215 @@ func (m *CLIManager) List(ctx context.Context) ([]string, error) {
|
|||
// add plugin name
|
||||
plugins = append(plugins, d.Name())
|
||||
return fs.SkipDir
|
||||
})
|
||||
}); err != nil {
|
||||
return nil, PluginDirectoryWalkError(fmt.Errorf("failed to list plugin: %w", err))
|
||||
}
|
||||
return plugins, nil
|
||||
}
|
||||
|
||||
// CLIInstallOptions provides user customized options for plugin installation
|
||||
type CLIInstallOptions struct {
|
||||
// PluginPath can be path of:
|
||||
//
|
||||
// 1. A directory which contains plugin related files. Sub-directories are
|
||||
// ignored. It MUST contain one and only one valid plugin executable file
|
||||
// following spec: https://github.com/notaryproject/specifications/blob/v1.0.0/specs/plugin-extensibility.md#installation
|
||||
// It may contain extra lib files and LICENSE files.
|
||||
// On success, these files will be installed as well.
|
||||
//
|
||||
// 2. A single plugin executable file following the spec.
|
||||
PluginPath string
|
||||
|
||||
// Overwrite is a boolean flag. When set, always install the new plugin.
|
||||
Overwrite bool
|
||||
}
|
||||
|
||||
// Install installs a plugin to the system. It returns existing
|
||||
// plugin metadata, new plugin metadata, and error. It returns nil error
|
||||
// if and only if the installation succeeded.
|
||||
//
|
||||
// If plugin does not exist, directly install the new plugin.
|
||||
//
|
||||
// If plugin already exists:
|
||||
//
|
||||
// If overwrite is not set, then the new plugin
|
||||
// version MUST be higher than the existing plugin version.
|
||||
//
|
||||
// If overwrite is set, version check is skipped. If existing
|
||||
// plugin is malfunctioning, it will be overwritten.
|
||||
func (m *CLIManager) Install(ctx context.Context, installOpts CLIInstallOptions) (*plugin.GetMetadataResponse, *plugin.GetMetadataResponse, error) {
|
||||
// initialization
|
||||
logger := log.GetLogger(ctx)
|
||||
overwrite := installOpts.Overwrite
|
||||
if installOpts.PluginPath == "" {
|
||||
return nil, nil, errors.New("plugin source path cannot be empty")
|
||||
}
|
||||
logger.Debugf("Installing plugin from path %s", installOpts.PluginPath)
|
||||
var installFromNonDir bool
|
||||
pluginExecutableFile, pluginName, err := parsePluginFromDir(ctx, installOpts.PluginPath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, file.ErrNotDirectory) {
|
||||
return nil, nil, fmt.Errorf("failed to read plugin from input directory: %w", err)
|
||||
}
|
||||
// input is not a dir, check if it's a single plugin executable file
|
||||
installFromNonDir = true
|
||||
pluginExecutableFile = installOpts.PluginPath
|
||||
pluginExecutableFileName := filepath.Base(pluginExecutableFile)
|
||||
pluginName, err = parsePluginName(pluginExecutableFileName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read plugin name from input file %s: %w", pluginExecutableFileName, err)
|
||||
}
|
||||
isExec, err := isExecutableFile(pluginExecutableFile)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to check if input file %s is executable: %w", pluginExecutableFileName, err)
|
||||
}
|
||||
if !isExec {
|
||||
return nil, nil, fmt.Errorf("input file %s is not executable", pluginExecutableFileName)
|
||||
}
|
||||
}
|
||||
// validate and get new plugin metadata
|
||||
newPlugin, err := NewCLIPlugin(ctx, pluginName, pluginExecutableFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
newPluginMetadata, err := newPlugin.GetMetadata(ctx, &plugin.GetMetadataRequest{})
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get metadata of new plugin: %w", err)
|
||||
}
|
||||
// check plugin existence and get existing plugin metadata
|
||||
var existingPluginMetadata *plugin.GetMetadataResponse
|
||||
existingPlugin, err := m.Get(ctx, pluginName)
|
||||
if err != nil {
|
||||
// fail only if overwrite is not set
|
||||
if !errors.Is(err, os.ErrNotExist) && !overwrite {
|
||||
return nil, nil, fmt.Errorf("failed to check plugin existence: %w", err)
|
||||
}
|
||||
} else { // plugin already exists
|
||||
existingPluginMetadata, err = existingPlugin.GetMetadata(ctx, &plugin.GetMetadataRequest{})
|
||||
if err != nil && !overwrite { // fail only if overwrite is not set
|
||||
return nil, nil, fmt.Errorf("failed to get metadata of existing plugin: %w", err)
|
||||
}
|
||||
// existing plugin is valid, and overwrite is not set, check version
|
||||
if !overwrite {
|
||||
comp, err := semver.ComparePluginVersion(newPluginMetadata.Version, existingPluginMetadata.Version)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to compare plugin versions: %w", err)
|
||||
}
|
||||
switch {
|
||||
case comp < 0:
|
||||
return nil, nil, PluginDowngradeError{Msg: fmt.Sprintf("failed to install plugin %s. The installing plugin version %s is lower than the existing plugin version %s", pluginName, newPluginMetadata.Version, existingPluginMetadata.Version)}
|
||||
case comp == 0:
|
||||
return nil, nil, InstallEqualVersionError{Msg: fmt.Sprintf("plugin %s with version %s already exists", pluginName, existingPluginMetadata.Version)}
|
||||
}
|
||||
}
|
||||
}
|
||||
// clean up before installation, this guarantees idempotent for install
|
||||
if err := m.Uninstall(ctx, pluginName); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil, fmt.Errorf("failed to clean up plugin %s before installation: %w", pluginName, err)
|
||||
}
|
||||
}
|
||||
// core process
|
||||
pluginDirPath, err := m.pluginFS.SysPath(pluginName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get the system path of plugin %s: %w", pluginName, err)
|
||||
}
|
||||
if installFromNonDir {
|
||||
if err := file.CopyToDir(pluginExecutableFile, pluginDirPath); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to copy plugin executable file from %s to %s: %w", pluginExecutableFile, pluginDirPath, err)
|
||||
}
|
||||
} else {
|
||||
if err := file.CopyDirToDir(installOpts.PluginPath, pluginDirPath); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to copy plugin files from %s to %s: %w", installOpts.PluginPath, pluginDirPath, err)
|
||||
}
|
||||
}
|
||||
return existingPluginMetadata, newPluginMetadata, nil
|
||||
}
|
||||
|
||||
// Uninstall uninstalls a plugin on the system by its name.
|
||||
// If the plugin dir does not exist, os.ErrNotExist is returned.
|
||||
func (m *CLIManager) Uninstall(ctx context.Context, name string) error {
|
||||
pluginDirPath, err := m.pluginFS.SysPath(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(pluginDirPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.RemoveAll(pluginDirPath)
|
||||
}
|
||||
|
||||
// parsePluginFromDir checks if a dir is a valid plugin dir which contains
|
||||
// one and only one plugin executable file candidate.
|
||||
// The dir may contain extra lib files and LICENSE files.
|
||||
// Sub-directories are ignored.
|
||||
//
|
||||
// On success, the plugin executable file path, plugin name and
|
||||
// nil error are returned.
|
||||
func parsePluginFromDir(ctx context.Context, path string) (string, string, error) {
|
||||
// sanity check
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if !fi.Mode().IsDir() {
|
||||
return "", "", file.ErrNotDirectory
|
||||
}
|
||||
logger := log.GetLogger(ctx)
|
||||
// walk the path
|
||||
var pluginExecutableFile, pluginName, candidatePluginName string
|
||||
var foundPluginExecutableFile bool
|
||||
var filesWithValidNameFormat []string
|
||||
if err := filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// skip sub-directories
|
||||
if d.IsDir() && d.Name() != filepath.Base(path) {
|
||||
return fs.SkipDir
|
||||
}
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// only take regular files
|
||||
if info.Mode().IsRegular() {
|
||||
if candidatePluginName, err = parsePluginName(d.Name()); err != nil {
|
||||
// file name does not follow the notation-{plugin-name} format,
|
||||
// continue
|
||||
return nil
|
||||
}
|
||||
filesWithValidNameFormat = append(filesWithValidNameFormat, p)
|
||||
isExec, err := isExecutableFile(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isExec {
|
||||
return nil
|
||||
}
|
||||
if foundPluginExecutableFile {
|
||||
return errors.New("found more than one plugin executable files")
|
||||
}
|
||||
foundPluginExecutableFile = true
|
||||
pluginExecutableFile = p
|
||||
pluginName = candidatePluginName
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if !foundPluginExecutableFile {
|
||||
// if no executable file was found, but there's one and only one
|
||||
// potential candidate, try install the candidate
|
||||
if len(filesWithValidNameFormat) == 1 {
|
||||
candidate := filesWithValidNameFormat[0]
|
||||
if err := setExecutable(candidate); err != nil {
|
||||
return "", "", fmt.Errorf("no plugin executable file was found: %w", err)
|
||||
}
|
||||
logger.Warnf("Found candidate plugin executable file %q without executable permission. Setting user executable bit and trying to install.", filepath.Base(candidate))
|
||||
return candidate, candidatePluginName, nil
|
||||
}
|
||||
return "", "", errors.New("no plugin executable file was found")
|
||||
}
|
||||
return pluginExecutableFile, pluginName, nil
|
||||
}
|
||||
|
|
|
@ -1,10 +1,26 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
|
@ -22,8 +38,50 @@ func (t testCommander) Output(ctx context.Context, path string, command proto.Co
|
|||
return t.stdout, t.stderr, t.err
|
||||
}
|
||||
|
||||
type testInstallCommander struct {
|
||||
existedPluginFilePath string
|
||||
existedPluginStdout []byte
|
||||
existedPluginStderr []byte
|
||||
existedPluginErr error
|
||||
newPluginFilePath string
|
||||
newPluginStdout []byte
|
||||
newPluginStderr []byte
|
||||
newPluginErr error
|
||||
err error
|
||||
}
|
||||
|
||||
func (t testInstallCommander) Output(ctx context.Context, path string, command proto.Command, req []byte) ([]byte, []byte, error) {
|
||||
if path == t.existedPluginFilePath {
|
||||
return t.existedPluginStdout, t.existedPluginStderr, t.existedPluginErr
|
||||
}
|
||||
if path == t.newPluginFilePath {
|
||||
return t.newPluginStdout, t.newPluginStderr, t.newPluginErr
|
||||
}
|
||||
return nil, nil, t.err
|
||||
}
|
||||
|
||||
var validMetadata = proto.GetMetadataResponse{
|
||||
Name: "foo", Description: "friendly", Version: "1", URL: "example.com",
|
||||
Name: "foo", Description: "friendly", Version: "1.0.0", URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
|
||||
}
|
||||
|
||||
var validMetadataHigherVersion = proto.GetMetadataResponse{
|
||||
Name: "foo", Description: "friendly", Version: "1.1.0", URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
|
||||
}
|
||||
|
||||
var validMetadataLowerVersion = proto.GetMetadataResponse{
|
||||
Name: "foo", Description: "friendly", Version: "0.1.0", URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
|
||||
}
|
||||
|
||||
var validMetadataBar = proto.GetMetadataResponse{
|
||||
Name: "bar", Description: "friendly", Version: "1.0.0", URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
|
||||
}
|
||||
|
||||
var validMetadataBarExample = proto.GetMetadataResponse{
|
||||
Name: "bar.example.plugin", Description: "friendly", Version: "1.0.0", URL: "example.com",
|
||||
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
|
||||
}
|
||||
|
||||
|
@ -38,6 +96,9 @@ var invalidContractVersionMetadata = proto.GetMetadataResponse{
|
|||
}
|
||||
|
||||
func TestManager_Get(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
executor = testCommander{stdout: metadataJSON(validMetadata)}
|
||||
mgr := NewCLIManager(mockfs.NewSysFSWithRootMock(fstest.MapFS{}, "./testdata/plugins"))
|
||||
_, err := mgr.Get(context.Background(), "foo")
|
||||
|
@ -74,6 +135,533 @@ func TestManager_List(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestManager_Install(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
|
||||
newPluginFilePath := "testdata/foo/notation-foo"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mgr := NewCLIManager(mockfs.NewSysFSWithRootMock(fstest.MapFS{}, "testdata/plugins"))
|
||||
|
||||
t.Run("success install with higher version", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataHigherVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting error to be nil, but got %v", err)
|
||||
}
|
||||
if existingPluginMetadata.Version != validMetadata.Version {
|
||||
t.Fatalf("existing plugin version mismatch, existing plugin version: %s, but got: %s", validMetadata.Version, existingPluginMetadata.Version)
|
||||
}
|
||||
if newPluginMetadata.Version != validMetadataHigherVersion.Version {
|
||||
t.Fatalf("new plugin version mismatch, new plugin version: %s, but got: %s", validMetadataHigherVersion.Version, newPluginMetadata.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success install with lower version and overwrite", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataLowerVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
Overwrite: true,
|
||||
}
|
||||
if _, _, err := mgr.Install(context.Background(), installOpts); err != nil {
|
||||
t.Fatalf("expecting error to be nil, but got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success install without existing plugin", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-bar"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBar),
|
||||
}
|
||||
defer mgr.Uninstall(context.Background(), "bar")
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting error to be nil, but got %v", err)
|
||||
}
|
||||
if existingPluginMetadata != nil {
|
||||
t.Fatalf("expecting existingPluginMetadata to be nil, but got %v", existingPluginMetadata)
|
||||
}
|
||||
if newPluginMetadata.Version != validMetadataBar.Version {
|
||||
t.Fatalf("new plugin version mismatch, new plugin version: %s, but got: %s", validMetadataBar.Version, newPluginMetadata.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success install with file extension", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-bar.example.plugin"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBarExample),
|
||||
}
|
||||
defer mgr.Uninstall(context.Background(), "bar.example.plugin")
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting error to be nil, but got %v", err)
|
||||
}
|
||||
if existingPluginMetadata != nil {
|
||||
t.Fatalf("expecting existingPluginMetadata to be nil, but got %v", existingPluginMetadata)
|
||||
}
|
||||
if newPluginMetadata.Version != validMetadataBar.Version {
|
||||
t.Fatalf("new plugin version mismatch, new plugin version: %s, but got: %s", validMetadataBar.Version, newPluginMetadata.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to equal version", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadata),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "plugin foo with version 1.0.0 already exists"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to lower version", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataLowerVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to install plugin foo. The installing plugin version 0.1.0 is lower than the existing plugin version 1.0.0"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to wrong plugin executable file name format", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/bar"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBar),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to read plugin name from input file bar: invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got bar"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to plugin executable file name missing plugin name", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBar),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to read plugin name from input file notation-: invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got notation-"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to wrong plugin file permission", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-bar"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBar),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "input file notation-bar is not executable"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to new plugin executable file does not exist", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-bar"
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(validMetadataBar),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to read plugin from input directory: stat testdata/bar/notation-bar: no such file or directory"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to invalid new plugin metadata", func(t *testing.T) {
|
||||
newPluginFilePath := "testdata/bar/notation-bar"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
newPluginStdout: metadataJSON(invalidMetadataName),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to get metadata of new plugin: plugin executable file name must be \"notation-foobar\" instead of \"notation-bar\""
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install due to invalid existing plugin metadata", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadataBar),
|
||||
newPluginStdout: metadataJSON(validMetadata),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
}
|
||||
expectedErrorMsg := "failed to get metadata of existing plugin: plugin executable file name must be \"notation-bar\" instead of \"notation-foo\""
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success to install with overwrite and invalid existing plugin metadata", func(t *testing.T) {
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadataBar),
|
||||
newPluginStdout: metadataJSON(validMetadata),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginFilePath,
|
||||
Overwrite: true,
|
||||
}
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting error to be nil, but got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success to install from plugin dir", func(t *testing.T) {
|
||||
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
|
||||
newPluginFilePath := "testdata/foo/notation-foo"
|
||||
newPluginLibPath := "testdata/foo/notation-libfoo"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := createFileAndChmod(newPluginLibPath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataHigherVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginDir,
|
||||
}
|
||||
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting nil error, but got %v", err)
|
||||
}
|
||||
if existingPluginMetadata.Version != "1.0.0" {
|
||||
t.Fatalf("expecting existing plugin metadata to be 1.0.0, but got %s", existingPluginMetadata.Version)
|
||||
}
|
||||
if newPluginMetadata.Version != "1.1.0" {
|
||||
t.Fatalf("expecting new plugin metadata to be 1.1.0, but got %s", newPluginMetadata.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success to install from plugin dir with no executable file and one valid candidate file", func(t *testing.T) {
|
||||
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
|
||||
newPluginFilePath := "testdata/foo/notation-foo"
|
||||
newPluginLibPath := "testdata/foo/libfoo"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := createFileAndChmod(newPluginLibPath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataHigherVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginDir,
|
||||
}
|
||||
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("expecting nil error, but got %v", err)
|
||||
}
|
||||
if existingPluginMetadata.Version != "1.0.0" {
|
||||
t.Fatalf("expecting existing plugin metadata to be 1.0.0, but got %s", existingPluginMetadata.Version)
|
||||
}
|
||||
if newPluginMetadata.Version != "1.1.0" {
|
||||
t.Fatalf("expecting new plugin metadata to be 1.1.0, but got %s", newPluginMetadata.Version)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install from plugin dir due to more than one candidate plugin executable files", func(t *testing.T) {
|
||||
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
|
||||
newPluginFilePath := "testdata/foo/notation-foo1"
|
||||
newPluginFilePath2 := "testdata/foo/notation-foo2"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := createFileAndChmod(newPluginFilePath2, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataHigherVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginDir,
|
||||
}
|
||||
expectedErrorMsg := "failed to read plugin from input directory: no plugin executable file was found"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fail to install from plugin dir due to more than one plugin executable files", func(t *testing.T) {
|
||||
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
|
||||
newPluginFilePath := "testdata/foo/notation-foo1"
|
||||
newPluginFilePath2 := "testdata/foo/notation-foo2"
|
||||
newPluginDir := filepath.Dir(newPluginFilePath)
|
||||
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
|
||||
t.Fatalf("failed to create %s: %v", newPluginDir, err)
|
||||
}
|
||||
defer os.RemoveAll(newPluginDir)
|
||||
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := createFileAndChmod(newPluginFilePath2, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
executor = testInstallCommander{
|
||||
existedPluginFilePath: existedPluginFilePath,
|
||||
newPluginFilePath: newPluginFilePath,
|
||||
existedPluginStdout: metadataJSON(validMetadata),
|
||||
newPluginStdout: metadataJSON(validMetadataHigherVersion),
|
||||
}
|
||||
installOpts := CLIInstallOptions{
|
||||
PluginPath: newPluginDir,
|
||||
}
|
||||
expectedErrorMsg := "failed to read plugin from input directory: found more than one plugin executable files"
|
||||
_, _, err := mgr.Install(context.Background(), installOpts)
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestManager_Uninstall(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
executor = testCommander{stdout: metadataJSON(validMetadata)}
|
||||
mgr := NewCLIManager(mockfs.NewSysFSWithRootMock(fstest.MapFS{}, "./testdata/plugins"))
|
||||
if err := os.MkdirAll("./testdata/plugins/toUninstall", 0777); err != nil {
|
||||
t.Fatalf("failed to create toUninstall dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll("./testdata/plugins/toUninstall")
|
||||
pluginFile, err := os.Create("./testdata/plugins/toUninstall/toUninstall")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create toUninstall file: %v", err)
|
||||
}
|
||||
if err := pluginFile.Close(); err != nil {
|
||||
t.Fatalf("failed to close toUninstall file: %v", err)
|
||||
}
|
||||
// test uninstall valid plugin
|
||||
if err := mgr.Uninstall(context.Background(), "toUninstall"); err != nil {
|
||||
t.Fatalf("Manager.Uninstall() err %v, want nil", err)
|
||||
}
|
||||
// test uninstall non-exist plugin
|
||||
expectedErrorMsg := "stat testdata/plugins/non-exist: no such file or directory"
|
||||
if err := mgr.Uninstall(context.Background(), "non-exist"); err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("Manager.Uninstall() err %v, want %s", err, expectedErrorMsg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePluginName(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
pluginName, err := parsePluginName("notation-my-plugin.exe")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil err, but got %v", err)
|
||||
}
|
||||
if pluginName != "my-plugin" {
|
||||
t.Fatalf("expected plugin name my-plugin, but got %s", pluginName)
|
||||
}
|
||||
|
||||
expectedErrorMsg := "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got notation-com.plugin"
|
||||
_, err = parsePluginName("notation-com.plugin")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
|
||||
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got my-plugin.exe"
|
||||
_, err = parsePluginName("my-plugin.exe")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
|
||||
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got notation-.exe"
|
||||
_, err = parsePluginName("notation-.exe")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
|
||||
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got my-plugin"
|
||||
_, err = parsePluginName("my-plugin")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
} else {
|
||||
pluginName, err := parsePluginName("notation-my-plugin")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil err, but got %v", err)
|
||||
}
|
||||
if pluginName != "my-plugin" {
|
||||
t.Fatalf("expected plugin name my-plugin, but got %s", pluginName)
|
||||
}
|
||||
|
||||
pluginName, err = parsePluginName("notation-com.example.plugin")
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil err, but got %v", err)
|
||||
}
|
||||
if pluginName != "com.example.plugin" {
|
||||
t.Fatalf("expected plugin name com.example.plugin, but got %s", pluginName)
|
||||
}
|
||||
|
||||
expectedErrorMsg := "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got myPlugin"
|
||||
_, err = parsePluginName("myPlugin")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
|
||||
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got my-plugin"
|
||||
_, err = parsePluginName("my-plugin")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
|
||||
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got notation-"
|
||||
_, err = parsePluginName("notation-")
|
||||
if err == nil || err.Error() != expectedErrorMsg {
|
||||
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func metadataJSON(m proto.GetMetadataResponse) []byte {
|
||||
d, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
|
@ -81,3 +669,14 @@ func metadataJSON(m proto.GetMetadataResponse) []byte {
|
|||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func createFileAndChmod(path string, mode fs.FileMode) error {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.Chmod(mode); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
|
|
@ -1,10 +1,61 @@
|
|||
// 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.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package plugin
|
||||
|
||||
import "github.com/notaryproject/notation-go/plugin/proto"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
func binName(name string) string {
|
||||
return proto.Prefix + name
|
||||
return plugin.BinaryPrefix + name
|
||||
}
|
||||
|
||||
// isExecutableFile checks if a file at filePath is user executable
|
||||
func isExecutableFile(filePath string) (bool, error) {
|
||||
fi, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
mode := fi.Mode()
|
||||
if !mode.IsRegular() {
|
||||
return false, ErrNotRegularFile
|
||||
}
|
||||
return mode.Perm()&0100 != 0, nil
|
||||
}
|
||||
|
||||
// parsePluginName checks if fileName is a valid plugin file name
|
||||
// and gets plugin name from it based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation
|
||||
func parsePluginName(fileName string) (string, error) {
|
||||
pluginName, found := strings.CutPrefix(fileName, plugin.BinaryPrefix)
|
||||
if !found || pluginName == "" {
|
||||
return "", fmt.Errorf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got %s", fileName)
|
||||
}
|
||||
return pluginName, nil
|
||||
}
|
||||
|
||||
// setExecutable sets file to be user executable
|
||||
func setExecutable(filePath string) error {
|
||||
fileInfo, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(filePath, fileInfo.Mode()|os.FileMode(0100))
|
||||
}
|
||||
|
|
|
@ -1,7 +1,60 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plugin
|
||||
|
||||
import "github.com/notaryproject/notation-go/plugin/proto"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-go/internal/file"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
func binName(name string) string {
|
||||
return proto.Prefix + name + ".exe"
|
||||
return plugin.BinaryPrefix + name + ".exe"
|
||||
}
|
||||
|
||||
// isExecutableFile checks if a file at filePath is executable
|
||||
func isExecutableFile(filePath string) (bool, error) {
|
||||
fi, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
return false, ErrNotRegularFile
|
||||
}
|
||||
return strings.EqualFold(filepath.Ext(filepath.Base(filePath)), ".exe"), nil
|
||||
}
|
||||
|
||||
// parsePluginName checks if fileName is a valid plugin file name
|
||||
// and gets plugin name from it based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation
|
||||
func parsePluginName(fileName string) (string, error) {
|
||||
if !strings.EqualFold(filepath.Ext(fileName), ".exe") {
|
||||
return "", fmt.Errorf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got %s", fileName)
|
||||
}
|
||||
fname := file.TrimFileExtension(fileName)
|
||||
pluginName, found := strings.CutPrefix(fname, plugin.BinaryPrefix)
|
||||
if !found || pluginName == "" {
|
||||
return "", fmt.Errorf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got %s", fileName)
|
||||
}
|
||||
return pluginName, nil
|
||||
}
|
||||
|
||||
// setExecutable returns error on Windows. User needs to install the correct
|
||||
// plugin file.
|
||||
func setExecutable(filePath string) error {
|
||||
return fmt.Errorf(`plugin executable file must have file extension ".exe", but got %q`, filepath.Base(filePath))
|
||||
}
|
||||
|
|
178
plugin/plugin.go
178
plugin/plugin.go
|
@ -1,4 +1,17 @@
|
|||
// Package plugin provides the toolings to use the notation plugin.
|
||||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package plugin provides the tooling to use the notation plugin.
|
||||
//
|
||||
// includes a CLIManager and a CLIPlugin implementation.
|
||||
package plugin
|
||||
|
@ -12,63 +25,58 @@ import (
|
|||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/notaryproject/notation-go/internal/io"
|
||||
"github.com/notaryproject/notation-go/internal/slices"
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
// maxPluginOutputSize is the maximum size of the plugin output.
|
||||
const maxPluginOutputSize = 64 * 1024 * 1024 // 64 MiB
|
||||
|
||||
var executor commander = &execCommander{} // for unit test
|
||||
|
||||
// GenericPlugin is the base requirement to be an plugin.
|
||||
type GenericPlugin interface {
|
||||
// GetMetadata returns the metadata information of the plugin.
|
||||
GetMetadata(ctx context.Context, req *proto.GetMetadataRequest) (*proto.GetMetadataResponse, error)
|
||||
}
|
||||
// GenericPlugin is the base requirement to be a plugin.
|
||||
//
|
||||
// Deprecated: GenericPlugin exists for historical compatibility and should not be used.
|
||||
// To access GenericPlugin, use the notation-plugin-framework-go's plugin.GenericPlugin type.
|
||||
type GenericPlugin = plugin.GenericPlugin
|
||||
|
||||
// SignPlugin defines the required methods to be a SignPlugin.
|
||||
type SignPlugin interface {
|
||||
GenericPlugin
|
||||
|
||||
// DescribeKey returns the KeySpec of a key.
|
||||
DescribeKey(ctx context.Context, req *proto.DescribeKeyRequest) (*proto.DescribeKeyResponse, error)
|
||||
|
||||
// GenerateSignature generates the raw signature based on the request.
|
||||
GenerateSignature(ctx context.Context, req *proto.GenerateSignatureRequest) (*proto.GenerateSignatureResponse, error)
|
||||
|
||||
// GenerateEnvelope generates the Envelope with signature based on the
|
||||
// request.
|
||||
GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error)
|
||||
}
|
||||
//
|
||||
// Deprecated: SignPlugin exists for historical compatibility and should not be used.
|
||||
// To access SignPlugin, use the notation-plugin-framework-go's plugin.SignPlugin type.
|
||||
type SignPlugin = plugin.SignPlugin
|
||||
|
||||
// VerifyPlugin defines the required method to be a VerifyPlugin.
|
||||
type VerifyPlugin interface {
|
||||
GenericPlugin
|
||||
//
|
||||
// Deprecated: VerifyPlugin exists for historical compatibility and should not be used.
|
||||
// To access VerifyPlugin, use the notation-plugin-framework-go's plugin.VerifyPlugin type.
|
||||
type VerifyPlugin = plugin.VerifyPlugin
|
||||
|
||||
// VerifySignature validates the signature based on the request.
|
||||
VerifySignature(ctx context.Context, req *proto.VerifySignatureRequest) (*proto.VerifySignatureResponse, error)
|
||||
}
|
||||
// Plugin defines required methods to be a Plugin.
|
||||
//
|
||||
// Deprecated: Plugin exists for historical compatibility and should not be used.
|
||||
// To access Plugin, use the notation-plugin-framework-go's plugin.Plugin type.
|
||||
type Plugin = plugin.Plugin
|
||||
|
||||
// Plugin defines required methods to be an Plugin.
|
||||
type Plugin interface {
|
||||
SignPlugin
|
||||
VerifyPlugin
|
||||
}
|
||||
|
||||
// CLIPlugin implements Plugin interface to CLI plugins.
|
||||
// CLIPlugin implements [Plugin] interface to CLI plugins.
|
||||
type CLIPlugin struct {
|
||||
name string
|
||||
path string
|
||||
}
|
||||
|
||||
// NewCLIPlugin validate the metadata of the plugin and return a *CLIPlugin.
|
||||
// NewCLIPlugin returns a *CLIPlugin.
|
||||
func NewCLIPlugin(ctx context.Context, name, path string) (*CLIPlugin, error) {
|
||||
// validate file existence
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil {
|
||||
// Ignore any file which we cannot Stat
|
||||
// (e.g. due to permissions or anything else).
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("plugin executable file is either not found or inaccessible: %w", err)
|
||||
}
|
||||
if !fi.Mode().IsRegular() {
|
||||
// Ignore non-regular files.
|
||||
|
@ -76,26 +84,28 @@ func NewCLIPlugin(ctx context.Context, name, path string) (*CLIPlugin, error) {
|
|||
}
|
||||
|
||||
// generate plugin
|
||||
plugin := CLIPlugin{
|
||||
return &CLIPlugin{
|
||||
name: name,
|
||||
path: path,
|
||||
}
|
||||
return &plugin, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetMetadata returns the metadata information of the plugin.
|
||||
func (p *CLIPlugin) GetMetadata(ctx context.Context, req *proto.GetMetadataRequest) (*proto.GetMetadataResponse, error) {
|
||||
var metadata proto.GetMetadataResponse
|
||||
func (p *CLIPlugin) GetMetadata(ctx context.Context, req *plugin.GetMetadataRequest) (*plugin.GetMetadataResponse, error) {
|
||||
var metadata plugin.GetMetadataResponse
|
||||
err := run(ctx, p.name, p.path, req, &metadata)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// validate metadata
|
||||
if err = validate(&metadata); err != nil {
|
||||
return nil, fmt.Errorf("invalid metadata: %w", err)
|
||||
return nil, &PluginMalformedError{
|
||||
Msg: fmt.Sprintf("metadata validation failed for plugin %s: %s", p.name, err),
|
||||
InnerError: err,
|
||||
}
|
||||
}
|
||||
if metadata.Name != p.name {
|
||||
return nil, fmt.Errorf("executable name must be %q instead of %q", binName(metadata.Name), filepath.Base(p.path))
|
||||
return nil, fmt.Errorf("plugin executable file name must be %q instead of %q", binName(metadata.Name), filepath.Base(p.path))
|
||||
}
|
||||
return &metadata, nil
|
||||
}
|
||||
|
@ -103,12 +113,12 @@ func (p *CLIPlugin) GetMetadata(ctx context.Context, req *proto.GetMetadataReque
|
|||
// DescribeKey returns the KeySpec of a key.
|
||||
//
|
||||
// if ContractVersion is not set, it will be set by the function.
|
||||
func (p *CLIPlugin) DescribeKey(ctx context.Context, req *proto.DescribeKeyRequest) (*proto.DescribeKeyResponse, error) {
|
||||
func (p *CLIPlugin) DescribeKey(ctx context.Context, req *plugin.DescribeKeyRequest) (*plugin.DescribeKeyResponse, error) {
|
||||
if req.ContractVersion == "" {
|
||||
req.ContractVersion = proto.ContractVersion
|
||||
req.ContractVersion = plugin.ContractVersion
|
||||
}
|
||||
|
||||
var resp proto.DescribeKeyResponse
|
||||
var resp plugin.DescribeKeyResponse
|
||||
err := run(ctx, p.name, p.path, req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
@ -116,12 +126,12 @@ func (p *CLIPlugin) DescribeKey(ctx context.Context, req *proto.DescribeKeyReque
|
|||
// GenerateSignature generates the raw signature based on the request.
|
||||
//
|
||||
// if ContractVersion is not set, it will be set by the function.
|
||||
func (p *CLIPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateSignatureRequest) (*proto.GenerateSignatureResponse, error) {
|
||||
func (p *CLIPlugin) GenerateSignature(ctx context.Context, req *plugin.GenerateSignatureRequest) (*plugin.GenerateSignatureResponse, error) {
|
||||
if req.ContractVersion == "" {
|
||||
req.ContractVersion = proto.ContractVersion
|
||||
req.ContractVersion = plugin.ContractVersion
|
||||
}
|
||||
|
||||
var resp proto.GenerateSignatureResponse
|
||||
var resp plugin.GenerateSignatureResponse
|
||||
err := run(ctx, p.name, p.path, req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
@ -129,12 +139,12 @@ func (p *CLIPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateSi
|
|||
// GenerateEnvelope generates the Envelope with signature based on the request.
|
||||
//
|
||||
// if ContractVersion is not set, it will be set by the function.
|
||||
func (p *CLIPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error) {
|
||||
func (p *CLIPlugin) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) {
|
||||
if req.ContractVersion == "" {
|
||||
req.ContractVersion = proto.ContractVersion
|
||||
req.ContractVersion = plugin.ContractVersion
|
||||
}
|
||||
|
||||
var resp proto.GenerateEnvelopeResponse
|
||||
var resp plugin.GenerateEnvelopeResponse
|
||||
err := run(ctx, p.name, p.path, req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
@ -142,76 +152,96 @@ func (p *CLIPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEnv
|
|||
// VerifySignature validates the signature based on the request.
|
||||
//
|
||||
// if ContractVersion is not set, it will be set by the function.
|
||||
func (p *CLIPlugin) VerifySignature(ctx context.Context, req *proto.VerifySignatureRequest) (*proto.VerifySignatureResponse, error) {
|
||||
func (p *CLIPlugin) VerifySignature(ctx context.Context, req *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) {
|
||||
if req.ContractVersion == "" {
|
||||
req.ContractVersion = proto.ContractVersion
|
||||
req.ContractVersion = plugin.ContractVersion
|
||||
}
|
||||
|
||||
var resp proto.VerifySignatureResponse
|
||||
var resp plugin.VerifySignatureResponse
|
||||
err := run(ctx, p.name, p.path, req, &resp)
|
||||
return &resp, err
|
||||
}
|
||||
|
||||
func run(ctx context.Context, pluginName string, pluginPath string, req proto.Request, resp interface{}) error {
|
||||
func run(ctx context.Context, pluginName string, pluginPath string, req plugin.Request, resp interface{}) error {
|
||||
logger := log.GetLogger(ctx)
|
||||
|
||||
// serialize request
|
||||
data, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: failed to marshal request object: %w", pluginName, err)
|
||||
logger.Errorf("Failed to marshal request object: %+v", req)
|
||||
return fmt.Errorf("failed to marshal request object: %w", err)
|
||||
}
|
||||
|
||||
logger.Debugf("Plugin %s request: %s", req.Command(), string(data))
|
||||
// execute request
|
||||
stdout, stderr, err := executor.Output(ctx, pluginPath, req.Command(), data)
|
||||
if err != nil {
|
||||
logger.Debugf("plugin %s execution status: %v", req.Command(), err)
|
||||
logger.Debugf("Plugin %s returned error: %s", req.Command(), string(stderr))
|
||||
var re proto.RequestError
|
||||
jsonErr := json.Unmarshal(stderr, &re)
|
||||
if jsonErr != nil {
|
||||
return proto.RequestError{
|
||||
Code: proto.ErrorCodeGeneric,
|
||||
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", err, string(stderr))}
|
||||
logger.Errorf("plugin %s execution status: %v", req.Command(), err)
|
||||
|
||||
if len(stderr) == 0 {
|
||||
// if stderr is empty, it is possible that the plugin is not
|
||||
// running properly.
|
||||
logger.Errorf("failed to execute the %s command for plugin %s: %s", req.Command(), pluginName, err)
|
||||
return &PluginExecutableFileError{
|
||||
InnerError: err,
|
||||
}
|
||||
} else {
|
||||
var re proto.RequestError
|
||||
jsonErr := json.Unmarshal(stderr, &re)
|
||||
if jsonErr != nil {
|
||||
logger.Errorf("failed to execute the %s command for plugin %s: %s", req.Command(), pluginName, strings.TrimSuffix(string(stderr), "\n"))
|
||||
return &PluginMalformedError{
|
||||
InnerError: jsonErr,
|
||||
}
|
||||
}
|
||||
logger.Errorf("failed to execute the %s command for plugin %s: %s: %w", req.Command(), pluginName, re.Code, re)
|
||||
return re
|
||||
}
|
||||
return re
|
||||
}
|
||||
|
||||
logger.Debugf("Plugin %s response: %s", req.Command(), string(stdout))
|
||||
// deserialize response
|
||||
err = json.Unmarshal(stdout, resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode json response: %w", ErrNotCompliant)
|
||||
if err = json.Unmarshal(stdout, resp); err != nil {
|
||||
logger.Errorf("failed to unmarshal plugin %s response: %w", req.Command(), err)
|
||||
return &PluginMalformedError{
|
||||
Msg: fmt.Sprintf("failed to unmarshal the response of %s command for plugin %s", req.Command(), pluginName),
|
||||
InnerError: err,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// commander is defined for mocking purposes.
|
||||
type commander interface {
|
||||
// Output runs the command, passing req to the its stdin.
|
||||
// Output runs the command, passing req to the stdin.
|
||||
// It only returns an error if the binary can't be executed.
|
||||
// Returns stdout if err is nil, stderr if err is not nil.
|
||||
Output(ctx context.Context, path string, command proto.Command, req []byte) (stdout []byte, stderr []byte, err error)
|
||||
Output(ctx context.Context, path string, command plugin.Command, req []byte) (stdout []byte, stderr []byte, err error)
|
||||
}
|
||||
|
||||
// execCommander implements the commander interface using exec.Command().
|
||||
type execCommander struct{}
|
||||
|
||||
func (c execCommander) Output(ctx context.Context, name string, command proto.Command, req []byte) ([]byte, []byte, error) {
|
||||
func (c execCommander) Output(ctx context.Context, name string, command plugin.Command, req []byte) ([]byte, []byte, error) {
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd := exec.CommandContext(ctx, name, string(command))
|
||||
cmd.Stdin = bytes.NewReader(req)
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Stdout = &stdout
|
||||
// The limit writer will be handled by the caller in run() by comparing the
|
||||
// bytes written with the expected length of the bytes.
|
||||
cmd.Stderr = io.LimitWriter(&stderr, maxPluginOutputSize)
|
||||
cmd.Stdout = io.LimitWriter(&stdout, maxPluginOutputSize)
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
|
||||
return nil, stderr.Bytes(), fmt.Errorf("'%s %s' command execution timeout: %w", name, string(command), err)
|
||||
}
|
||||
return nil, stderr.Bytes(), err
|
||||
}
|
||||
return stdout.Bytes(), nil, nil
|
||||
}
|
||||
|
||||
// validate checks if the metadata is correctly populated.
|
||||
func validate(metadata *proto.GetMetadataResponse) error {
|
||||
func validate(metadata *plugin.GetMetadataResponse) error {
|
||||
if metadata.Name == "" {
|
||||
return errors.New("empty name")
|
||||
}
|
||||
|
@ -230,10 +260,10 @@ func validate(metadata *proto.GetMetadataResponse) error {
|
|||
if len(metadata.SupportedContractVersions) == 0 {
|
||||
return errors.New("supported contract versions not specified")
|
||||
}
|
||||
if !slices.Contains(metadata.SupportedContractVersions, proto.ContractVersion) {
|
||||
if !slices.Contains(metadata.SupportedContractVersions, plugin.ContractVersion) {
|
||||
return fmt.Errorf(
|
||||
"contract version %q is not in the list of the plugin supported versions %v",
|
||||
proto.ContractVersion, metadata.SupportedContractVersions,
|
||||
plugin.ContractVersion, metadata.SupportedContractVersions,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -1,15 +1,29 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
)
|
||||
|
@ -18,14 +32,12 @@ func TestGetMetadata(t *testing.T) {
|
|||
t.Run("plugin error is in invalid json format", func(t *testing.T) {
|
||||
exitErr := errors.New("unknown error")
|
||||
stderr := []byte("sad")
|
||||
wantErr := proto.RequestError{
|
||||
Code: proto.ErrorCodeGeneric,
|
||||
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", exitErr, string(stderr))}
|
||||
plugin := CLIPlugin{}
|
||||
expectedErrMsg := "invalid character 's' looking for beginning of value"
|
||||
plugin := CLIPlugin{name: "test-plugin"}
|
||||
executor = testCommander{stdout: nil, stderr: stderr, err: exitErr}
|
||||
_, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("should error. got err = %v, want %v", err, wantErr)
|
||||
if err.Error() != expectedErrMsg {
|
||||
t.Fatalf("should error. got err = %v, want %v", err, expectedErrMsg)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -45,14 +57,12 @@ func TestGetMetadata(t *testing.T) {
|
|||
t.Run("plugin cause system error", func(t *testing.T) {
|
||||
exitErr := errors.New("system error")
|
||||
stderr := []byte("")
|
||||
wantErr := proto.RequestError{
|
||||
Code: proto.ErrorCodeGeneric,
|
||||
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", exitErr, string(stderr))}
|
||||
plugin := CLIPlugin{}
|
||||
expectedErrMsg := "system error"
|
||||
plugin := CLIPlugin{name: "test-plugin"}
|
||||
executor = testCommander{stdout: nil, stderr: stderr, err: exitErr}
|
||||
_, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("should error. got err = %v, want %v", err, wantErr)
|
||||
if err.Error() != expectedErrMsg {
|
||||
t.Fatalf("should error. got err = %v, want %v", err, expectedErrMsg)
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -173,7 +183,7 @@ func TestValidateMetadata(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewCLIPlugin_PathError(t *testing.T) {
|
||||
func TestNewCLIPlugin_Error(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
t.Run("plugin directory exists without executable.", func(t *testing.T) {
|
||||
p, err := NewCLIPlugin(ctx, "emptyplugin", "./testdata/plugins/emptyplugin/notation-emptyplugin")
|
||||
|
@ -186,14 +196,34 @@ func TestNewCLIPlugin_PathError(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("plugin is not a regular file", func(t *testing.T) {
|
||||
expectedErrMsg := "plugin executable file is not a regular file"
|
||||
p, err := NewCLIPlugin(ctx, "badplugin", "./testdata/plugins/badplugin/notation-badplugin")
|
||||
if !errors.Is(err, ErrNotRegularFile) {
|
||||
t.Errorf("NewCLIPlugin() error = %v, want %v", err, ErrNotRegularFile)
|
||||
if err.Error() != expectedErrMsg {
|
||||
t.Errorf("NewCLIPlugin() error = %v, want %v", err, expectedErrMsg)
|
||||
}
|
||||
if p != nil {
|
||||
t.Errorf("NewCLIPlugin() plugin = %v, want nil", p)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("plugin timeout error", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
expectedErrMsg := "'sleep 2' command execution timeout: signal: killed"
|
||||
ctxWithTimout, cancel := context.WithTimeout(ctx, 10 * time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
var twoSeconds proto.Command
|
||||
twoSeconds = "2"
|
||||
_, _, err := execCommander{}.Output(ctxWithTimout, "sleep", twoSeconds, nil);
|
||||
if err == nil {
|
||||
t.Errorf("execCommander{}.Output() expected error = %v, got nil", expectedErrMsg)
|
||||
}
|
||||
if err.Error() != expectedErrMsg {
|
||||
t.Errorf("execCommander{}.Output() error = %v, want %v", err, expectedErrMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewCLIPlugin_ValidError(t *testing.T) {
|
||||
|
@ -205,23 +235,23 @@ func TestNewCLIPlugin_ValidError(t *testing.T) {
|
|||
t.Run("command no response", func(t *testing.T) {
|
||||
executor = testCommander{}
|
||||
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
|
||||
if !strings.Contains(err.Error(), ErrNotCompliant.Error()) {
|
||||
t.Fatal("should fail the operation.")
|
||||
if _, ok := err.(*PluginMalformedError); !ok {
|
||||
t.Fatal("should return plugin validity error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid json", func(t *testing.T) {
|
||||
executor = testCommander{stdout: []byte("content")}
|
||||
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
|
||||
if !strings.Contains(err.Error(), ErrNotCompliant.Error()) {
|
||||
t.Fatal("should fail the operation.")
|
||||
if _, ok := err.(*PluginMalformedError); !ok {
|
||||
t.Fatal("should return plugin validity error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid metadata name", func(t *testing.T) {
|
||||
executor = testCommander{stdout: metadataJSON(invalidMetadataName)}
|
||||
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
|
||||
if !strings.Contains(err.Error(), "executable name must be") {
|
||||
if !strings.Contains(err.Error(), "executable file name must be") {
|
||||
t.Fatal("should fail the operation.")
|
||||
}
|
||||
})
|
||||
|
@ -229,8 +259,8 @@ func TestNewCLIPlugin_ValidError(t *testing.T) {
|
|||
t.Run("invalid metadata content", func(t *testing.T) {
|
||||
executor = testCommander{stdout: metadataJSON(proto.GetMetadataResponse{Name: "foo"})}
|
||||
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
|
||||
if !strings.Contains(err.Error(), "invalid metadata") {
|
||||
t.Fatal("should fail the operation.")
|
||||
if _, ok := err.(*PluginMalformedError); !ok {
|
||||
t.Fatal("should be plugin validity error.")
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 proto
|
||||
|
||||
import (
|
||||
|
@ -5,67 +18,74 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
// KeySpec is type of the signing algorithm, including algorithm and size.
|
||||
type KeySpec string
|
||||
//
|
||||
// Deprecated: KeySpec exists for historical compatibility and should not be used.
|
||||
// To access KeySpec, use the notation-plugin-framework-go's [plugin.KeySpec] type.
|
||||
type KeySpec = plugin.KeySpec
|
||||
|
||||
// one of the following supported key spec names.
|
||||
//
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection
|
||||
// Deprecated: KeySpec exists for historical compatibility and should not be used.
|
||||
// To access KeySpec, use the notation-plugin-framework-go's [plugin.KeySpec].
|
||||
//
|
||||
// [keys spec]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||
const (
|
||||
KeySpecRSA2048 KeySpec = "RSA-2048"
|
||||
KeySpecRSA3072 KeySpec = "RSA-3072"
|
||||
KeySpecRSA4096 KeySpec = "RSA-4096"
|
||||
KeySpecEC256 KeySpec = "EC-256"
|
||||
KeySpecEC384 KeySpec = "EC-384"
|
||||
KeySpecEC521 KeySpec = "EC-521"
|
||||
KeySpecRSA2048 = plugin.KeySpecRSA2048
|
||||
KeySpecRSA3072 = plugin.KeySpecRSA3072
|
||||
KeySpecRSA4096 = plugin.KeySpecRSA4096
|
||||
KeySpecEC256 = plugin.KeySpecEC256
|
||||
KeySpecEC384 = plugin.KeySpecEC384
|
||||
KeySpecEC521 = plugin.KeySpecEC521
|
||||
)
|
||||
|
||||
// EncodeKeySpec returns the name of a keySpec according to the spec.
|
||||
func EncodeKeySpec(k signature.KeySpec) (KeySpec, error) {
|
||||
func EncodeKeySpec(k signature.KeySpec) (plugin.KeySpec, error) {
|
||||
switch k.Type {
|
||||
case signature.KeyTypeEC:
|
||||
switch k.Size {
|
||||
case 256:
|
||||
return KeySpecEC256, nil
|
||||
return plugin.KeySpecEC256, nil
|
||||
case 384:
|
||||
return KeySpecEC384, nil
|
||||
return plugin.KeySpecEC384, nil
|
||||
case 521:
|
||||
return KeySpecEC521, nil
|
||||
return plugin.KeySpecEC521, nil
|
||||
}
|
||||
case signature.KeyTypeRSA:
|
||||
switch k.Size {
|
||||
case 2048:
|
||||
return KeySpecRSA2048, nil
|
||||
return plugin.KeySpecRSA2048, nil
|
||||
case 3072:
|
||||
return KeySpecRSA3072, nil
|
||||
return plugin.KeySpecRSA3072, nil
|
||||
case 4096:
|
||||
return KeySpecRSA4096, nil
|
||||
return plugin.KeySpecRSA4096, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("invalid KeySpec %q", k)
|
||||
}
|
||||
|
||||
// DecodeKeySpec parses keySpec name to a signature.keySpec type.
|
||||
func DecodeKeySpec(k KeySpec) (keySpec signature.KeySpec, err error) {
|
||||
func DecodeKeySpec(k plugin.KeySpec) (keySpec signature.KeySpec, err error) {
|
||||
switch k {
|
||||
case KeySpecRSA2048:
|
||||
case plugin.KeySpecRSA2048:
|
||||
keySpec.Size = 2048
|
||||
keySpec.Type = signature.KeyTypeRSA
|
||||
case KeySpecRSA3072:
|
||||
case plugin.KeySpecRSA3072:
|
||||
keySpec.Size = 3072
|
||||
keySpec.Type = signature.KeyTypeRSA
|
||||
case KeySpecRSA4096:
|
||||
case plugin.KeySpecRSA4096:
|
||||
keySpec.Size = 4096
|
||||
keySpec.Type = signature.KeyTypeRSA
|
||||
case KeySpecEC256:
|
||||
case plugin.KeySpecEC256:
|
||||
keySpec.Size = 256
|
||||
keySpec.Type = signature.KeyTypeEC
|
||||
case KeySpecEC384:
|
||||
case plugin.KeySpecEC384:
|
||||
keySpec.Size = 384
|
||||
keySpec.Type = signature.KeyTypeEC
|
||||
case KeySpecEC521:
|
||||
case plugin.KeySpecEC521:
|
||||
keySpec.Size = 521
|
||||
keySpec.Type = signature.KeyTypeEC
|
||||
default:
|
||||
|
@ -75,92 +95,104 @@ func DecodeKeySpec(k KeySpec) (keySpec signature.KeySpec, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// HashAlgorithm is the type of a hash algorithm.
|
||||
type HashAlgorithm string
|
||||
// HashAlgorithm is the type of hash algorithm.
|
||||
//
|
||||
// Deprecated: HashAlgorithm exists for historical compatibility and should not be used.
|
||||
// To access HashAlgorithm, use the notation-plugin-framework-go's [plugin.HashAlgorithm] type.
|
||||
type HashAlgorithm = plugin.HashAlgorithm
|
||||
|
||||
// one of the following supported hash algorithm names.
|
||||
//
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection
|
||||
// Deprecated: HashAlgorithm exists for historical compatibility and should not be used.
|
||||
// To access HashAlgorithm, use the notation-plugin-framework-go's [plugin.HashAlgorithm] type.
|
||||
//
|
||||
// [hash algorithm]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||
const (
|
||||
HashAlgorithmSHA256 HashAlgorithm = "SHA-256"
|
||||
HashAlgorithmSHA384 HashAlgorithm = "SHA-384"
|
||||
HashAlgorithmSHA512 HashAlgorithm = "SHA-512"
|
||||
HashAlgorithmSHA256 = plugin.HashAlgorithmSHA256
|
||||
HashAlgorithmSHA384 = plugin.HashAlgorithmSHA384
|
||||
HashAlgorithmSHA512 = plugin.HashAlgorithmSHA512
|
||||
)
|
||||
|
||||
// HashAlgorithmFromKeySpec returns the name of hash function according to the spec.
|
||||
func HashAlgorithmFromKeySpec(k signature.KeySpec) (HashAlgorithm, error) {
|
||||
func HashAlgorithmFromKeySpec(k signature.KeySpec) (plugin.HashAlgorithm, error) {
|
||||
switch k.Type {
|
||||
case signature.KeyTypeEC:
|
||||
switch k.Size {
|
||||
case 256:
|
||||
return HashAlgorithmSHA256, nil
|
||||
return plugin.HashAlgorithmSHA256, nil
|
||||
case 384:
|
||||
return HashAlgorithmSHA384, nil
|
||||
return plugin.HashAlgorithmSHA384, nil
|
||||
case 521:
|
||||
return HashAlgorithmSHA512, nil
|
||||
return plugin.HashAlgorithmSHA512, nil
|
||||
}
|
||||
case signature.KeyTypeRSA:
|
||||
switch k.Size {
|
||||
case 2048:
|
||||
return HashAlgorithmSHA256, nil
|
||||
return plugin.HashAlgorithmSHA256, nil
|
||||
case 3072:
|
||||
return HashAlgorithmSHA384, nil
|
||||
return plugin.HashAlgorithmSHA384, nil
|
||||
case 4096:
|
||||
return HashAlgorithmSHA512, nil
|
||||
return plugin.HashAlgorithmSHA512, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("invalid KeySpec %q", k)
|
||||
}
|
||||
|
||||
// SignatureAlgorithm is the type of signature algorithm
|
||||
type SignatureAlgorithm string
|
||||
|
||||
// one of the following supported signing algorithm names.
|
||||
//
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection
|
||||
// Deprecated: SignatureAlgorithm exists for historical compatibility and should not be used.
|
||||
// To access SignatureAlgorithm, use the notation-plugin-framework-go's [plugin.SignatureAlgorithm] type.
|
||||
type SignatureAlgorithm = plugin.SignatureAlgorithm
|
||||
|
||||
// one of the following supported [signing algorithm] names.
|
||||
//
|
||||
// Deprecated: SignatureAlgorithm exists for historical compatibility and should not be used.
|
||||
// To access SignatureAlgorithm, use the notation-plugin-framework-go's [plugin.SignatureAlgorithm] type.
|
||||
//
|
||||
// [signing algorithm]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||
const (
|
||||
SignatureAlgorithmECDSA_SHA256 SignatureAlgorithm = "ECDSA-SHA-256"
|
||||
SignatureAlgorithmECDSA_SHA384 SignatureAlgorithm = "ECDSA-SHA-384"
|
||||
SignatureAlgorithmECDSA_SHA512 SignatureAlgorithm = "ECDSA-SHA-512"
|
||||
SignatureAlgorithmRSASSA_PSS_SHA256 SignatureAlgorithm = "RSASSA-PSS-SHA-256"
|
||||
SignatureAlgorithmRSASSA_PSS_SHA384 SignatureAlgorithm = "RSASSA-PSS-SHA-384"
|
||||
SignatureAlgorithmRSASSA_PSS_SHA512 SignatureAlgorithm = "RSASSA-PSS-SHA-512"
|
||||
SignatureAlgorithmECDSA_SHA256 = plugin.SignatureAlgorithmECDSA_SHA256
|
||||
SignatureAlgorithmECDSA_SHA384 = plugin.SignatureAlgorithmECDSA_SHA384
|
||||
SignatureAlgorithmECDSA_SHA512 = plugin.SignatureAlgorithmECDSA_SHA512
|
||||
SignatureAlgorithmRSASSA_PSS_SHA256 = plugin.SignatureAlgorithmRSASSA_PSS_SHA256
|
||||
SignatureAlgorithmRSASSA_PSS_SHA384 = plugin.SignatureAlgorithmRSASSA_PSS_SHA384
|
||||
SignatureAlgorithmRSASSA_PSS_SHA512 = plugin.SignatureAlgorithmRSASSA_PSS_SHA512
|
||||
)
|
||||
|
||||
// EncodeSigningAlgorithm returns the signing algorithm name of an algorithm
|
||||
// according to the spec.
|
||||
func EncodeSigningAlgorithm(alg signature.Algorithm) (SignatureAlgorithm, error) {
|
||||
func EncodeSigningAlgorithm(alg signature.Algorithm) (plugin.SignatureAlgorithm, error) {
|
||||
switch alg {
|
||||
case signature.AlgorithmES256:
|
||||
return SignatureAlgorithmECDSA_SHA256, nil
|
||||
return plugin.SignatureAlgorithmECDSA_SHA256, nil
|
||||
case signature.AlgorithmES384:
|
||||
return SignatureAlgorithmECDSA_SHA384, nil
|
||||
return plugin.SignatureAlgorithmECDSA_SHA384, nil
|
||||
case signature.AlgorithmES512:
|
||||
return SignatureAlgorithmECDSA_SHA512, nil
|
||||
return plugin.SignatureAlgorithmECDSA_SHA512, nil
|
||||
case signature.AlgorithmPS256:
|
||||
return SignatureAlgorithmRSASSA_PSS_SHA256, nil
|
||||
return plugin.SignatureAlgorithmRSASSA_PSS_SHA256, nil
|
||||
case signature.AlgorithmPS384:
|
||||
return SignatureAlgorithmRSASSA_PSS_SHA384, nil
|
||||
return plugin.SignatureAlgorithmRSASSA_PSS_SHA384, nil
|
||||
case signature.AlgorithmPS512:
|
||||
return SignatureAlgorithmRSASSA_PSS_SHA512, nil
|
||||
return plugin.SignatureAlgorithmRSASSA_PSS_SHA512, nil
|
||||
}
|
||||
return "", fmt.Errorf("invalid algorithm %q", alg)
|
||||
}
|
||||
|
||||
// DecodeSigningAlgorithm parses the signing algorithm name from a given string.
|
||||
func DecodeSigningAlgorithm(raw SignatureAlgorithm) (signature.Algorithm, error) {
|
||||
func DecodeSigningAlgorithm(raw plugin.SignatureAlgorithm) (signature.Algorithm, error) {
|
||||
switch raw {
|
||||
case SignatureAlgorithmECDSA_SHA256:
|
||||
case plugin.SignatureAlgorithmECDSA_SHA256:
|
||||
return signature.AlgorithmES256, nil
|
||||
case SignatureAlgorithmECDSA_SHA384:
|
||||
case plugin.SignatureAlgorithmECDSA_SHA384:
|
||||
return signature.AlgorithmES384, nil
|
||||
case SignatureAlgorithmECDSA_SHA512:
|
||||
case plugin.SignatureAlgorithmECDSA_SHA512:
|
||||
return signature.AlgorithmES512, nil
|
||||
case SignatureAlgorithmRSASSA_PSS_SHA256:
|
||||
case plugin.SignatureAlgorithmRSASSA_PSS_SHA256:
|
||||
return signature.AlgorithmPS256, nil
|
||||
case SignatureAlgorithmRSASSA_PSS_SHA384:
|
||||
case plugin.SignatureAlgorithmRSASSA_PSS_SHA384:
|
||||
return signature.AlgorithmPS384, nil
|
||||
case SignatureAlgorithmRSASSA_PSS_SHA512:
|
||||
case plugin.SignatureAlgorithmRSASSA_PSS_SHA512:
|
||||
return signature.AlgorithmPS512, nil
|
||||
}
|
||||
return 0, errors.New("unknown signing algorithm")
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 proto
|
||||
|
||||
import (
|
||||
|
@ -215,7 +228,7 @@ func TestDecodeKeySpec(t *testing.T) {
|
|||
},
|
||||
{
|
||||
name: "Unsupported key spec",
|
||||
raw: "unsuppored",
|
||||
raw: "unsupported",
|
||||
expected: signature.KeySpec{},
|
||||
expectErr: true,
|
||||
},
|
||||
|
|
|
@ -1,51 +1,78 @@
|
|||
// 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 proto
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
type ErrorCode string
|
||||
// Deprecated: ErrorCode exists for historical compatibility and should not be used.
|
||||
// To access ErrorCode, use the notation-plugin-framework-go's plugin.ErrorCode type.
|
||||
type ErrorCode = plugin.ErrorCode
|
||||
|
||||
const (
|
||||
// Any of the required request fields was empty,
|
||||
// or a value was malformed/invalid.
|
||||
ErrorCodeValidation ErrorCode = "VALIDATION_ERROR"
|
||||
// ErrorCodeValidation is used when any of the required request fields is empty ormalformed/invalid.
|
||||
//
|
||||
// Deprecated: ErrorCodeValidation exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeValidation, use the notation-plugin-framework-go's [plugin.ErrorCodeValidation].
|
||||
ErrorCodeValidation = plugin.ErrorCodeValidation
|
||||
|
||||
// The contract version used in the request is unsupported.
|
||||
ErrorCodeUnsupportedContractVersion ErrorCode = "UNSUPPORTED_CONTRACT_VERSION"
|
||||
// ErrorCodeUnsupportedContractVersion is used when when the contract version used in the request is unsupported.
|
||||
//
|
||||
// Deprecated: ErrorCodeUnsupportedContractVersion exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeUnsupportedContractVersion, use the notation-plugin-framework-go's [plugin.ErrorCodeUnsupportedContractVersion].
|
||||
ErrorCodeUnsupportedContractVersion = plugin.ErrorCodeUnsupportedContractVersion
|
||||
|
||||
// Authentication/authorization error to use given key.
|
||||
ErrorCodeAccessDenied ErrorCode = "ACCESS_DENIED"
|
||||
// ErrorCodeAccessDenied is used when user doesn't have required permission to access the key.
|
||||
//
|
||||
// Deprecated: ErrorCodeAccessDenied exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeAccessDenied, use the notation-plugin-framework-go's [plugin.ErrorCodeAccessDenied].
|
||||
ErrorCodeAccessDenied = plugin.ErrorCodeAccessDenied
|
||||
|
||||
// The operation to generate signature timed out
|
||||
// ErrorCodeTimeout is used when an operation to generate signature timed out and can be retried by Notation.
|
||||
//
|
||||
// Deprecated: ErrorCodeTimeout exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeTimeout, use the notation-plugin-framework-go's [plugin.ErrorCodeTimeout].
|
||||
ErrorCodeTimeout = plugin.ErrorCodeTimeout
|
||||
|
||||
// ErrorCodeThrottled is used when an operation to generate signature was throttles
|
||||
// and can be retried by Notation.
|
||||
ErrorCodeTimeout ErrorCode = "TIMEOUT"
|
||||
//
|
||||
// Deprecated: ErrorCodeThrottled exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeThrottled, use the notation-plugin-framework-go's [plugin.ErrorCodeThrottled].
|
||||
ErrorCodeThrottled = plugin.ErrorCodeThrottled
|
||||
|
||||
// The operation to generate signature was throttles
|
||||
// and can be retried by Notation.
|
||||
ErrorCodeThrottled ErrorCode = "THROTTLED"
|
||||
|
||||
// Any general error that does not fall into any categories.
|
||||
ErrorCodeGeneric ErrorCode = "ERROR"
|
||||
// ErrorCodeGeneric is used when an general error occurred that does not fall into any categories.
|
||||
//
|
||||
// Deprecated: ErrorCodeGeneric exists for historical compatibility and should not be used.
|
||||
// To access ErrorCodeGeneric, use the notation-plugin-framework-go's [plugin.ErrorCodeGeneric].
|
||||
ErrorCodeGeneric = plugin.ErrorCodeGeneric
|
||||
)
|
||||
|
||||
type jsonErr struct {
|
||||
Code ErrorCode `json:"errorCode"`
|
||||
Message string `json:"errorMessage,omitempty"`
|
||||
Metadata map[string]string `json:"errorMetadata,omitempty"`
|
||||
}
|
||||
|
||||
// RequestError is the common error response for any request.
|
||||
type RequestError struct {
|
||||
Code ErrorCode
|
||||
Code plugin.ErrorCode
|
||||
Err error
|
||||
Metadata map[string]string
|
||||
}
|
||||
|
||||
func (e RequestError) Error() string {
|
||||
return fmt.Sprintf("%s: %v", e.Code, e.Err)
|
||||
return fmt.Sprintf("%v", e.Err)
|
||||
}
|
||||
|
||||
func (e RequestError) Unwrap() error {
|
||||
|
@ -70,19 +97,19 @@ func (e RequestError) MarshalJSON() ([]byte, error) {
|
|||
if e.Err != nil {
|
||||
msg = e.Err.Error()
|
||||
}
|
||||
return json.Marshal(jsonErr{e.Code, msg, e.Metadata})
|
||||
return json.Marshal(plugin.Error{ErrCode: e.Code, Message: msg, Metadata: e.Metadata})
|
||||
}
|
||||
|
||||
func (e *RequestError) UnmarshalJSON(data []byte) error {
|
||||
var tmp jsonErr
|
||||
var tmp plugin.Error
|
||||
err := json.Unmarshal(data, &tmp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tmp.Code == "" && tmp.Message == "" && tmp.Metadata == nil {
|
||||
if tmp.ErrCode == "" && tmp.Message == "" && tmp.Metadata == nil {
|
||||
return errors.New("incomplete json")
|
||||
}
|
||||
*e = RequestError{Code: tmp.Code, Metadata: tmp.Metadata}
|
||||
*e = RequestError{Code: tmp.ErrCode, Metadata: tmp.Metadata}
|
||||
if tmp.Message != "" {
|
||||
e.Err = errors.New(tmp.Message)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 proto
|
||||
|
||||
import (
|
||||
|
@ -9,7 +22,7 @@ import (
|
|||
|
||||
func TestRequestError_Error(t *testing.T) {
|
||||
err := RequestError{Code: ErrorCodeAccessDenied, Err: errors.New("an error")}
|
||||
want := string(ErrorCodeAccessDenied) + ": an error"
|
||||
want := "an error"
|
||||
if got := err.Error(); got != want {
|
||||
t.Errorf("RequestError.Error() = %v, want %v", got, want)
|
||||
}
|
||||
|
|
|
@ -1,36 +1,28 @@
|
|||
// 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 proto
|
||||
|
||||
// GetMetadataRequest contains the parameters passed in a get-plugin-metadata
|
||||
// request.
|
||||
type GetMetadataRequest struct {
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
import "github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
|
||||
func (GetMetadataRequest) Command() Command {
|
||||
return CommandGetMetadata
|
||||
}
|
||||
// GetMetadataRequest contains the parameters passed in a get-plugin-metadata request.
|
||||
//
|
||||
// Deprecated: GetMetadataRequest exists for historical compatibility and should not be used.
|
||||
// To access GetMetadataRequest, use the notation-plugin-framework-go's [plugin.GetMetadataRequest] type.
|
||||
type GetMetadataRequest = plugin.GetMetadataRequest
|
||||
|
||||
// GetMetadataResponse provided by the plugin.
|
||||
type GetMetadataResponse struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
URL string `json:"url"`
|
||||
SupportedContractVersions []string `json:"supportedContractVersions"`
|
||||
Capabilities []Capability `json:"capabilities"`
|
||||
}
|
||||
|
||||
// HasCapability return true if the metadata states that the
|
||||
// capability is supported.
|
||||
// Returns true if capability is empty.
|
||||
func (resp *GetMetadataResponse) HasCapability(capability Capability) bool {
|
||||
if capability == "" {
|
||||
return true
|
||||
}
|
||||
for _, c := range resp.Capabilities {
|
||||
if c == capability {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
//
|
||||
// Deprecated: GetMetadataResponse exists for historical compatibility and should not be used.
|
||||
// To access GetMetadataResponse, use the notation-plugin-framework-go's [plugin.GetMetadataResponse] type.
|
||||
type GetMetadataResponse = plugin.GetMetadataResponse
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 proto
|
||||
|
||||
import (
|
||||
|
|
|
@ -1,65 +1,120 @@
|
|||
// 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 proto defines the protocol layer for communication between notation
|
||||
// and notation external plugin.
|
||||
package proto
|
||||
|
||||
import "github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
|
||||
// Prefix is the prefix required on all plugin binary names.
|
||||
const Prefix = "notation-"
|
||||
//
|
||||
// Deprecated: Prefix exists for historical compatibility and should not be used.
|
||||
// To access Prefix, use the notation-plugin-framework-go's [plugin.BinaryPrefix] type.
|
||||
const Prefix = plugin.BinaryPrefix
|
||||
|
||||
// ContractVersion is the <major>.<minor> version of the plugin contract.
|
||||
const ContractVersion = "1.0"
|
||||
//
|
||||
// Deprecated: ContractVersion exists for historical compatibility and should not be used.
|
||||
// To access ContractVersion, use the notation-plugin-framework-go's [plugin.ContractVersion] type.
|
||||
const ContractVersion = plugin.ContractVersion
|
||||
|
||||
// Command is a CLI command available in the plugin contract.
|
||||
type Command string
|
||||
//
|
||||
// Deprecated: Command exists for historical compatibility and should not be used.
|
||||
// To access Command, use the notation-plugin-framework-go's [plugin.Command] type.
|
||||
type Command = plugin.Command
|
||||
|
||||
// Request defines a plugin request, which is always associated to a command.
|
||||
type Request interface {
|
||||
Command() Command
|
||||
}
|
||||
//
|
||||
// Deprecated: Request exists for historical compatibility and should not be used.
|
||||
// To access Request, use the notation-plugin-framework-go's [plugin.Request] type.
|
||||
type Request = plugin.Request
|
||||
|
||||
const (
|
||||
// CommandGetMetadata is the name of the plugin command
|
||||
// which must be supported by every plugin and returns the
|
||||
// plugin metadata.
|
||||
CommandGetMetadata Command = "get-plugin-metadata"
|
||||
//
|
||||
// Deprecated: CommandGetMetadata exists for historical compatibility and should not be used.
|
||||
// To access CommandGetMetadata, use the notation-plugin-framework-go's [plugin.CommandGetMetadata].
|
||||
CommandGetMetadata = plugin.CommandGetMetadata
|
||||
|
||||
// CommandDescribeKey is the name of the plugin command
|
||||
// which must be supported by every plugin that has the
|
||||
// SIGNATURE_GENERATOR.RAW capability.
|
||||
CommandDescribeKey Command = "describe-key"
|
||||
//
|
||||
// Deprecated: CommandDescribeKey exists for historical compatibility and should not be used.
|
||||
// To access CommandDescribeKey, use the notation-plugin-framework-go's [plugin.CommandDescribeKey].
|
||||
CommandDescribeKey = plugin.CommandDescribeKey
|
||||
|
||||
// CommandGenerateSignature is the name of the plugin command
|
||||
// which must be supported by every plugin that has the
|
||||
// SIGNATURE_GENERATOR.RAW capability.
|
||||
CommandGenerateSignature Command = "generate-signature"
|
||||
//
|
||||
// Deprecated: CommandGenerateSignature exists for historical compatibility and should not be used.
|
||||
// To access CommandGenerateSignature, use the notation-plugin-framework-go's [plugin.CommandGenerateSignature].
|
||||
CommandGenerateSignature = plugin.CommandGenerateSignature
|
||||
|
||||
// CommandGenerateEnvelope is the name of the plugin command
|
||||
// which must be supported by every plugin that has the
|
||||
// SIGNATURE_GENERATOR.ENVELOPE capability.
|
||||
CommandGenerateEnvelope Command = "generate-envelope"
|
||||
//
|
||||
// Deprecated: CommandGenerateEnvelope exists for historical compatibility and should not be used.
|
||||
// To access CommandGenerateEnvelope, use the notation-plugin-framework-go's [plugin.CommandGenerateEnvelope].
|
||||
CommandGenerateEnvelope = plugin.CommandGenerateEnvelope
|
||||
|
||||
// CommandVerifySignature is the name of the plugin command
|
||||
// which must be supported by every plugin that has
|
||||
// any SIGNATURE_VERIFIER.* capability
|
||||
CommandVerifySignature Command = "verify-signature"
|
||||
//
|
||||
// Deprecated: CommandVerifySignature exists for historical compatibility and should not be used.
|
||||
// To access CommandVerifySignature, use the notation-plugin-framework-go's [plugin.CommandVerifySignature].
|
||||
CommandVerifySignature = plugin.CommandVerifySignature
|
||||
)
|
||||
|
||||
// Capability is a feature available in the plugin contract.
|
||||
type Capability string
|
||||
//
|
||||
// Deprecated: Capability exists for historical compatibility and should not be used.
|
||||
// To access Capability, use the notation-plugin-framework-go's [plugin.Capability] type.
|
||||
type Capability = plugin.Capability
|
||||
|
||||
const (
|
||||
// CapabilitySignatureGenerator is the name of the capability
|
||||
// for a plugin to support generating raw signatures.
|
||||
CapabilitySignatureGenerator Capability = "SIGNATURE_GENERATOR.RAW"
|
||||
//
|
||||
// Deprecated: CapabilitySignatureGenerator exists for historical compatibility and should not be used.
|
||||
// To access CapabilitySignatureGenerator, use the notation-plugin-framework-go's [plugin.CapabilitySignatureGenerator].
|
||||
CapabilitySignatureGenerator = plugin.CapabilitySignatureGenerator
|
||||
|
||||
// CapabilityEnvelopeGenerator is the name of the capability
|
||||
// for a plugin to support generating envelope signatures.
|
||||
CapabilityEnvelopeGenerator Capability = "SIGNATURE_GENERATOR.ENVELOPE"
|
||||
//
|
||||
// Deprecated: CapabilityEnvelopeGenerator exists for historical compatibility and should not be used.
|
||||
// To access CapabilityEnvelopeGenerator, use the notation-plugin-framework-go's [plugin.CapabilityEnvelopeGenerator].
|
||||
CapabilityEnvelopeGenerator = plugin.CapabilityEnvelopeGenerator
|
||||
|
||||
// CapabilityTrustedIdentityVerifier is the name of the
|
||||
// capability for a plugin to support verifying trusted identities.
|
||||
CapabilityTrustedIdentityVerifier Capability = "SIGNATURE_VERIFIER.TRUSTED_IDENTITY"
|
||||
//
|
||||
// Deprecated: CapabilityTrustedIdentityVerifier exists for historical compatibility and should not be used.
|
||||
// To access CapabilityTrustedIdentityVerifier, use the notation-plugin-framework-go's [plugin.CapabilityTrustedIdentityVerifier].
|
||||
CapabilityTrustedIdentityVerifier = plugin.CapabilityTrustedIdentityVerifier
|
||||
|
||||
// CapabilityRevocationCheckVerifier is the name of the
|
||||
// capability for a plugin to support verifying revocation checks.
|
||||
CapabilityRevocationCheckVerifier Capability = "SIGNATURE_VERIFIER.REVOCATION_CHECK"
|
||||
//
|
||||
// Deprecated: CapabilityRevocationCheckVerifier exists for historical compatibility and should not be used.
|
||||
// To access CapabilityRevocationCheckVerifier, use the notation-plugin-framework-go's [plugin.CapabilityRevocationCheckVerifier].
|
||||
CapabilityRevocationCheckVerifier = plugin.CapabilityRevocationCheckVerifier
|
||||
)
|
||||
|
|
|
@ -1,71 +1,54 @@
|
|||
// 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 proto
|
||||
|
||||
// DescribeKeyRequest contains the parameters passed in a describe-key request.
|
||||
type DescribeKeyRequest struct {
|
||||
ContractVersion string `json:"contractVersion"`
|
||||
KeyID string `json:"keyId"`
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
import "github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
|
||||
func (DescribeKeyRequest) Command() Command {
|
||||
return CommandDescribeKey
|
||||
}
|
||||
// DescribeKeyRequest contains the parameters passed in a describe-key request.
|
||||
//
|
||||
// Deprecated: DescribeKeyRequest exists for historical compatibility and should not be used.
|
||||
// To access DescribeKeyRequest, use the notation-plugin-framework-go's [plugin.DescribeKeyRequest] type.
|
||||
type DescribeKeyRequest = plugin.DescribeKeyRequest
|
||||
|
||||
// DescribeKeyResponse is the response of a describe-key request.
|
||||
type DescribeKeyResponse struct {
|
||||
// The same key id as passed in the request.
|
||||
KeyID string `json:"keyId"`
|
||||
|
||||
// One of following supported key types:
|
||||
// https://github.com/notaryproject/notaryproject/blob/main/signature-specification.md#algorithm-selection
|
||||
KeySpec KeySpec `json:"keySpec"`
|
||||
}
|
||||
//
|
||||
// Deprecated: DescribeKeyResponse exists for historical compatibility and should not be used.
|
||||
// To access DescribeKeyResponse, use the notation-plugin-framework-go's [plugin.DescribeKeyResponse] type.
|
||||
type DescribeKeyResponse = plugin.DescribeKeyResponse
|
||||
|
||||
// GenerateSignatureRequest contains the parameters passed in a
|
||||
// generate-signature request.
|
||||
type GenerateSignatureRequest struct {
|
||||
ContractVersion string `json:"contractVersion"`
|
||||
KeyID string `json:"keyId"`
|
||||
KeySpec KeySpec `json:"keySpec"`
|
||||
Hash HashAlgorithm `json:"hashAlgorithm"`
|
||||
Payload []byte `json:"payload"`
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
|
||||
func (GenerateSignatureRequest) Command() Command {
|
||||
return CommandGenerateSignature
|
||||
}
|
||||
//
|
||||
// Deprecated: GenerateSignatureRequest exists for historical compatibility and should not be used.
|
||||
// To access GenerateSignatureRequest, use the notation-plugin-framework-go's [plugin.GenerateSignatureRequest] type.
|
||||
type GenerateSignatureRequest = plugin.GenerateSignatureRequest
|
||||
|
||||
// GenerateSignatureResponse is the response of a generate-signature request.
|
||||
type GenerateSignatureResponse struct {
|
||||
KeyID string `json:"keyId"`
|
||||
Signature []byte `json:"signature"`
|
||||
SigningAlgorithm string `json:"signingAlgorithm"`
|
||||
|
||||
// Ordered list of certificates starting with leaf certificate
|
||||
// and ending with root certificate.
|
||||
CertificateChain [][]byte `json:"certificateChain"`
|
||||
}
|
||||
//
|
||||
// Deprecated: GenerateSignatureResponse exists for historical compatibility and should not be used.
|
||||
// To access GenerateSignatureResponse, use the notation-plugin-framework-go's [plugin.GenerateSignatureResponse] type.
|
||||
type GenerateSignatureResponse = plugin.GenerateSignatureResponse
|
||||
|
||||
// GenerateEnvelopeRequest contains the parameters passed in a generate-envelope
|
||||
// request.
|
||||
type GenerateEnvelopeRequest struct {
|
||||
ContractVersion string `json:"contractVersion"`
|
||||
KeyID string `json:"keyId"`
|
||||
PayloadType string `json:"payloadType"`
|
||||
SignatureEnvelopeType string `json:"signatureEnvelopeType"`
|
||||
Payload []byte `json:"payload"`
|
||||
ExpiryDurationInSeconds uint64 `json:"expiryDurationInSeconds,omitempty"`
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
|
||||
func (GenerateEnvelopeRequest) Command() Command {
|
||||
return CommandGenerateEnvelope
|
||||
}
|
||||
//
|
||||
// Deprecated: GenerateEnvelopeRequest exists for historical compatibility and should not be used.
|
||||
// To access GenerateEnvelopeRequest, use the notation-plugin-framework-go's [plugin.GenerateEnvelopeRequest] type.
|
||||
type GenerateEnvelopeRequest = plugin.GenerateEnvelopeRequest
|
||||
|
||||
// GenerateEnvelopeResponse is the response of a generate-envelope request.
|
||||
type GenerateEnvelopeResponse struct {
|
||||
SignatureEnvelope []byte `json:"signatureEnvelope"`
|
||||
SignatureEnvelopeType string `json:"signatureEnvelopeType"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
||||
//
|
||||
// Deprecated: GenerateEnvelopeResponse exists for historical compatibility and should not be used.
|
||||
// To access GenerateEnvelopeResponse, use the notation-plugin-framework-go's [plugin.GenerateEnvelopeResponse] type.
|
||||
type GenerateEnvelopeResponse = plugin.GenerateEnvelopeResponse
|
||||
|
|
|
@ -1,51 +1,56 @@
|
|||
// 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 proto
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
)
|
||||
|
||||
// VerifySignatureRequest contains the parameters passed in a verify-signature
|
||||
// request.
|
||||
type VerifySignatureRequest struct {
|
||||
ContractVersion string `json:"contractVersion"`
|
||||
Signature Signature `json:"signature"`
|
||||
TrustPolicy TrustPolicy `json:"trustPolicy"`
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
|
||||
func (VerifySignatureRequest) Command() Command {
|
||||
return CommandVerifySignature
|
||||
}
|
||||
//
|
||||
// Deprecated: VerifySignatureRequest exists for historical compatibility and should not be used.
|
||||
// To access VerifySignatureRequest, use the notation-plugin-framework-go'[s plugin.VerifySignatureRequest] type.
|
||||
type VerifySignatureRequest = plugin.VerifySignatureRequest
|
||||
|
||||
// Signature represents a signature pulled from the envelope
|
||||
type Signature struct {
|
||||
CriticalAttributes CriticalAttributes `json:"criticalAttributes"`
|
||||
UnprocessedAttributes []string `json:"unprocessedAttributes"`
|
||||
CertificateChain [][]byte `json:"certificateChain"`
|
||||
}
|
||||
//
|
||||
// Deprecated: Signature exists for historical compatibility and should not be used.
|
||||
// To access Signature, use the notation-plugin-framework-go's [plugin.Signature] type.
|
||||
type Signature = plugin.Signature
|
||||
|
||||
// CriticalAttributes contains all Notary V2 defined critical
|
||||
// CriticalAttributes contains all Notary Project defined critical
|
||||
// attributes and their values in the signature envelope
|
||||
type CriticalAttributes struct {
|
||||
ContentType string `json:"contentType"`
|
||||
SigningScheme string `json:"signingScheme"`
|
||||
Expiry *time.Time `json:"expiry,omitempty"`
|
||||
AuthenticSigningTime *time.Time `json:"authenticSigningTime,omitempty"`
|
||||
ExtendedAttributes map[string]interface{} `json:"extendedAttributes,omitempty"`
|
||||
}
|
||||
//
|
||||
// Deprecated: CriticalAttributes exists for historical compatibility and should not be used.
|
||||
// To access CriticalAttributes, use the notation-plugin-framework-go's [plugin.CriticalAttributes] type.
|
||||
type CriticalAttributes = plugin.CriticalAttributes
|
||||
|
||||
// TrustPolicy represents trusted identities that sign the artifacts
|
||||
type TrustPolicy struct {
|
||||
TrustedIdentities []string `json:"trustedIdentities"`
|
||||
SignatureVerification []Capability `json:"signatureVerification"`
|
||||
}
|
||||
//
|
||||
// Deprecated: TrustPolicy exists for historical compatibility and should not be used.
|
||||
// To access TrustPolicy, use the notation-plugin-framework-go's [plugin.TrustPolicy] type.
|
||||
type TrustPolicy = plugin.TrustPolicy
|
||||
|
||||
// VerifySignatureResponse is the response of a verify-signature request.
|
||||
type VerifySignatureResponse struct {
|
||||
VerificationResults map[Capability]*VerificationResult `json:"verificationResults"`
|
||||
ProcessedAttributes []interface{} `json:"processedAttributes"`
|
||||
}
|
||||
//
|
||||
// Deprecated: VerifySignatureResponse exists for historical compatibility and should not be used.
|
||||
// To access VerifySignatureResponse, use the notation-plugin-framework-go's [plugin.VerifySignatureResponse] type.
|
||||
type VerifySignatureResponse = plugin.VerifySignatureResponse
|
||||
|
||||
// VerificationResult is the result of a verification performed by the plugin
|
||||
type VerificationResult struct {
|
||||
Success bool `json:"success"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
// VerificationResult is the result of a verification performed by the plugin.
|
||||
//
|
||||
// Deprecated: VerificationResult exists for historical compatibility and should not be used.
|
||||
// To access VerificationResult, use the notation-plugin-framework-go's [plugin.VerificationResult] type.
|
||||
type VerificationResult = plugin.VerificationResult
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 registry provides access to signatures in a registry
|
||||
package registry
|
||||
|
||||
|
@ -14,7 +27,7 @@ type Repository interface {
|
|||
Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error)
|
||||
|
||||
// ListSignatures returns signature manifests filtered by fn given the
|
||||
// artifact manifest descriptor
|
||||
// target artifact's manifest descriptor
|
||||
ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error
|
||||
|
||||
// FetchSignatureBlob returns signature envelope blob and descriptor for
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
// 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 artifactspec
|
||||
|
||||
import ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
// MediaTypeArtifactManifest specifies the media type for a content descriptor.
|
||||
const MediaTypeArtifactManifest = "application/vnd.oci.artifact.manifest.v1+json"
|
||||
|
||||
// Artifact describes an artifact manifest.
|
||||
// This structure provides `application/vnd.oci.artifact.manifest.v1+json` mediatype when marshalled to JSON.
|
||||
//
|
||||
// This manifest type was introduced in image-spec v1.1.0-rc1 and was removed in
|
||||
// image-spec v1.1.0-rc3. It is not part of the current image-spec and is kept
|
||||
// here for Go compatibility.
|
||||
//
|
||||
// Reference: https://github.com/opencontainers/image-spec/pull/999
|
||||
type Artifact struct {
|
||||
// MediaType is the media type of the object this schema refers to.
|
||||
MediaType string `json:"mediaType"`
|
||||
|
||||
// ArtifactType is the IANA media type of the artifact this schema refers to.
|
||||
ArtifactType string `json:"artifactType"`
|
||||
|
||||
// Blobs is a collection of blobs referenced by this manifest.
|
||||
Blobs []ocispec.Descriptor `json:"blobs,omitempty"`
|
||||
|
||||
// Subject (reference) is an optional link from the artifact to another manifest forming an association between the artifact and the other manifest.
|
||||
Subject *ocispec.Descriptor `json:"subject,omitempty"`
|
||||
|
||||
// Annotations contains arbitrary metadata for the artifact manifest.
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
}
|
|
@ -1,5 +1,18 @@
|
|||
// 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 registry
|
||||
|
||||
// ArtifactTypeNotation specifies the artifact type for a notation object.
|
||||
// spec: https://github.com/notaryproject/notaryproject/blob/efc828223710f99ab9639d2d0f72d59036a8e80c/specs/signature-specification.md#storage
|
||||
// spec: https://github.com/notaryproject/specifications/blob/v1.1.0/specs/signature-specification.md#signature
|
||||
const ArtifactTypeNotation = "application/vnd.cncf.notary.signature"
|
||||
|
|
|
@ -1,13 +1,30 @@
|
|||
// 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 registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
"github.com/notaryproject/notation-go/registry/internal/artifactspec"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/content/oci"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
)
|
||||
|
||||
|
@ -16,51 +33,71 @@ const (
|
|||
maxManifestSizeLimit = 4 * 1024 * 1024 // 4 MiB
|
||||
)
|
||||
|
||||
// RepositoryOptions provides user options when creating a Repository
|
||||
type RepositoryOptions struct {
|
||||
// OCIImageManifest specifies if user wants to use OCI image manifest
|
||||
// to store signatures in remote registries.
|
||||
// By default, Notation will use OCI artifact manifest to store signatures.
|
||||
// If OCIImageManifest flag is set to true, Notation will instead use
|
||||
// OCI image manifest.
|
||||
// Note, Notation will not automatically convert between these two types
|
||||
// on any occasion.
|
||||
// OCI artifact manifest: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/artifact.md
|
||||
// OCI image manifest: https://github.com/opencontainers/image-spec/blob/v1.1.0-rc2/manifest.md
|
||||
OCIImageManifest bool
|
||||
}
|
||||
// RepositoryOptions provides user options when creating a [Repository]
|
||||
// it is kept for future extensibility
|
||||
type RepositoryOptions struct{}
|
||||
|
||||
// repositoryClient implements Repository
|
||||
// repositoryClient implements [Repository]
|
||||
type repositoryClient struct {
|
||||
registry.Repository
|
||||
oras.GraphTarget
|
||||
RepositoryOptions
|
||||
}
|
||||
|
||||
// NewRepository returns a new Repository
|
||||
func NewRepository(repo registry.Repository) Repository {
|
||||
// NewRepository returns a new [Repository].
|
||||
// Known implementations of oras.GraphTarget:
|
||||
// - [remote.Repository](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote#Repository)
|
||||
// - [oci.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/oci#Store)
|
||||
func NewRepository(target oras.GraphTarget) Repository {
|
||||
return &repositoryClient{
|
||||
Repository: repo,
|
||||
GraphTarget: target,
|
||||
}
|
||||
}
|
||||
|
||||
// NewRepositoryWithOptions returns a new Repository with user specified
|
||||
// NewRepositoryWithOptions returns a new [Repository] with user specified
|
||||
// options.
|
||||
func NewRepositoryWithOptions(repo registry.Repository, opts RepositoryOptions) Repository {
|
||||
func NewRepositoryWithOptions(target oras.GraphTarget, opts RepositoryOptions) Repository {
|
||||
return &repositoryClient{
|
||||
Repository: repo,
|
||||
GraphTarget: target,
|
||||
RepositoryOptions: opts,
|
||||
}
|
||||
}
|
||||
|
||||
// NewOCIRepository returns a new [Repository] with oci.Store as
|
||||
// its oras.GraphTarget. `path` denotes directory path to the target OCI layout.
|
||||
func NewOCIRepository(path string, opts RepositoryOptions) (Repository, error) {
|
||||
fileInfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OCI store: %w", err)
|
||||
}
|
||||
if !fileInfo.IsDir() {
|
||||
return nil, fmt.Errorf("failed to create OCI store: the input path is not a directory")
|
||||
}
|
||||
ociStore, err := oci.New(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create OCI store: %w", err)
|
||||
}
|
||||
return NewRepositoryWithOptions(ociStore, opts), nil
|
||||
}
|
||||
|
||||
// Resolve resolves a reference(tag or digest) to a manifest descriptor
|
||||
func (c *repositoryClient) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
|
||||
return c.Repository.Manifests().Resolve(ctx, reference)
|
||||
if repo, ok := c.GraphTarget.(registry.Repository); ok {
|
||||
return repo.Manifests().Resolve(ctx, reference)
|
||||
}
|
||||
return c.GraphTarget.Resolve(ctx, reference)
|
||||
}
|
||||
|
||||
// ListSignatures returns signature manifests filtered by fn given the
|
||||
// artifact manifest descriptor
|
||||
// target artifact's manifest descriptor
|
||||
func (c *repositoryClient) ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error {
|
||||
return c.Repository.Referrers(ctx, desc, ArtifactTypeNotation, fn)
|
||||
if repo, ok := c.GraphTarget.(registry.ReferrerLister); ok {
|
||||
return repo.Referrers(ctx, desc, ArtifactTypeNotation, fn)
|
||||
}
|
||||
signatureManifests, err := signatureReferrers(ctx, c.GraphTarget, desc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get referrers during ListSignatures due to %w", err)
|
||||
}
|
||||
return fn(signatureManifests)
|
||||
}
|
||||
|
||||
// FetchSignatureBlob returns signature envelope blob and descriptor given
|
||||
|
@ -73,7 +110,11 @@ func (c *repositoryClient) FetchSignatureBlob(ctx context.Context, desc ocispec.
|
|||
if sigBlobDesc.Size > maxBlobSizeLimit {
|
||||
return nil, ocispec.Descriptor{}, fmt.Errorf("signature blob too large: %d bytes", sigBlobDesc.Size)
|
||||
}
|
||||
sigBlob, err := content.FetchAll(ctx, c.Repository.Blobs(), sigBlobDesc)
|
||||
var fetcher content.Fetcher = c.GraphTarget
|
||||
if repo, ok := c.GraphTarget.(registry.Repository); ok {
|
||||
fetcher = repo.Blobs()
|
||||
}
|
||||
sigBlob, err := content.FetchAll(ctx, fetcher, sigBlobDesc)
|
||||
if err != nil {
|
||||
return nil, ocispec.Descriptor{}, err
|
||||
}
|
||||
|
@ -84,33 +125,42 @@ func (c *repositoryClient) FetchSignatureBlob(ctx context.Context, desc ocispec.
|
|||
// linked signature envelope blob. Upon successful, PushSignature returns
|
||||
// signature envelope blob and manifest descriptors.
|
||||
func (c *repositoryClient) PushSignature(ctx context.Context, mediaType string, blob []byte, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) {
|
||||
blobDesc, err = oras.PushBytes(ctx, c.Repository.Blobs(), mediaType, blob)
|
||||
var pusher content.Pusher = c.GraphTarget
|
||||
if repo, ok := c.GraphTarget.(registry.Repository); ok {
|
||||
pusher = repo.Blobs()
|
||||
}
|
||||
blobDesc, err = oras.PushBytes(ctx, pusher, mediaType, blob)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
manifestDesc, err = c.uploadSignatureManifest(ctx, subject, blobDesc, annotations)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
return blobDesc, manifestDesc, nil
|
||||
}
|
||||
|
||||
// getSignatureBlobDesc returns signature blob descriptor from
|
||||
// signature manifest blobs or layers given signature manifest descriptor
|
||||
func (c *repositoryClient) getSignatureBlobDesc(ctx context.Context, sigManifestDesc ocispec.Descriptor) (ocispec.Descriptor, error) {
|
||||
if sigManifestDesc.MediaType != ocispec.MediaTypeArtifactManifest && sigManifestDesc.MediaType != ocispec.MediaTypeImageManifest {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("sigManifestDesc.MediaType requires %q or %q, got %q", ocispec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest, sigManifestDesc.MediaType)
|
||||
if sigManifestDesc.MediaType != artifactspec.MediaTypeArtifactManifest && sigManifestDesc.MediaType != ocispec.MediaTypeImageManifest {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("sigManifestDesc.MediaType requires %q or %q, got %q", artifactspec.MediaTypeArtifactManifest, ocispec.MediaTypeImageManifest, sigManifestDesc.MediaType)
|
||||
}
|
||||
if sigManifestDesc.Size > maxManifestSizeLimit {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("signature manifest too large: %d bytes", sigManifestDesc.Size)
|
||||
}
|
||||
manifestJSON, err := content.FetchAll(ctx, c.Repository.Manifests(), sigManifestDesc)
|
||||
|
||||
// get the signature manifest from sigManifestDesc
|
||||
var fetcher content.Fetcher = c.GraphTarget
|
||||
if repo, ok := c.GraphTarget.(registry.Repository); ok {
|
||||
fetcher = repo.Manifests()
|
||||
}
|
||||
manifestJSON, err := content.FetchAll(ctx, fetcher, sigManifestDesc)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
|
||||
// get the signature blob descriptor from signature manifest
|
||||
var signatureBlobs []ocispec.Descriptor
|
||||
|
||||
// OCI image manifest
|
||||
|
@ -121,27 +171,111 @@ func (c *repositoryClient) getSignatureBlobDesc(ctx context.Context, sigManifest
|
|||
}
|
||||
signatureBlobs = sigManifest.Layers
|
||||
} else { // OCI artifact manifest
|
||||
var sigManifest ocispec.Artifact
|
||||
var sigManifest artifactspec.Artifact
|
||||
if err := json.Unmarshal(manifestJSON, &sigManifest); err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
signatureBlobs = sigManifest.Blobs
|
||||
}
|
||||
|
||||
if len(signatureBlobs) != 1 {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("signature manifest requries exactly one signature envelope blob, got %d", len(signatureBlobs))
|
||||
}
|
||||
|
||||
return signatureBlobs[0], nil
|
||||
}
|
||||
|
||||
// uploadSignatureManifest uploads the signature manifest to the registry
|
||||
func (c *repositoryClient) uploadSignatureManifest(ctx context.Context, subject, blobDesc ocispec.Descriptor, annotations map[string]string) (ocispec.Descriptor, error) {
|
||||
opts := oras.PackOptions{
|
||||
opts := oras.PackManifestOptions{
|
||||
Subject: &subject,
|
||||
ManifestAnnotations: annotations,
|
||||
PackImageManifest: c.OCIImageManifest,
|
||||
Layers: []ocispec.Descriptor{blobDesc},
|
||||
}
|
||||
|
||||
return oras.Pack(ctx, c.Repository, ArtifactTypeNotation, []ocispec.Descriptor{blobDesc}, opts)
|
||||
return oras.PackManifest(ctx, c.GraphTarget, oras.PackManifestVersion1_1, ArtifactTypeNotation, opts)
|
||||
}
|
||||
|
||||
// signatureReferrers returns referrer nodes of desc in target filtered by
|
||||
// the "application/vnd.cncf.notary.signature" artifact type
|
||||
func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
|
||||
var results []ocispec.Descriptor
|
||||
predecessors, err := target.Predecessors(ctx, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, node := range predecessors {
|
||||
switch node.MediaType {
|
||||
case artifactspec.MediaTypeArtifactManifest:
|
||||
if node.Size > maxManifestSizeLimit {
|
||||
return nil, fmt.Errorf("referrer node too large: %d bytes", node.Size)
|
||||
}
|
||||
fetched, err := content.FetchAll(ctx, target, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var artifact artifactspec.Artifact
|
||||
if err := json.Unmarshal(fetched, &artifact); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if artifact.Subject == nil || !content.Equal(*artifact.Subject, desc) {
|
||||
continue
|
||||
}
|
||||
if artifact.ArtifactType != ArtifactTypeNotation {
|
||||
// not a valid Notary Project signature
|
||||
continue
|
||||
}
|
||||
node.ArtifactType = artifact.ArtifactType
|
||||
node.Annotations = artifact.Annotations
|
||||
case ocispec.MediaTypeImageManifest:
|
||||
if node.Size > maxManifestSizeLimit {
|
||||
return nil, fmt.Errorf("referrer node too large: %d bytes", node.Size)
|
||||
}
|
||||
fetched, err := content.FetchAll(ctx, target, node)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var image ocispec.Manifest
|
||||
if err := json.Unmarshal(fetched, &image); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if image.Subject == nil || !content.Equal(*image.Subject, desc) {
|
||||
continue
|
||||
}
|
||||
|
||||
// check if image is a valid Notary Project signature
|
||||
switch image.ArtifactType {
|
||||
case ArtifactTypeNotation:
|
||||
// 1. artifactType is "application/vnd.cncf.notary.signature",
|
||||
// and config.mediaType is "application/vnd.oci.empty.v1+json"
|
||||
if image.Config.MediaType == ocispec.MediaTypeEmptyJSON {
|
||||
node.ArtifactType = image.ArtifactType
|
||||
} else {
|
||||
// not a valid Notary Project signature
|
||||
logger.Warnf("not a valid Notary Project signature with artifactType %q, but config.mediaType is %q", image.ArtifactType, image.Config.MediaType)
|
||||
continue
|
||||
}
|
||||
case "":
|
||||
// 2. artifacteType does not exist,
|
||||
// and config.mediaType is "application/vnd.cncf.notary.signature"
|
||||
if image.Config.MediaType == ArtifactTypeNotation {
|
||||
node.ArtifactType = image.Config.MediaType
|
||||
} else {
|
||||
// not a valid Notary Project signature
|
||||
continue
|
||||
}
|
||||
default:
|
||||
// not a valid Notary Project signature
|
||||
continue
|
||||
}
|
||||
node.Annotations = image.Annotations
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
// add the node to results
|
||||
results = append(results, node)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
|
|
@ -1,25 +1,49 @@
|
|||
// 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 registry
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/notaryproject/notation-go/internal/envelope"
|
||||
"github.com/notaryproject/notation-go/internal/mock/ocilayout"
|
||||
"github.com/notaryproject/notation-go/internal/slices"
|
||||
"github.com/notaryproject/notation-go/registry/internal/artifactspec"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
"oras.land/oras-go/v2/content/memory"
|
||||
"oras.land/oras-go/v2/content/oci"
|
||||
"oras.land/oras-go/v2/registry"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
)
|
||||
|
||||
const (
|
||||
zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
|
||||
emptyConfigDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
|
||||
validDigest = "6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b"
|
||||
validDigest2 = "1834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f2"
|
||||
invalidDigest = "invaliddigest"
|
||||
|
@ -43,7 +67,7 @@ const (
|
|||
{
|
||||
"Manifests": [
|
||||
{
|
||||
"MediaType": "application/vnd.oci.artifact.manifest.v1+json",
|
||||
"MediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||
"Digest": "sha256:cf2a0974295fc17b8351ef52abae2f40212e20e0359ea980ec5597bb0315347b",
|
||||
"Size": 620,
|
||||
"ArtifactType": "application/vnd.cncf.notary.signature"
|
||||
|
@ -69,7 +93,10 @@ const (
|
|||
}`
|
||||
)
|
||||
|
||||
var validDigestWithAlgoSlice = []string{validDigestWithAlgo, validDigestWithAlgo2}
|
||||
var (
|
||||
validDigestWithAlgoSlice = []string{validDigestWithAlgo, validDigestWithAlgo2}
|
||||
signaturePath = filepath.FromSlash("../internal/testdata/jws_signature.sig")
|
||||
)
|
||||
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
|
@ -108,8 +135,13 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
|
|||
"Docker-Content-Digest": {validDigestWithAlgo2},
|
||||
},
|
||||
}, nil
|
||||
case "/v2/test/blobs/" + emptyConfigDigest:
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusNotFound,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte{})),
|
||||
}, nil
|
||||
case "/v2/test/manifests/" + invalidDigest:
|
||||
return &http.Response{}, fmt.Errorf(errMsg)
|
||||
return &http.Response{}, errors.New(errMsg)
|
||||
case "v2/test/manifest/" + validDigest2:
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
|
@ -133,21 +165,39 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
|
|||
},
|
||||
}, nil
|
||||
default:
|
||||
return &http.Response{}, fmt.Errorf(msg)
|
||||
return &http.Response{}, errors.New(msg)
|
||||
}
|
||||
case "/v2/test/referrers/":
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(validPage))),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{ocispec.MediaTypeImageIndex},
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(validPage))),
|
||||
Request: &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/v2/test/referrers/"},
|
||||
},
|
||||
}, nil
|
||||
case "/v2/test/referrers/" + validDigestWithAlgo:
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{ocispec.MediaTypeImageIndex},
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(validPage))),
|
||||
Request: &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/v2/test/referrers/" + validDigestWithAlgo},
|
||||
},
|
||||
}, nil
|
||||
case "/v2/test/referrers/" + zeroDigest:
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(validPageImage))),
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{ocispec.MediaTypeImageIndex},
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(validPageImage))),
|
||||
Request: &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/v2/test/referrers/" + zeroDigest},
|
||||
|
@ -161,15 +211,17 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
|
|||
default:
|
||||
_, digest, found := strings.Cut(req.URL.Path, "/v2/test/manifests/")
|
||||
if found && !slices.Contains(validDigestWithAlgoSlice, digest) {
|
||||
return &http.Response{
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusCreated,
|
||||
Body: io.NopCloser(bytes.NewReader([]byte(msg))),
|
||||
Header: map[string][]string{
|
||||
"Content-Type": {joseTag},
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{joseTag},
|
||||
"Oci-Subject": []string{validDigestWithAlgo},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
return &http.Response{}, fmt.Errorf(errMsg)
|
||||
return &http.Response{}, errors.New(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,7 +278,7 @@ func TestFetchSignatureBlob(t *testing.T) {
|
|||
remoteClient: mockRemoteClient{},
|
||||
plainHttp: false,
|
||||
signatureManifestDesc: ocispec.Descriptor{
|
||||
MediaType: ocispec.MediaTypeArtifactManifest,
|
||||
MediaType: artifactspec.MediaTypeArtifactManifest,
|
||||
Digest: digest.Digest(invalidDigest),
|
||||
},
|
||||
},
|
||||
|
@ -241,7 +293,7 @@ func TestFetchSignatureBlob(t *testing.T) {
|
|||
remoteClient: mockRemoteClient{},
|
||||
plainHttp: false,
|
||||
signatureManifestDesc: ocispec.Descriptor{
|
||||
MediaType: ocispec.MediaTypeArtifactManifest,
|
||||
MediaType: artifactspec.MediaTypeArtifactManifest,
|
||||
Digest: digest.Digest(validDigestWithAlgo2),
|
||||
},
|
||||
},
|
||||
|
@ -280,16 +332,24 @@ func TestListSignatures(t *testing.T) {
|
|||
reference: validReference,
|
||||
remoteClient: mockRemoteClient{},
|
||||
plainHttp: false,
|
||||
artifactManifestDesc: ocispec.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: validDigestWithAlgo,
|
||||
Size: 481,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
args := tt.args
|
||||
ref, _ := registry.ParseReference(args.reference)
|
||||
ref, err := registry.ParseReference(args.reference)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client := newRepositoryClient(args.remoteClient, ref, args.plainHttp)
|
||||
|
||||
err := client.ListSignatures(args.ctx, args.artifactManifestDesc, func(signatureManifests []ocispec.Descriptor) error {
|
||||
err = client.ListSignatures(args.ctx, args.artifactManifestDesc, func(signatureManifests []ocispec.Descriptor) error {
|
||||
if len(signatureManifests) != 1 {
|
||||
return fmt.Errorf("length of signatureManifests expected 1, got %d", len(signatureManifests))
|
||||
}
|
||||
|
@ -309,6 +369,10 @@ func TestListSignatures(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPushSignature(t *testing.T) {
|
||||
signature, err := os.ReadFile(signaturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read signature: %v", err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
|
@ -320,20 +384,30 @@ func TestPushSignature(t *testing.T) {
|
|||
name: "failed to upload signature",
|
||||
expectErr: true,
|
||||
args: args{
|
||||
reference: referenceWithInvalidHost,
|
||||
signature: make([]byte, 0),
|
||||
ctx: context.Background(),
|
||||
remoteClient: mockRemoteClient{},
|
||||
reference: referenceWithInvalidHost,
|
||||
signatureMediaType: joseTag,
|
||||
signature: signature,
|
||||
ctx: context.Background(),
|
||||
remoteClient: mockRemoteClient{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successfully uploaded signature manifest",
|
||||
expectErr: false,
|
||||
args: args{
|
||||
reference: validReference,
|
||||
signature: make([]byte, 0),
|
||||
ctx: context.Background(),
|
||||
remoteClient: mockRemoteClient{},
|
||||
reference: validReference,
|
||||
signatureMediaType: joseTag,
|
||||
signature: signature,
|
||||
ctx: context.Background(),
|
||||
remoteClient: mockRemoteClient{},
|
||||
subjectManifest: ocispec.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: validDigestWithAlgo,
|
||||
Size: 481,
|
||||
},
|
||||
annotations: map[string]string{
|
||||
envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -357,8 +431,11 @@ func TestPushSignatureImageManifest(t *testing.T) {
|
|||
t.Fatalf("failed to parse reference")
|
||||
}
|
||||
client := newRepositoryClientWithImageManifest(mockRemoteClient{}, ref, false)
|
||||
|
||||
_, manifestDesc, err := client.PushSignature(context.Background(), coseTag, make([]byte, 0), ocispec.Descriptor{}, nil)
|
||||
signature, err := os.ReadFile(signaturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read signature: %v", err)
|
||||
}
|
||||
_, manifestDesc, err := client.PushSignature(context.Background(), joseTag, signature, ocispec.Descriptor{}, annotations)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to push signature")
|
||||
}
|
||||
|
@ -375,7 +452,7 @@ func newRepositoryClient(client remote.Client, ref registry.Reference, plainHTTP
|
|||
PlainHTTP: plainHTTP,
|
||||
}
|
||||
return &repositoryClient{
|
||||
Repository: &repo,
|
||||
GraphTarget: &repo,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -383,13 +460,570 @@ func newRepositoryClient(client remote.Client, ref registry.Reference, plainHTTP
|
|||
// pushing OCI image manifest
|
||||
func newRepositoryClientWithImageManifest(client remote.Client, ref registry.Reference, plainHTTP bool) *repositoryClient {
|
||||
return &repositoryClient{
|
||||
Repository: &remote.Repository{
|
||||
GraphTarget: &remote.Repository{
|
||||
Client: client,
|
||||
Reference: ref,
|
||||
PlainHTTP: plainHTTP,
|
||||
},
|
||||
RepositoryOptions: RepositoryOptions{
|
||||
OCIImageManifest: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
reference = "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
|
||||
expectedTargetDesc = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0",
|
||||
Size: 481,
|
||||
}
|
||||
annotations = map[string]string{
|
||||
envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]",
|
||||
ocispec.AnnotationCreated: "2023-03-14T08:10:02Z",
|
||||
}
|
||||
expectedSignatureManifestDesc = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: "sha256:64300ad03f1dcd18136787363f3069c9598623221cbe76e3233d35266b7973d6",
|
||||
Size: 793,
|
||||
}
|
||||
expectedSignatureBlobDesc = ocispec.Descriptor{
|
||||
MediaType: joseTag,
|
||||
Digest: "sha256:586c5e0f341d7d07e835a06b7c9f21c21fff4f4a85933079e5859f99ba0ad02d",
|
||||
Size: 2078,
|
||||
}
|
||||
)
|
||||
|
||||
func TestOciLayoutRepositoryPushAndFetch(t *testing.T) {
|
||||
// create a temp OCI layout
|
||||
ociLayoutTestdataPath, err := filepath.Abs(filepath.Join("..", "internal", "testdata", "oci-layout"))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get oci layout path: %v", err)
|
||||
}
|
||||
|
||||
newOCILayoutPath := t.TempDir()
|
||||
if err := ocilayout.Copy(ociLayoutTestdataPath, newOCILayoutPath, "v2"); err != nil {
|
||||
t.Fatalf("failed to create temp oci layout: %v", err)
|
||||
}
|
||||
repo, err := NewOCIRepository(newOCILayoutPath, RepositoryOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create oci.Store as registry.Repository: %v", err)
|
||||
}
|
||||
|
||||
// test resolve
|
||||
targetDesc, err := repo.Resolve(context.Background(), reference)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to resolve reference: %v", err)
|
||||
}
|
||||
if !content.Equal(targetDesc, expectedTargetDesc) {
|
||||
t.Fatalf("failed to resolve reference. expected descriptor: %v, but got: %v", expectedTargetDesc, targetDesc)
|
||||
}
|
||||
|
||||
t.Run("oci layout push", func(t *testing.T) {
|
||||
signature, err := os.ReadFile(signaturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read signature: %v", err)
|
||||
}
|
||||
_, signatureManifestDesc, err := repo.PushSignature(context.Background(), joseTag, signature, targetDesc, annotations)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to push signature: %v", err)
|
||||
}
|
||||
if !content.Equal(expectedSignatureManifestDesc, signatureManifestDesc) {
|
||||
t.Fatalf("expected desc: %v, got: %v", expectedSignatureManifestDesc, signatureManifestDesc)
|
||||
}
|
||||
expectedAnnotations := map[string]string{
|
||||
envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]",
|
||||
ocispec.AnnotationCreated: "2023-03-14T08:10:02Z",
|
||||
}
|
||||
if !reflect.DeepEqual(expectedAnnotations, signatureManifestDesc.Annotations) {
|
||||
t.Fatalf("expected annotations: %v, but got: %v", expectedAnnotations, signatureManifestDesc.Annotations)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("oci layout fetch", func(t *testing.T) {
|
||||
err = repo.ListSignatures(context.Background(), targetDesc, func(signatureManifests []ocispec.Descriptor) error {
|
||||
if len(signatureManifests) == 0 {
|
||||
return fmt.Errorf("expected to find signature in the OCI layout folder, but got none")
|
||||
}
|
||||
var found bool
|
||||
for _, sigManifestDesc := range signatureManifests {
|
||||
if !content.Equal(sigManifestDesc, expectedSignatureManifestDesc) {
|
||||
continue
|
||||
}
|
||||
_, sigDesc, err := repo.FetchSignatureBlob(context.Background(), sigManifestDesc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch blob: %w", err)
|
||||
}
|
||||
if !content.Equal(expectedSignatureBlobDesc, sigDesc) {
|
||||
return fmt.Errorf("expected to get signature blob desc: %v, got: %v", expectedSignatureBlobDesc, sigDesc)
|
||||
}
|
||||
found = true
|
||||
}
|
||||
if !found {
|
||||
return fmt.Errorf("expected to find the signature with manifest desc: %v, but failed", expectedSignatureManifestDesc)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNewRepository(t *testing.T) {
|
||||
target, err := oci.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create oci.Store as registry.Repository: %v", err)
|
||||
}
|
||||
repo := NewRepository(target)
|
||||
if repo == nil {
|
||||
t.Fatalf("failed to create repository")
|
||||
}
|
||||
repoClient, ok := repo.(*repositoryClient)
|
||||
if !ok {
|
||||
t.Fatalf("failed to create repositoryClient")
|
||||
}
|
||||
if target != repoClient.GraphTarget {
|
||||
t.Fatalf("expected target: %v, got: %v", target, repoClient.GraphTarget)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOCIRepositoryFailed(t *testing.T) {
|
||||
t.Run("os stat failed", func(t *testing.T) {
|
||||
_, err := NewOCIRepository("invalid-path", RepositoryOptions{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with invalid path")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("path is regular file", func(t *testing.T) {
|
||||
// create a regular file in the temp dir
|
||||
filePath := filepath.Join(t.TempDir(), "file")
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create file: %v", err)
|
||||
}
|
||||
file.Close()
|
||||
|
||||
_, err = NewOCIRepository(filePath, RepositoryOptions{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with regular file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no permission to create new path", func(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows")
|
||||
}
|
||||
// create a directory in the temp dir
|
||||
dirPath := filepath.Join(t.TempDir(), "dir")
|
||||
err := os.Mkdir(dirPath, 0000)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create dir: %v", err)
|
||||
}
|
||||
|
||||
_, err = NewOCIRepository(dirPath, RepositoryOptions{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with no permission to create new path")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// testStorage implements content.ReadOnlyGraphStorage
|
||||
type testStorage struct {
|
||||
store *memory.Store
|
||||
FetchError error
|
||||
FetchContent []byte
|
||||
PredecessorsError error
|
||||
PredecessorsDesc []ocispec.Descriptor
|
||||
}
|
||||
|
||||
func (s *testStorage) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error {
|
||||
return s.store.Push(ctx, expected, reader)
|
||||
}
|
||||
|
||||
func (s *testStorage) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
|
||||
if s.FetchError != nil {
|
||||
return nil, s.FetchError
|
||||
}
|
||||
return io.NopCloser(bytes.NewReader(s.FetchContent)), nil
|
||||
}
|
||||
|
||||
func (s *testStorage) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
|
||||
return s.store.Exists(ctx, target)
|
||||
}
|
||||
|
||||
func (s *testStorage) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
if s.PredecessorsError != nil {
|
||||
return nil, s.PredecessorsError
|
||||
}
|
||||
return s.PredecessorsDesc, nil
|
||||
}
|
||||
|
||||
func TestSignatureReferrers(t *testing.T) {
|
||||
t.Run("get predecessors failed", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsError: fmt.Errorf("failed to get predecessors"),
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with getting predecessors")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("artifact manifest exceds max blob size", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: validDigestWithAlgo2,
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 4*1024*1024 + 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: validDigestWithAlgo2,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with artifact manifest exceds max blob size")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest exceds max blob size", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: validDigestWithAlgo2,
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 4*1024*1024 + 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: validDigestWithAlgo2,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with image manifest exceds max blob size")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("artifact manifest fetchAll failed", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: validDigestWithAlgo,
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 481,
|
||||
},
|
||||
},
|
||||
FetchError: fmt.Errorf("failed to fetch all"),
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: validDigestWithAlgo,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with fetchAll failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest fetchAll failed", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: validDigestWithAlgo,
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 481,
|
||||
},
|
||||
},
|
||||
FetchError: fmt.Errorf("failed to fetch all"),
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: validDigestWithAlgo,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with fetchAll failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("artifact manifest marshal failed", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 15,
|
||||
},
|
||||
},
|
||||
FetchContent: []byte("invalid content"),
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with marshal failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest marshal failed", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 15,
|
||||
},
|
||||
},
|
||||
FetchContent: []byte("invalid content"),
|
||||
}
|
||||
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatalf("expected to fail with marshal failed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no valid artifact manifest", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 2,
|
||||
},
|
||||
},
|
||||
FetchContent: []byte("{}"),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("artifact manifest with invalid artifactType", func(t *testing.T) {
|
||||
sigManifest := `{"artifactType":"invalid", "subject":{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:835c3386406350fbddf5ee376b358bd20c6c423d6becbec166f83c533e4df5d6",
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 198,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("no valid image manifest", func(t *testing.T) {
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{
|
||||
{
|
||||
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
},
|
||||
},
|
||||
FetchContent: []byte("{}"),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with invalid mediaType", func(t *testing.T) {
|
||||
sigManifest := `{}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "invalid",
|
||||
Size: 2,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatal("expected length of descriptors to be 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with valid artifactType and config.MediaType", func(t *testing.T) {
|
||||
sigManifest := `{"artifactType":"application/vnd.cncf.notary.signature","config":{"mediaType":"application/vnd.oci.empty.v1+json"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:ad3ab7874c72d7bf5db0e55ce839b37ee71320bf7c18ac1a512600963f03c54d",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 283,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 1 {
|
||||
t.Fatal("expected length of descriptors to be 1")
|
||||
}
|
||||
if !content.Equal(sigManifestDesc, descriptors[0]) {
|
||||
t.Fatalf("expected %v, got: %v", sigManifestDesc, descriptors[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with valid artifactType but invalid config.MediaType", func(t *testing.T) {
|
||||
sigManifest := `{"artifactType":"application/vnd.cncf.notary.signature","config":{"mediaType":"invalid"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:becfe1975b40352d0c7bd1337707a4c471fdcfa1ac380f2875fe8076a3bc3581",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 257,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatal("expected length of descriptors to be 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with no artifactType and valid config.MediaType", func(t *testing.T) {
|
||||
sigManifest := `{"config":{"mediaType":"application/vnd.cncf.notary.signature"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:0e0be61f687ba634dd772f6d3048101f78f22fabda64cc9600671cee41ab2d47",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 232,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 1 {
|
||||
t.Fatal("expected length of descriptors to be 1")
|
||||
}
|
||||
if !content.Equal(sigManifestDesc, descriptors[0]) {
|
||||
t.Fatalf("expected %v, got: %v", sigManifestDesc, descriptors[0])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with no artifactType and invalid config.MediaType", func(t *testing.T) {
|
||||
sigManifest := `{"config":{"mediaType":"invalid"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:1580e4f590269bd40a33e902888429c9bbb250902f5a7eb50f04fbb8bd4dbab3",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 202,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatal("expected length of descriptors to be 0")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image manifest with invalid artifactType", func(t *testing.T) {
|
||||
sigManifest := `{"artifactType":"invalid","config":{"mediaType":"application/vnd.oci.empty.v1+json"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
|
||||
sigManifestDesc := ocispec.Descriptor{
|
||||
Digest: "sha256:d8c225cb4eca3e15fa2a44c9d302044e8c8683399939e26f417edb82f8b69cc3",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 253,
|
||||
}
|
||||
store := &testStorage{
|
||||
store: &memory.Store{},
|
||||
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
|
||||
FetchContent: []byte(sigManifest),
|
||||
}
|
||||
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
|
||||
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Size: 2,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get referrers: %v", err)
|
||||
}
|
||||
if len(descriptors) != 0 {
|
||||
t.Fatal("expected length of descriptors to be 0")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUploadSignatureManifest(t *testing.T) {
|
||||
ref, err := registry.ParseReference(validReference)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse reference")
|
||||
}
|
||||
client := newRepositoryClientWithImageManifest(mockRemoteClient{}, ref, false)
|
||||
manifest, err := client.uploadSignatureManifest(context.Background(),
|
||||
ocispec.Descriptor{}, ocispec.Descriptor{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to upload signature manifest: %v", err)
|
||||
}
|
||||
if manifest.ArtifactType != ArtifactTypeNotation {
|
||||
t.Fatalf("expected artifact type: %s, got: %s", ArtifactTypeNotation, manifest.ArtifactType)
|
||||
}
|
||||
}
|
||||
|
|
203
signer/plugin.go
203
signer/plugin.go
|
@ -1,44 +1,76 @@
|
|||
// 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 signer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"oras.land/oras-go/v2/content"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/internal/envelope"
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
"github.com/notaryproject/notation-go/plugin"
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"oras.land/oras-go/v2/content"
|
||||
)
|
||||
|
||||
// pluginSigner signs artifacts and generates signatures.
|
||||
// It implements notation.Signer
|
||||
type pluginSigner struct {
|
||||
// PluginSigner signs artifacts and generates signatures.
|
||||
//
|
||||
// It implements [notation.Signer] and [notation.BlobSigner].
|
||||
type PluginSigner struct {
|
||||
plugin plugin.SignPlugin
|
||||
keyID string
|
||||
pluginConfig map[string]string
|
||||
manifestAnnotations map[string]string
|
||||
}
|
||||
|
||||
// NewFromPlugin creates a notation.Signer that signs artifacts and generates
|
||||
var algorithms = map[crypto.Hash]digest.Algorithm{
|
||||
crypto.SHA256: digest.SHA256,
|
||||
crypto.SHA384: digest.SHA384,
|
||||
crypto.SHA512: digest.SHA512,
|
||||
}
|
||||
|
||||
// NewFromPlugin creates a [PluginSigner] that signs artifacts and generates
|
||||
// signatures by delegating the one or more operations to the named plugin,
|
||||
// as defined in https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md#signing-interfaces.
|
||||
func NewFromPlugin(plugin plugin.Plugin, keyID string, pluginConfig map[string]string) (notation.Signer, error) {
|
||||
//
|
||||
// Deprecated: NewFromPlugin function exists for historical compatibility and
|
||||
// should not be used. To create [PluginSigner], use NewPluginSigner() function.
|
||||
func NewFromPlugin(plugin plugin.SignPlugin, keyID string, pluginConfig map[string]string) (notation.Signer, error) {
|
||||
return NewPluginSigner(plugin, keyID, pluginConfig)
|
||||
}
|
||||
|
||||
// NewPluginSigner creates a [PluginSigner] that signs artifacts and generates
|
||||
// signatures by delegating the one or more operations to the named plugin,
|
||||
// as defined in https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md#signing-interfaces.
|
||||
func NewPluginSigner(plugin plugin.SignPlugin, keyID string, pluginConfig map[string]string) (*PluginSigner, error) {
|
||||
if plugin == nil {
|
||||
return nil, errors.New("nil plugin")
|
||||
}
|
||||
if keyID == "" {
|
||||
return nil, errors.New("keyID not specified")
|
||||
}
|
||||
|
||||
return &pluginSigner{
|
||||
return &PluginSigner{
|
||||
plugin: plugin,
|
||||
keyID: keyID,
|
||||
pluginConfig: pluginConfig,
|
||||
|
@ -46,66 +78,103 @@ func NewFromPlugin(plugin plugin.Plugin, keyID string, pluginConfig map[string]s
|
|||
}
|
||||
|
||||
// PluginAnnotations returns signature manifest annotations returned from plugin
|
||||
func (s *pluginSigner) PluginAnnotations() map[string]string {
|
||||
func (s *PluginSigner) PluginAnnotations() map[string]string {
|
||||
return s.manifestAnnotations
|
||||
}
|
||||
|
||||
// Sign signs the artifact described by its descriptor and returns the
|
||||
// marshalled envelope.
|
||||
func (s *pluginSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
// signature and SignerInfo.
|
||||
func (s *PluginSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
mergedConfig := s.mergeConfig(opts.PluginConfig)
|
||||
logger.Debug("Invoking plugin's get-plugin-metadata command")
|
||||
req := &proto.GetMetadataRequest{
|
||||
PluginConfig: s.mergeConfig(opts.PluginConfig),
|
||||
}
|
||||
metadata, err := s.plugin.GetMetadata(ctx, req)
|
||||
metadata, err := s.plugin.GetMetadata(ctx, &plugin.GetMetadataRequest{PluginConfig: mergedConfig})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
logger.Debugf("Using plugin %v with capabilities %v to sign artifact %v in signature media type %v", metadata.Name, metadata.Capabilities, desc.Digest, opts.SignatureMediaType)
|
||||
if metadata.HasCapability(proto.CapabilitySignatureGenerator) {
|
||||
return s.generateSignature(ctx, desc, opts, metadata)
|
||||
} else if metadata.HasCapability(proto.CapabilityEnvelopeGenerator) {
|
||||
return s.generateSignatureEnvelope(ctx, desc, opts)
|
||||
logger.Debugf("Using plugin %v with capabilities %v to sign oci artifact %v in signature media type %v", metadata.Name, metadata.Capabilities, desc.Digest, opts.SignatureMediaType)
|
||||
if metadata.HasCapability(plugin.CapabilitySignatureGenerator) {
|
||||
ks, err := s.getKeySpec(ctx, mergedConfig)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
|
||||
}
|
||||
sig, signerInfo, err := s.generateSignature(ctx, desc, opts, ks, metadata, mergedConfig)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
|
||||
}
|
||||
return sig, signerInfo, nil
|
||||
} else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) {
|
||||
sig, signerInfo, err := s.generateSignatureEnvelope(ctx, desc, opts)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
|
||||
}
|
||||
return sig, signerInfo, nil
|
||||
}
|
||||
return nil, nil, fmt.Errorf("plugin does not have signing capabilities")
|
||||
}
|
||||
|
||||
func (s *pluginSigner) generateSignature(ctx context.Context, desc ocispec.Descriptor, opts notation.SignOptions, metadata *proto.GetMetadataResponse) ([]byte, *signature.SignerInfo, error) {
|
||||
// SignBlob signs the descriptor returned by genDesc, and returns the
|
||||
// signature and SignerInfo.
|
||||
func (s *PluginSigner) SignBlob(ctx context.Context, descGenFunc notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
mergedConfig := s.mergeConfig(opts.PluginConfig)
|
||||
logger.Debug("Invoking plugin's get-plugin-metadata command")
|
||||
metadata, err := s.plugin.GetMetadata(ctx, &plugin.GetMetadataRequest{PluginConfig: mergedConfig})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// only support blob signing with the signature generator capability because
|
||||
// the envelope generator capability is designed for OCI signing.
|
||||
// A new capability may be added in the future for blob signing.
|
||||
if !metadata.HasCapability(plugin.CapabilitySignatureGenerator) {
|
||||
return nil, nil, fmt.Errorf("the plugin %q lacks the signature generator capability required for blob signing", metadata.Name)
|
||||
}
|
||||
|
||||
logger.Debug("Invoking plugin's describe-key command")
|
||||
ks, err := s.getKeySpec(ctx, mergedConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// get descriptor to sign
|
||||
desc, err := getDescriptor(ks, descGenFunc)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
logger.Debugf("Using plugin %v with capabilities %v to sign blob using descriptor %+v", metadata.Name, metadata.Capabilities, desc)
|
||||
return s.generateSignature(ctx, desc, opts, ks, metadata, mergedConfig)
|
||||
}
|
||||
|
||||
func (s *PluginSigner) getKeySpec(ctx context.Context, config map[string]string) (signature.KeySpec, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debug("Invoking plugin's describe-key command")
|
||||
descKeyResp, err := s.describeKey(ctx, config)
|
||||
if err != nil {
|
||||
return signature.KeySpec{}, err
|
||||
}
|
||||
if s.keyID != descKeyResp.KeyID {
|
||||
return signature.KeySpec{}, fmt.Errorf("keyID in describeKey response %q does not match request %q", descKeyResp.KeyID, s.keyID)
|
||||
}
|
||||
return proto.DecodeKeySpec(descKeyResp.KeySpec)
|
||||
}
|
||||
|
||||
func (s *PluginSigner) generateSignature(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions, ks signature.KeySpec, metadata *plugin.GetMetadataResponse, pluginConfig map[string]string) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debug("Generating signature by plugin")
|
||||
config := s.mergeConfig(opts.PluginConfig)
|
||||
// Get key info.
|
||||
key, err := s.describeKey(ctx, config)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Check keyID is honored.
|
||||
if s.keyID != key.KeyID {
|
||||
return nil, nil, fmt.Errorf("keyID in describeKey response %q does not match request %q", key.KeyID, s.keyID)
|
||||
}
|
||||
ks, err := proto.DecodeKeySpec(key.KeySpec)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
genericSigner := genericSigner{
|
||||
Signer: &pluginPrimitiveSigner{
|
||||
genericSigner := GenericSigner{
|
||||
signer: &pluginPrimitiveSigner{
|
||||
ctx: ctx,
|
||||
plugin: s.plugin,
|
||||
keyID: s.keyID,
|
||||
pluginConfig: config,
|
||||
pluginConfig: pluginConfig,
|
||||
keySpec: ks,
|
||||
},
|
||||
}
|
||||
|
||||
opts.SigningAgent = fmt.Sprintf("%s %s/%s", signingAgent, metadata.Name, metadata.Version)
|
||||
return genericSigner.Sign(ctx, desc, opts)
|
||||
}
|
||||
|
||||
func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocispec.Descriptor, opts notation.SignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debug("Generating signature envelope by plugin")
|
||||
payload := envelope.Payload{TargetArtifact: envelope.SanitizeTargetArtifact(desc)}
|
||||
|
@ -113,8 +182,10 @@ func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
|
|||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("envelope payload can't be marshalled: %w", err)
|
||||
}
|
||||
|
||||
// Execute plugin sign command.
|
||||
req := &proto.GenerateEnvelopeRequest{
|
||||
req := &plugin.GenerateEnvelopeRequest{
|
||||
ContractVersion: plugin.ContractVersion,
|
||||
KeyID: s.keyID,
|
||||
Payload: payloadBytes,
|
||||
SignatureEnvelopeType: opts.SignatureMediaType,
|
||||
|
@ -134,13 +205,11 @@ func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
|
|||
resp.SignatureEnvelopeType, req.SignatureEnvelopeType,
|
||||
)
|
||||
}
|
||||
|
||||
logger.Debug("Verifying signature envelope generated by the plugin")
|
||||
sigEnv, err := signature.ParseEnvelope(opts.SignatureMediaType, resp.SignatureEnvelope)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
envContent, err := sigEnv.Verify()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("generated signature failed verification: %w", err)
|
||||
|
@ -148,31 +217,29 @@ func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
|
|||
if err := envelope.ValidatePayloadContentType(&envContent.Payload); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
content := envContent.Payload.Content
|
||||
var signedPayload envelope.Payload
|
||||
if err = json.Unmarshal(content, &signedPayload); err != nil {
|
||||
return nil, nil, fmt.Errorf("signed envelope payload can't be unmarshalled: %w", err)
|
||||
}
|
||||
|
||||
if !isPayloadDescriptorValid(desc, signedPayload.TargetArtifact) {
|
||||
return nil, nil, fmt.Errorf("during signing descriptor subject has changed from %+v to %+v", desc, signedPayload.TargetArtifact)
|
||||
}
|
||||
|
||||
if unknownAttributes := areUnknownAttributesAdded(content); len(unknownAttributes) != 0 {
|
||||
return nil, nil, fmt.Errorf("during signing, following unknown attributes were added to subject descriptor: %+q", unknownAttributes)
|
||||
}
|
||||
|
||||
s.manifestAnnotations = resp.Annotations
|
||||
return resp.SignatureEnvelope, &envContent.SignerInfo, nil
|
||||
}
|
||||
|
||||
func (s *pluginSigner) mergeConfig(config map[string]string) map[string]string {
|
||||
func (s *PluginSigner) mergeConfig(config map[string]string) map[string]string {
|
||||
c := make(map[string]string, len(s.pluginConfig)+len(config))
|
||||
|
||||
// First clone s.PluginConfig.
|
||||
for k, v := range s.pluginConfig {
|
||||
c[k] = v
|
||||
}
|
||||
|
||||
// Then set or override entries from config.
|
||||
for k, v := range config {
|
||||
c[k] = v
|
||||
|
@ -180,16 +247,16 @@ func (s *pluginSigner) mergeConfig(config map[string]string) map[string]string {
|
|||
return c
|
||||
}
|
||||
|
||||
func (s *pluginSigner) describeKey(ctx context.Context, config map[string]string) (*proto.DescribeKeyResponse, error) {
|
||||
req := &proto.DescribeKeyRequest{
|
||||
KeyID: s.keyID,
|
||||
PluginConfig: config,
|
||||
func (s *PluginSigner) describeKey(ctx context.Context, config map[string]string) (*plugin.DescribeKeyResponse, error) {
|
||||
req := &plugin.DescribeKeyRequest{
|
||||
ContractVersion: plugin.ContractVersion,
|
||||
KeyID: s.keyID,
|
||||
PluginConfig: config,
|
||||
}
|
||||
resp, err := s.plugin.DescribeKey(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("describe-key command failed: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
@ -199,6 +266,7 @@ func isDescriptorSubset(original, newDesc ocispec.Descriptor) bool {
|
|||
if !content.Equal(original, newDesc) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Plugins may append additional annotations but not replace/override
|
||||
// existing.
|
||||
for k, v := range original.Annotations {
|
||||
|
@ -216,6 +284,7 @@ func isPayloadDescriptorValid(originalDesc, newDesc ocispec.Descriptor) bool {
|
|||
|
||||
func areUnknownAttributesAdded(content []byte) []string {
|
||||
var targetArtifactMap map[string]interface{}
|
||||
|
||||
// Ignoring error because we already successfully unmarshalled before this
|
||||
// point
|
||||
_ = json.Unmarshal(content, &targetArtifactMap)
|
||||
|
@ -272,23 +341,21 @@ func (s *pluginPrimitiveSigner) Sign(payload []byte) ([]byte, []*x509.Certificat
|
|||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
keySpecHash, err := proto.HashAlgorithmFromKeySpec(s.keySpec)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req := &proto.GenerateSignatureRequest{
|
||||
KeyID: s.keyID,
|
||||
KeySpec: keySpec,
|
||||
Hash: keySpecHash,
|
||||
Payload: payload,
|
||||
PluginConfig: s.pluginConfig,
|
||||
req := &plugin.GenerateSignatureRequest{
|
||||
ContractVersion: plugin.ContractVersion,
|
||||
KeyID: s.keyID,
|
||||
KeySpec: keySpec,
|
||||
Hash: keySpecHash,
|
||||
Payload: payload,
|
||||
PluginConfig: s.pluginConfig,
|
||||
}
|
||||
|
||||
resp, err := s.plugin.GenerateSignature(s.ctx, req)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("generate-signature command failed: %w", err)
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Check keyID is honored.
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 signer
|
||||
|
||||
import (
|
||||
|
@ -19,6 +32,7 @@ import (
|
|||
"github.com/notaryproject/notation-go/internal/envelope"
|
||||
"github.com/notaryproject/notation-go/plugin"
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
|
@ -57,6 +71,16 @@ type mockPlugin struct {
|
|||
keySpec signature.KeySpec
|
||||
}
|
||||
|
||||
func getDescriptorFunc(throwError bool) func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) {
|
||||
return func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) {
|
||||
if throwError {
|
||||
return ocispec.Descriptor{}, errors.New("")
|
||||
}
|
||||
return validSignDescriptor, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func newMockPlugin(key crypto.PrivateKey, certs []*x509.Certificate, keySpec signature.KeySpec) *mockPlugin {
|
||||
return &mockPlugin{
|
||||
key: key,
|
||||
|
@ -102,7 +126,7 @@ func (p *mockPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateS
|
|||
return &proto.GenerateSignatureResponse{
|
||||
KeyID: req.KeyID,
|
||||
Signature: invalidSignatureEnvelope,
|
||||
SigningAlgorithm: string(sigAlg),
|
||||
SigningAlgorithm: sigAlg,
|
||||
CertificateChain: certChain,
|
||||
}, err
|
||||
}
|
||||
|
@ -123,7 +147,7 @@ func (p *mockPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateS
|
|||
|
||||
// GenerateEnvelope generates the Envelope with signature based on the request.
|
||||
func (p *mockPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error) {
|
||||
internalPluginSigner := pluginSigner{
|
||||
internalPluginSigner := PluginSigner{
|
||||
plugin: newMockPlugin(p.key, p.certs, p.keySpec),
|
||||
}
|
||||
|
||||
|
@ -192,19 +216,49 @@ func (p *mockPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEn
|
|||
return &proto.GenerateEnvelopeResponse{}, nil
|
||||
}
|
||||
|
||||
func TestPluginSignerImpl(t *testing.T) {
|
||||
p := &PluginSigner{}
|
||||
if _, ok := interface{}(p).(notation.Signer); !ok {
|
||||
t.Fatal("PluginSigner does not implement notation.Signer")
|
||||
}
|
||||
|
||||
if _, ok := interface{}(p).(notation.BlobSigner); !ok {
|
||||
t.Fatal("PluginSigner does not implement notation.BlobSigner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromPluginFailed(t *testing.T) {
|
||||
wantErr := "keyID not specified"
|
||||
_, err := NewFromPlugin(&plugin.CLIPlugin{}, "", make(map[string]string))
|
||||
if err == nil || err.Error() != wantErr {
|
||||
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", wantErr, err.Error())
|
||||
tests := map[string]struct {
|
||||
pl plugin.SignPlugin
|
||||
keyID string
|
||||
errMsg string
|
||||
}{
|
||||
"Invalid KeyID": {
|
||||
pl: &plugin.CLIPlugin{},
|
||||
keyID: "",
|
||||
errMsg: "keyID not specified",
|
||||
},
|
||||
"nilPlugin": {
|
||||
pl: nil,
|
||||
keyID: "someKeyId",
|
||||
errMsg: "nil plugin",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := NewFromPlugin(tc.pl, tc.keyID, make(map[string]string))
|
||||
if err == nil || err.Error() != tc.errMsg {
|
||||
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", tc.errMsg, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigner_Sign_EnvelopeNotSupported(t *testing.T) {
|
||||
signer := pluginSigner{
|
||||
signer := PluginSigner{
|
||||
plugin: newMockPlugin(nil, nil, signature.KeySpec{Type: signature.KeyTypeRSA, Size: 2048}),
|
||||
}
|
||||
opts := notation.SignOptions{SignatureMediaType: "unsupported"}
|
||||
opts := notation.SignerSignOptions{SignatureMediaType: "unsupported"}
|
||||
testSignerError(t, signer, fmt.Sprintf("signature envelope format with media type %q is not supported", opts.SignatureMediaType), opts)
|
||||
}
|
||||
|
||||
|
@ -212,11 +266,11 @@ func TestSigner_Sign_DescribeKeyIDMismatch(t *testing.T) {
|
|||
respKeyId := ""
|
||||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
|
||||
signer := pluginSigner{
|
||||
signer := PluginSigner{
|
||||
plugin: newMockPlugin(nil, nil, signature.KeySpec{}),
|
||||
keyID: "1",
|
||||
}
|
||||
testSignerError(t, signer, fmt.Sprintf("keyID in describeKey response %q does not match request %q", respKeyId, signer.keyID), notation.SignOptions{SignatureMediaType: envelopeType})
|
||||
testSignerError(t, signer, fmt.Sprintf("keyID in describeKey response %q does not match request %q", respKeyId, signer.keyID), notation.SignerSignOptions{SignatureMediaType: envelopeType})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -225,10 +279,10 @@ func TestSigner_Sign_ExpiryInValid(t *testing.T) {
|
|||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
|
||||
ks, _ := signature.ExtractKeySpec(keyCertPairCollections[0].certs[0])
|
||||
signer := pluginSigner{
|
||||
signer := PluginSigner{
|
||||
plugin: newMockPlugin(keyCertPairCollections[0].key, keyCertPairCollections[0].certs, ks),
|
||||
}
|
||||
_, _, err := signer.Sign(context.Background(), ocispec.Descriptor{}, notation.SignOptions{ExpiryDuration: -24 * time.Hour, SignatureMediaType: envelopeType})
|
||||
_, _, err := signer.Sign(context.Background(), ocispec.Descriptor{}, notation.SignerSignOptions{ExpiryDuration: -24 * time.Hour, SignatureMediaType: envelopeType})
|
||||
wantEr := "expiry cannot be equal or before the signing time"
|
||||
if err == nil || !strings.Contains(err.Error(), wantEr) {
|
||||
t.Errorf("Signer.Sign() error = %v, wantErr %v", err, wantEr)
|
||||
|
@ -242,10 +296,10 @@ func TestSigner_Sign_InvalidCertChain(t *testing.T) {
|
|||
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
|
||||
mockPlugin := newMockPlugin(defaultKeyCert.key, defaultKeyCert.certs, defaultKeySpec)
|
||||
mockPlugin.invalidCertChain = true
|
||||
signer := pluginSigner{
|
||||
signer := PluginSigner{
|
||||
plugin: mockPlugin,
|
||||
}
|
||||
testSignerError(t, signer, "x509: malformed certificate", notation.SignOptions{SignatureMediaType: envelopeType})
|
||||
testSignerError(t, signer, "x509: malformed certificate", notation.SignerSignOptions{SignatureMediaType: envelopeType})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -256,10 +310,10 @@ func TestSigner_Sign_InvalidDescriptor(t *testing.T) {
|
|||
mockPlugin := newMockPlugin(defaultKeyCert.key, defaultKeyCert.certs, defaultKeySpec)
|
||||
mockPlugin.wantEnvelope = true
|
||||
mockPlugin.invalidDescriptor = true
|
||||
signer := pluginSigner{
|
||||
signer := PluginSigner{
|
||||
plugin: mockPlugin,
|
||||
}
|
||||
testSignerError(t, signer, "during signing, following unknown attributes were added to subject descriptor: [\"additional_field\"]", notation.SignOptions{SignatureMediaType: envelopeType})
|
||||
testSignerError(t, signer, "during signing, following unknown attributes were added to subject descriptor: [\"additional_field\"]", notation.SignerSignOptions{SignatureMediaType: envelopeType})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -269,10 +323,10 @@ func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) {
|
|||
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
|
||||
mockPlugin := newMockPlugin(defaultKeyCert.key, defaultKeyCert.certs, defaultKeySpec)
|
||||
mockPlugin.invalidSig = true
|
||||
signer := pluginSigner{
|
||||
signer := PluginSigner{
|
||||
plugin: mockPlugin,
|
||||
}
|
||||
testSignerError(t, signer, "signature is invalid", notation.SignOptions{SignatureMediaType: envelopeType})
|
||||
testSignerError(t, signer, "signature is invalid", notation.SignerSignOptions{SignatureMediaType: envelopeType})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -282,15 +336,48 @@ func TestPluginSigner_Sign_Valid(t *testing.T) {
|
|||
for _, keyCert := range keyCertPairCollections {
|
||||
t.Run(fmt.Sprintf("external plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
|
||||
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
|
||||
pluginSigner := pluginSigner{
|
||||
pluginSigner := PluginSigner{
|
||||
plugin: newMockPlugin(keyCert.key, keyCert.certs, keySpec),
|
||||
}
|
||||
basicSignTest(t, &pluginSigner, envelopeType, &validMetadata)
|
||||
validSignOpts.SignatureMediaType = envelopeType
|
||||
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginSigner_SignBlob_Valid(t *testing.T) {
|
||||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
for _, keyCert := range keyCertPairCollections {
|
||||
t.Run(fmt.Sprintf("external plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
|
||||
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
|
||||
pluginSigner := PluginSigner{
|
||||
plugin: newMockPlugin(keyCert.key, keyCert.certs, keySpec),
|
||||
}
|
||||
validSignOpts.SignatureMediaType = envelopeType
|
||||
data, signerInfo, err := pluginSigner.SignBlob(context.Background(), getDescriptorFunc(false), validSignOpts)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginSigner_SignBlob_Invalid(t *testing.T) {
|
||||
t.Run("blob signing with generate envelope plugin should fail", func(t *testing.T) {
|
||||
plugin := &mockPlugin{}
|
||||
plugin.wantEnvelope = true
|
||||
pluginSigner := PluginSigner{
|
||||
plugin: plugin,
|
||||
}
|
||||
_, _, err := pluginSigner.SignBlob(context.Background(), getDescriptorFunc(false), validSignOpts)
|
||||
expectedErrMsg := "the plugin \"testPlugin\" lacks the signature generator capability required for blob signing"
|
||||
if err == nil || !strings.Contains(err.Error(), expectedErrMsg) {
|
||||
t.Fatalf("expected error %q, got %v", expectedErrMsg, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPluginSigner_SignEnvelope_RunFailed(t *testing.T) {
|
||||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
|
||||
|
@ -298,10 +385,10 @@ func TestPluginSigner_SignEnvelope_RunFailed(t *testing.T) {
|
|||
wantEnvelope: true,
|
||||
failEnvelope: true,
|
||||
}
|
||||
signer := pluginSigner{
|
||||
signer := PluginSigner{
|
||||
plugin: p,
|
||||
}
|
||||
testSignerError(t, signer, "failed GenerateEnvelope", notation.SignOptions{SignatureMediaType: envelopeType})
|
||||
testSignerError(t, signer, "failed GenerateEnvelope", notation.SignerSignOptions{SignatureMediaType: envelopeType})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -313,10 +400,12 @@ func TestPluginSigner_SignEnvelope_Valid(t *testing.T) {
|
|||
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
|
||||
mockPlugin := newMockPlugin(keyCert.key, keyCert.certs, keySpec)
|
||||
mockPlugin.wantEnvelope = true
|
||||
pluginSigner := pluginSigner{
|
||||
pluginSigner := PluginSigner{
|
||||
plugin: mockPlugin,
|
||||
}
|
||||
basicSignTest(t, &pluginSigner, envelopeType, &validMetadata)
|
||||
validSignOpts.SignatureMediaType = envelopeType
|
||||
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -328,7 +417,7 @@ func TestPluginSigner_SignWithAnnotations_Valid(t *testing.T) {
|
|||
t.Run(fmt.Sprintf("external plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
|
||||
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
|
||||
annts := map[string]string{"key": "value"}
|
||||
pluginSigner := pluginSigner{
|
||||
pluginSigner := PluginSigner{
|
||||
plugin: &mockPlugin{
|
||||
key: keyCert.key,
|
||||
certs: keyCert.certs,
|
||||
|
@ -337,7 +426,9 @@ func TestPluginSigner_SignWithAnnotations_Valid(t *testing.T) {
|
|||
wantEnvelope: true,
|
||||
},
|
||||
}
|
||||
basicSignTest(t, &pluginSigner, envelopeType, &validMetadata)
|
||||
validSignOpts.SignatureMediaType = envelopeType
|
||||
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
|
||||
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
|
||||
if !reflect.DeepEqual(pluginSigner.PluginAnnotations(), annts) {
|
||||
fmt.Println(pluginSigner.PluginAnnotations())
|
||||
t.Errorf("mismatch in annotations returned from PluginAnnotations()")
|
||||
|
@ -347,7 +438,7 @@ func TestPluginSigner_SignWithAnnotations_Valid(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func testSignerError(t *testing.T, signer pluginSigner, wantEr string, opts notation.SignOptions) {
|
||||
func testSignerError(t *testing.T, signer PluginSigner, wantEr string, opts notation.SignerSignOptions) {
|
||||
t.Helper()
|
||||
_, _, err := signer.Sign(context.Background(), ocispec.Descriptor{}, opts)
|
||||
if err == nil || !strings.Contains(err.Error(), wantEr) {
|
||||
|
@ -355,9 +446,7 @@ func testSignerError(t *testing.T, signer pluginSigner, wantEr string, opts nota
|
|||
}
|
||||
}
|
||||
|
||||
func basicSignTest(t *testing.T, pluginSigner *pluginSigner, envelopeType string, metadata *proto.GetMetadataResponse) {
|
||||
validSignOpts.SignatureMediaType = envelopeType
|
||||
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
|
||||
func basicSignTest(t *testing.T, ps *PluginSigner, envelopeType string, data []byte, signerInfo *signature.SignerInfo, err error) {
|
||||
if err != nil {
|
||||
t.Fatalf("Signer.Sign() error = %v, wantErr nil", err)
|
||||
}
|
||||
|
@ -386,12 +475,12 @@ func basicSignTest(t *testing.T, pluginSigner *pluginSigner, envelopeType string
|
|||
TargetArtifact: validSignDescriptor,
|
||||
}
|
||||
if !reflect.DeepEqual(expectedPayload, gotPayload) {
|
||||
t.Fatalf("Signer.Sign() descriptor subject changed, expect: %v, got: %v", expectedPayload, payload)
|
||||
t.Fatalf("Signer.Sign() descriptor subject changed, expect: %+v, got: %+v", expectedPayload, payload)
|
||||
}
|
||||
if signerInfo.SignedAttributes.SigningScheme != signature.SigningSchemeX509 {
|
||||
t.Fatalf("Signer.Sign() signing scheme changed, expect: %v, got: %v", signerInfo.SignedAttributes.SigningScheme, signature.SigningSchemeX509)
|
||||
t.Fatalf("Signer.Sign() signing scheme changed, expect: %+v, got: %+v", signerInfo.SignedAttributes.SigningScheme, signature.SigningSchemeX509)
|
||||
}
|
||||
mockPlugin := pluginSigner.plugin.(*mockPlugin)
|
||||
mockPlugin := ps.plugin.(*mockPlugin)
|
||||
if mockPlugin.keySpec.SignatureAlgorithm() != signerInfo.SignatureAlgorithm {
|
||||
t.Fatalf("Signer.Sign() signing algorithm changed")
|
||||
}
|
||||
|
@ -401,5 +490,5 @@ func basicSignTest(t *testing.T, pluginSigner *pluginSigner, envelopeType string
|
|||
if !reflect.DeepEqual(mockPlugin.certs, signerInfo.CertificateChain) {
|
||||
t.Fatalf(" Signer.Sign() cert chain changed")
|
||||
}
|
||||
basicVerification(t, data, envelopeType, mockPlugin.certs[len(mockPlugin.certs)-1], metadata)
|
||||
basicVerification(t, data, envelopeType, mockPlugin.certs[len(mockPlugin.certs)-1], &validMetadata)
|
||||
}
|
||||
|
|
110
signer/signer.go
110
signer/signer.go
|
@ -1,6 +1,19 @@
|
|||
// 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 signer provides notation signing functionality. It implements the
|
||||
// notation.Signer interface by providing builtinSigner for local signing and
|
||||
// pluginSigner for remote signing.
|
||||
// [notation.Signer] and [notation.BlobSigner] interfaces by providing
|
||||
// builtinSigner for local signing and [PluginSigner] for remote signing.
|
||||
package signer
|
||||
|
||||
import (
|
||||
|
@ -21,26 +34,42 @@ import (
|
|||
)
|
||||
|
||||
// signingAgent is the unprotected header field used by signature.
|
||||
const signingAgent = "Notation/1.0.0"
|
||||
const signingAgent = "notation-go/1.3.0+unreleased"
|
||||
|
||||
// genericSigner implements notation.Signer and embeds signature.Signer
|
||||
type genericSigner struct {
|
||||
signature.Signer
|
||||
// GenericSigner implements [notation.Signer] and [notation.BlobSigner].
|
||||
// It embeds signature.Signer.
|
||||
type GenericSigner struct {
|
||||
signer signature.Signer
|
||||
}
|
||||
|
||||
// New returns a builtinSigner given key and cert chain
|
||||
// New returns a [notation.Signer] given key and cert chain.
|
||||
//
|
||||
// Deprecated: New function exists for historical compatibility and
|
||||
// should not be used. To create [GenericSigner],
|
||||
// use NewGenericSigner() function.
|
||||
func New(key crypto.PrivateKey, certChain []*x509.Certificate) (notation.Signer, error) {
|
||||
return NewGenericSigner(key, certChain)
|
||||
}
|
||||
|
||||
// NewGenericSigner returns a builtinSigner given key and cert chain.
|
||||
func NewGenericSigner(key crypto.PrivateKey, certChain []*x509.Certificate) (*GenericSigner, error) {
|
||||
localSigner, err := signature.NewLocalSigner(certChain, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &genericSigner{
|
||||
Signer: localSigner,
|
||||
return &GenericSigner{
|
||||
signer: localSigner,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewFromFiles returns a builtinSigner given key and certChain paths.
|
||||
// NewFromFiles returns a [notation.Signer] given key and certChain paths.
|
||||
func NewFromFiles(keyPath, certChainPath string) (notation.Signer, error) {
|
||||
return NewGenericSignerFromFiles(keyPath, certChainPath)
|
||||
}
|
||||
|
||||
// NewGenericSignerFromFiles returns a builtinSigner given key and certChain
|
||||
// paths.
|
||||
func NewGenericSignerFromFiles(keyPath, certChainPath string) (*GenericSigner, error) {
|
||||
if keyPath == "" {
|
||||
return nil, errors.New("key path not specified")
|
||||
}
|
||||
|
@ -67,12 +96,12 @@ func NewFromFiles(keyPath, certChainPath string) (notation.Signer, error) {
|
|||
}
|
||||
|
||||
// create signer
|
||||
return New(cert.PrivateKey, certs)
|
||||
return NewGenericSigner(cert.PrivateKey, certs)
|
||||
}
|
||||
|
||||
// Sign signs the artifact described by its descriptor and returns the
|
||||
// marshalled envelope.
|
||||
func (s *genericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
// signature and SignerInfo.
|
||||
func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debugf("Generic signing for %v in signature media type %v", desc.Digest, opts.SignatureMediaType)
|
||||
// Generate payload to be signed.
|
||||
|
@ -81,22 +110,30 @@ func (s *genericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
|
|||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("envelope payload can't be marshalled: %w", err)
|
||||
}
|
||||
|
||||
var signingAgentId string
|
||||
if opts.SigningAgent != "" {
|
||||
signingAgentId = opts.SigningAgent
|
||||
} else {
|
||||
signingAgentId = signingAgent
|
||||
}
|
||||
if opts.Timestamper != nil && opts.TSARootCAs == nil {
|
||||
return nil, nil, errors.New("timestamping: got Timestamper but nil TSARootCAs")
|
||||
}
|
||||
if opts.TSARootCAs != nil && opts.Timestamper == nil {
|
||||
return nil, nil, errors.New("timestamping: got TSARootCAs but nil Timestamper")
|
||||
}
|
||||
signReq := &signature.SignRequest{
|
||||
Payload: signature.Payload{
|
||||
ContentType: envelope.MediaTypePayloadV1,
|
||||
Content: payloadBytes,
|
||||
},
|
||||
Signer: s.Signer,
|
||||
SigningTime: time.Now(),
|
||||
SigningScheme: signature.SigningSchemeX509,
|
||||
SigningAgent: signingAgentId,
|
||||
Signer: s.signer,
|
||||
SigningTime: time.Now(),
|
||||
SigningScheme: signature.SigningSchemeX509,
|
||||
SigningAgent: signingAgentId,
|
||||
Timestamper: opts.Timestamper,
|
||||
TSARootCAs: opts.TSARootCAs,
|
||||
TSARevocationValidator: opts.TSARevocationValidator,
|
||||
}
|
||||
|
||||
// Add expiry only if ExpiryDuration is not zero
|
||||
|
@ -110,18 +147,25 @@ func (s *genericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
|
|||
logger.Debugf(" Expiry: %v", signReq.Expiry)
|
||||
logger.Debugf(" SigningScheme: %v", signReq.SigningScheme)
|
||||
logger.Debugf(" SigningAgent: %v", signReq.SigningAgent)
|
||||
if signReq.Timestamper != nil {
|
||||
logger.Debug("Enabled timestamping")
|
||||
if signReq.TSARevocationValidator != nil {
|
||||
logger.Debug("Enabled timestamping certificate chain revocation check")
|
||||
}
|
||||
}
|
||||
|
||||
// Add ctx to the SignRequest
|
||||
signReq = signReq.WithContext(ctx)
|
||||
|
||||
// perform signing
|
||||
sigEnv, err := signature.NewEnvelope(opts.SignatureMediaType)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
sig, err := sigEnv.Sign(signReq)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
envContent, err := sigEnv.Verify()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("generated signature failed verification: %v", err)
|
||||
|
@ -129,7 +173,29 @@ func (s *genericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
|
|||
if err := envelope.ValidatePayloadContentType(&envContent.Payload); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO: re-enable timestamping https://github.com/notaryproject/notation-go/issues/78
|
||||
return sig, &envContent.SignerInfo, nil
|
||||
}
|
||||
|
||||
// SignBlob signs the descriptor returned by genDesc, and returns the
|
||||
// signature and SignerInfo.
|
||||
func (s *GenericSigner) SignBlob(ctx context.Context, genDesc notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debugf("Generic blob signing for signature media type %s", opts.SignatureMediaType)
|
||||
ks, err := s.signer.KeySpec()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
desc, err := getDescriptor(ks, genDesc)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return s.Sign(ctx, desc, opts)
|
||||
}
|
||||
|
||||
func getDescriptor(ks signature.KeySpec, genDesc notation.BlobDescriptorGenerator) (ocispec.Descriptor, error) {
|
||||
digestAlg, ok := algorithms[ks.SignatureAlgorithm().Hash()]
|
||||
if !ok {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("unknown hashing algo %v", ks.SignatureAlgorithm().Hash())
|
||||
}
|
||||
return genDesc(digestAlg)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
// 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 signer
|
||||
|
||||
import (
|
||||
|
@ -17,17 +30,23 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/revocation"
|
||||
"github.com/notaryproject/notation-core-go/revocation/purpose"
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
_ "github.com/notaryproject/notation-core-go/signature/cose"
|
||||
_ "github.com/notaryproject/notation-core-go/signature/jws"
|
||||
"github.com/notaryproject/notation-core-go/testhelper"
|
||||
nx509 "github.com/notaryproject/notation-core-go/x509"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/internal/envelope"
|
||||
"github.com/notaryproject/notation-go/plugin/proto"
|
||||
"github.com/notaryproject/tspclient-go"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
const rfc3161URL = "http://timestamp.digicert.com"
|
||||
|
||||
type keyCertPair struct {
|
||||
keySpecName string
|
||||
key crypto.PrivateKey
|
||||
|
@ -36,7 +55,8 @@ type keyCertPair struct {
|
|||
|
||||
var keyCertPairCollections []*keyCertPair
|
||||
|
||||
// setUpKeyCertPairCollections setups all combinations of private key and certificates.
|
||||
// setUpKeyCertPairCollections setups all combinations of private key and
|
||||
// certificates.
|
||||
func setUpKeyCertPairCollections() []*keyCertPair {
|
||||
// rsa
|
||||
var keyCertPairs []*keyCertPair
|
||||
|
@ -104,7 +124,7 @@ func generateKeyBytes(key crypto.PrivateKey) (keyBytes []byte, err error) {
|
|||
return keyBytes, nil
|
||||
}
|
||||
|
||||
func prepareTestKeyCertFile(keyCert *keyCertPair, envelopeType, dir string) (string, string, error) {
|
||||
func prepareTestKeyCertFile(keyCert *keyCertPair, dir string) (string, string, error) {
|
||||
keyPath, certPath := filepath.Join(dir, keyCert.keySpecName+".key"), filepath.Join(dir, keyCert.keySpecName+".cert")
|
||||
keyBytes, err := generateKeyBytes(keyCert.key)
|
||||
if err != nil {
|
||||
|
@ -125,7 +145,7 @@ func prepareTestKeyCertFile(keyCert *keyCertPair, envelopeType, dir string) (str
|
|||
}
|
||||
|
||||
func testSignerFromFile(t *testing.T, keyCert *keyCertPair, envelopeType, dir string) {
|
||||
keyPath, certPath, err := prepareTestKeyCertFile(keyCert, envelopeType, dir)
|
||||
keyPath, certPath, err := prepareTestKeyCertFile(keyCert, dir)
|
||||
if err != nil {
|
||||
t.Fatalf("prepareTestKeyCertFile() failed: %v", err)
|
||||
}
|
||||
|
@ -143,6 +163,17 @@ func testSignerFromFile(t *testing.T, keyCert *keyCertPair, envelopeType, dir st
|
|||
basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1], nil)
|
||||
}
|
||||
|
||||
func TestGenericSignerImpl(t *testing.T) {
|
||||
g := &GenericSigner{}
|
||||
if _, ok := interface{}(g).(notation.Signer); !ok {
|
||||
t.Fatal("GenericSigner does not implement notation.Signer")
|
||||
}
|
||||
|
||||
if _, ok := interface{}(g).(notation.BlobSigner); !ok {
|
||||
t.Fatal("GenericSigner does not implement notation.BlobSigner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewFromFiles(t *testing.T) {
|
||||
// sign with key
|
||||
dir := t.TempDir()
|
||||
|
@ -155,12 +186,134 @@ func TestNewFromFiles(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNewFromFilesError(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
keyPath string
|
||||
certPath string
|
||||
errMsg string
|
||||
}{
|
||||
"empty key path": {
|
||||
keyPath: "",
|
||||
certPath: "someCert",
|
||||
errMsg: "key path not specified",
|
||||
},
|
||||
"empty cert path": {
|
||||
keyPath: "someKeyId",
|
||||
certPath: "",
|
||||
errMsg: "certificate path not specified",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := NewFromFiles(tc.keyPath, tc.certPath)
|
||||
if err == nil || err.Error() != tc.errMsg {
|
||||
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", tc.errMsg, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewError(t *testing.T) {
|
||||
wantErr := "\"certs\" param is invalid. Error: empty certs"
|
||||
_, err := New(nil, nil)
|
||||
if err == nil || err.Error() != wantErr {
|
||||
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", wantErr, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithCertChain(t *testing.T) {
|
||||
// sign with key
|
||||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
for _, keyCert := range keyCertPairCollections {
|
||||
t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
|
||||
validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs)
|
||||
validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs, false)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithTimestamping(t *testing.T) {
|
||||
// sign with key
|
||||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
for _, keyCert := range keyCertPairCollections {
|
||||
t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
|
||||
validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs, true)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// timestamping without timestamper
|
||||
envelopeType := signature.RegisteredEnvelopeTypes()[0]
|
||||
keyCert := keyCertPairCollections[0]
|
||||
s, err := New(keyCert.key, keyCert.certs)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner() error = %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
desc, sOpts := generateSigningContent()
|
||||
sOpts.SignatureMediaType = envelopeType
|
||||
sOpts.TSARootCAs = x509.NewCertPool()
|
||||
_, _, err = s.Sign(ctx, desc, sOpts)
|
||||
expectedErrMsg := "timestamping: got TSARootCAs but nil Timestamper"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
|
||||
// timestamping without TSARootCAs
|
||||
desc, sOpts = generateSigningContent()
|
||||
sOpts.SignatureMediaType = envelopeType
|
||||
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, err = s.Sign(ctx, desc, sOpts)
|
||||
expectedErrMsg = "timestamping: got Timestamper but nil TSARootCAs"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
|
||||
// timestamping with unknown authority
|
||||
desc, sOpts = generateSigningContent()
|
||||
sOpts.SignatureMediaType = envelopeType
|
||||
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sOpts.TSARootCAs = x509.NewCertPool()
|
||||
tsaRevocationValidator, err := revocation.NewWithOptions(revocation.Options{
|
||||
CertChainPurpose: purpose.Timestamping,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sOpts.TSARevocationValidator = tsaRevocationValidator
|
||||
_, _, err = s.Sign(ctx, desc, sOpts)
|
||||
expectedErrMsg = "timestamp: failed to verify signed token: cms verification failure: x509: certificate signed by unknown authority"
|
||||
if err == nil || err.Error() != expectedErrMsg {
|
||||
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignBlobWithCertChain(t *testing.T) {
|
||||
// sign with key
|
||||
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
|
||||
for _, keyCert := range keyCertPairCollections {
|
||||
t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
|
||||
s, err := NewGenericSigner(keyCert.key, keyCert.certs)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner() error = %v", err)
|
||||
}
|
||||
|
||||
sOpts := notation.SignerSignOptions{
|
||||
SignatureMediaType: envelopeType,
|
||||
}
|
||||
sig, _, err := s.SignBlob(context.Background(), getDescriptorFunc(false), sOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign() error = %v", err)
|
||||
}
|
||||
|
||||
// basic verification
|
||||
basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1], nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -196,7 +349,7 @@ func signRSA(digest []byte, hash crypto.Hash, pk *rsa.PrivateKey) ([]byte, error
|
|||
return rsa.SignPSS(rand.Reader, pk, hash, digest, &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash})
|
||||
}
|
||||
|
||||
func signECDSA(digest []byte, hash crypto.Hash, pk *ecdsa.PrivateKey) ([]byte, error) {
|
||||
func signECDSA(digest []byte, pk *ecdsa.PrivateKey) ([]byte, error) {
|
||||
r, s, err := ecdsa.Sign(rand.Reader, pk, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -216,14 +369,14 @@ func localSign(payload []byte, hash crypto.Hash, pk crypto.PrivateKey) ([]byte,
|
|||
case *rsa.PrivateKey:
|
||||
return signRSA(digest, hash, key)
|
||||
case *ecdsa.PrivateKey:
|
||||
return signECDSA(digest, hash, key)
|
||||
return signECDSA(digest, key)
|
||||
default:
|
||||
return nil, errors.New("signing private key not supported")
|
||||
}
|
||||
}
|
||||
|
||||
// generateSigningContent generates common signing content with options for testing.
|
||||
func generateSigningContent() (ocispec.Descriptor, notation.SignOptions) {
|
||||
func generateSigningContent() (ocispec.Descriptor, notation.SignerSignOptions) {
|
||||
content := "hello world"
|
||||
desc := ocispec.Descriptor{
|
||||
MediaType: "test media type",
|
||||
|
@ -234,7 +387,7 @@ func generateSigningContent() (ocispec.Descriptor, notation.SignOptions) {
|
|||
"foo": "bar",
|
||||
},
|
||||
}
|
||||
sOpts := notation.SignOptions{ExpiryDuration: 24 * time.Hour}
|
||||
sOpts := notation.SignerSignOptions{ExpiryDuration: 24 * time.Hour}
|
||||
|
||||
return desc, sOpts
|
||||
}
|
||||
|
@ -281,7 +434,7 @@ func verifySigningAgent(t *testing.T, signingAgentId string, metadata *proto.Get
|
|||
}
|
||||
}
|
||||
|
||||
func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.PrivateKey, certs []*x509.Certificate) {
|
||||
func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.PrivateKey, certs []*x509.Certificate, timestamp bool) {
|
||||
s, err := New(key, certs)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner() error = %v", err)
|
||||
|
@ -290,6 +443,19 @@ func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.Private
|
|||
ctx := context.Background()
|
||||
desc, sOpts := generateSigningContent()
|
||||
sOpts.SignatureMediaType = envelopeType
|
||||
if timestamp {
|
||||
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCerts, err := nx509.ReadCertificateFile("./testdata/DigiCertTSARootSHA384.cer")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rootCAs := x509.NewCertPool()
|
||||
rootCAs.AddCert(rootCerts[0])
|
||||
sOpts.TSARootCAs = rootCAs
|
||||
}
|
||||
sig, _, err := s.Sign(ctx, desc, sOpts)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign() error = %v", err)
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,171 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package crl provides functionalities for crl revocation check.
|
||||
package crl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
corecrl "github.com/notaryproject/notation-core-go/revocation/crl"
|
||||
"github.com/notaryproject/notation-go/internal/file"
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
)
|
||||
|
||||
// FileCache implements corecrl.Cache.
|
||||
//
|
||||
// Key: url of the CRL.
|
||||
//
|
||||
// Value: corecrl.Bundle.
|
||||
//
|
||||
// This cache builds on top of the UNIX file system to leverage the file system's
|
||||
// atomic operations. The `rename` and `remove` operations will unlink the old
|
||||
// file but keep the inode and file descriptor for existing processes to access
|
||||
// the file. The old inode will be dereferenced when all processes close the old
|
||||
// file descriptor. Additionally, the operations are proven to be atomic on
|
||||
// UNIX-like platforms, so there is no need to handle file locking.
|
||||
//
|
||||
// NOTE: For Windows, the `open`, `rename` and `remove` operations need file
|
||||
// locking to ensure atomicity. The current implementation does not handle
|
||||
// file locking, so the concurrent write from multiple processes may be failed.
|
||||
// Please do not use this cache in a multi-process environment on Windows.
|
||||
type FileCache struct {
|
||||
// root is the root directory of the cache
|
||||
root string
|
||||
}
|
||||
|
||||
// fileCacheContent is the actual content saved in a FileCache
|
||||
type fileCacheContent struct {
|
||||
// BaseCRL is the ASN.1 encoded base CRL
|
||||
BaseCRL []byte `json:"baseCRL"`
|
||||
|
||||
// DeltaCRL is the ASN.1 encoded delta CRL
|
||||
DeltaCRL []byte `json:"deltaCRL,omitempty"`
|
||||
}
|
||||
|
||||
// NewFileCache creates a FileCache with root as the root directory
|
||||
//
|
||||
// An example for root is `dir.CacheFS().SysPath(dir.PathCRLCache)`
|
||||
func NewFileCache(root string) (*FileCache, error) {
|
||||
if err := os.MkdirAll(root, 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create crl file cache: %w", err)
|
||||
}
|
||||
return &FileCache{
|
||||
root: root,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get retrieves CRL bundle from c given url as key. If the key does not exist
|
||||
// or the content has expired, corecrl.ErrCacheMiss is returned.
|
||||
func (c *FileCache) Get(ctx context.Context, url string) (*corecrl.Bundle, error) {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debugf("Retrieving crl bundle from file cache with key %q ...", url)
|
||||
|
||||
// get content from file cache
|
||||
contentBytes, err := os.ReadFile(filepath.Join(c.root, c.fileName(url)))
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
logger.Debugf("CRL file cache miss. Key %q does not exist", url)
|
||||
return nil, corecrl.ErrCacheMiss
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get crl bundle from file cache with key %q: %w", url, err)
|
||||
}
|
||||
|
||||
// decode content to crl Bundle
|
||||
var content fileCacheContent
|
||||
if err := json.Unmarshal(contentBytes, &content); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode file retrieved from file cache: %w", err)
|
||||
}
|
||||
var bundle corecrl.Bundle
|
||||
bundle.BaseCRL, err = x509.ParseRevocationList(content.BaseCRL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse base CRL of file retrieved from file cache: %w", err)
|
||||
}
|
||||
if content.DeltaCRL != nil {
|
||||
bundle.DeltaCRL, err = x509.ParseRevocationList(content.DeltaCRL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse delta CRL of file retrieved from file cache: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// check expiry
|
||||
if err := checkExpiry(ctx, bundle.BaseCRL.NextUpdate); err != nil {
|
||||
return nil, fmt.Errorf("check BaseCRL expiry failed: %w", err)
|
||||
}
|
||||
if bundle.DeltaCRL != nil {
|
||||
if err := checkExpiry(ctx, bundle.DeltaCRL.NextUpdate); err != nil {
|
||||
return nil, fmt.Errorf("check DeltaCRL expiry failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return &bundle, nil
|
||||
}
|
||||
|
||||
// Set stores the CRL bundle in c with url as key.
|
||||
func (c *FileCache) Set(ctx context.Context, url string, bundle *corecrl.Bundle) error {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debugf("Storing crl bundle to file cache with key %q ...", url)
|
||||
|
||||
if bundle == nil {
|
||||
return errors.New("failed to store crl bundle in file cache: bundle cannot be nil")
|
||||
}
|
||||
if bundle.BaseCRL == nil {
|
||||
return errors.New("failed to store crl bundle in file cache: bundle BaseCRL cannot be nil")
|
||||
}
|
||||
|
||||
// actual content to be saved in the cache
|
||||
content := fileCacheContent{
|
||||
BaseCRL: bundle.BaseCRL.Raw,
|
||||
}
|
||||
if bundle.DeltaCRL != nil {
|
||||
content.DeltaCRL = bundle.DeltaCRL.Raw
|
||||
}
|
||||
contentBytes, err := json.Marshal(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to store crl bundle in file cache: %w", err)
|
||||
}
|
||||
if err := file.WriteFile(c.root, filepath.Join(c.root, c.fileName(url)), contentBytes); err != nil {
|
||||
return fmt.Errorf("failed to store crl bundle in file cache: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fileName returns the filename of the content stored in c
|
||||
func (c *FileCache) fileName(url string) string {
|
||||
hash := sha256.Sum256([]byte(url))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// checkExpiry returns nil when nextUpdate is bounded before current time
|
||||
func checkExpiry(ctx context.Context, nextUpdate time.Time) error {
|
||||
logger := log.GetLogger(ctx)
|
||||
|
||||
if nextUpdate.IsZero() {
|
||||
return errors.New("crl bundle retrieved from file cache does not contain valid NextUpdate")
|
||||
}
|
||||
if time.Now().After(nextUpdate) {
|
||||
logger.Debugf("CRL bundle retrieved from file cache has expired at %s", nextUpdate)
|
||||
return corecrl.ErrCacheMiss
|
||||
}
|
||||
return nil
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue