Compare commits
275 Commits
v0.7.0-alp
...
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 | |
|
de8b7ddaa5 | |
|
064887e1ee | |
|
156ceddb01 | |
|
7987c85e46 | |
|
1b2ec27aea | |
|
6ef3544efa | |
|
5e5cba1e9a | |
|
920ded2440 | |
|
d44cb5d18d | |
|
510def1a3f | |
|
32d23721c0 | |
|
ed6ff0c142 | |
|
e290acfef6 | |
|
e8f14a5503 | |
|
36d97f79ab | |
|
e11bfd8f2c | |
|
c90d5213ae | |
|
6c88d3de12 | |
|
7b22cec0e5 | |
|
bce61623c0 | |
|
6514ba4431 | |
|
180ad994fe | |
|
1a9436d7a1 | |
|
e8a1ad03ee | |
|
0355e8eb49 | |
|
e9545a7183 | |
|
bc022cc61d | |
|
35d90607a6 | |
|
7ae1f5fd07 | |
|
2da0327d37 | |
|
ab113ebd2a | |
|
7e391f7562 | |
|
38198df806 | |
|
6548adc2e8 | |
|
27251a828d | |
|
2573c88a5f | |
|
ed31122368 | |
|
9d6b902999 | |
|
e43b2925ba | |
|
fc5044e670 | |
|
d7c82a39dc | |
|
f9c0b87a90 | |
|
61ffb09828 | |
|
79b49af853 | |
|
c3e6e07814 | |
|
49ba3a341f | |
|
a44d663e7b | |
|
faeac45740 | |
|
75b3248459 | |
|
55b8746fbc | |
|
82ec47d568 | |
|
bccecbc830 | |
|
7ad4eca1a5 | |
|
f35c52efb5 | |
|
e0892fc600 | |
|
3d8de5f05f | |
|
e2ae1fecf0 | |
|
2bcfd343f9 | |
|
f89d6cfa96 | |
|
84d4de1c21 | |
|
ea9ee15677 | |
|
76d4ca4e5e | |
|
a9fb055b8d | |
|
3f1301e307 | |
|
78bc70a154 | |
|
c350ef73e5 | |
|
f3bae8315a | |
|
55fd020cc2 | |
|
234866cbd7 | |
|
e360d6886f | |
|
e698b39314 | |
|
6312370a35 | |
|
ba141e111a | |
|
34be24db50 | |
|
aaca74d9f2 | |
|
cdf50d9ff2 | |
|
7af715044c | |
|
c78f4ad635 | |
|
5abc8e3074 | |
|
962d79cd40 | |
|
3d22fbc121 | |
|
4a649a91cc | |
|
ed2b227d00 | |
|
ff39f29419 | |
|
68e41ad3c9 | |
|
d65bdf6a09 | |
|
52fcb4aae8 | |
|
44bffb0b61 | |
|
45f149e1b5 | |
|
9f26faa2eb | |
|
54c8362669 | |
|
0757b20223 | |
|
a4fdff4190 | |
|
d75cdbdd5e | |
|
fe1997f6c6 | |
|
746ba33f99 | |
|
2c9a8b0e04 | |
|
d238345a8f | |
|
407537596e | |
|
d029c710cf | |
|
182873bded | |
|
9398a19a0f | |
|
b1d0c27305 | |
|
2f17ae391c | |
|
4ff60ca32c | |
|
96df588f2f | |
|
b5d900264a | |
|
1b9631b346 | |
|
80eeec8173 | |
|
575548db49 | |
|
f9b93c4030 |
|
@ -0,0 +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
|
|
@ -0,0 +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.
|
||||
|
||||
on:
|
||||
issues:
|
||||
types:
|
||||
- opened
|
||||
|
||||
jobs:
|
||||
add-to-project:
|
||||
name: Add issue to project
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/add-to-project@main
|
||||
with:
|
||||
project-url: https://github.com/orgs/notaryproject/projects/10
|
||||
github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
|
|
@ -0,0 +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
|
||||
- release-*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: notaryproject/notation-core-go/.github/workflows/reusable-build.yml@main
|
||||
secrets:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
@ -0,0 +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: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- release-*
|
||||
schedule:
|
||||
- cron: '29 2 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
uses: notaryproject/notation-core-go/.github/workflows/reusable-codeql.yml@main
|
|
@ -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,42 +0,0 @@
|
|||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: main
|
||||
pull_request:
|
||||
branches: main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Continuous Testing
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.17]
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Set up Go ${{ matrix.go-version }}
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
- name: Build and test
|
||||
run: make test
|
||||
line_endings:
|
||||
name: Check Line Endings
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: true
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
- name: Check line endings
|
||||
run: make check-line-endings
|
|
@ -1 +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.
|
||||
|
||||
# Code Editors
|
||||
.vscode
|
||||
.idea
|
||||
*.sublime-project
|
||||
*.sublime-workspace
|
||||
|
||||
# Custom
|
||||
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)
|
20
Makefile
20
Makefile
|
@ -1,13 +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.
|
||||
|
||||
.PHONY: help
|
||||
help:
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}'
|
||||
|
||||
.PHONY: all
|
||||
all: check-line-endings test
|
||||
all: test
|
||||
|
||||
.PHONY: test
|
||||
test: ## run unit tests
|
||||
go test ./...
|
||||
test: check-line-endings ## run unit tests
|
||||
go test -race -v -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
|
@ -16,6 +29,7 @@ clean:
|
|||
.PHONY: check-line-endings
|
||||
check-line-endings: ## check line endings
|
||||
! find . -name "*.go" -type f -exec file "{}" ";" | grep CRLF
|
||||
! find . -name "*.sh" -type f -exec file "{}" ";" | grep CRLF
|
||||
|
||||
.PHONY: fix-line-endings
|
||||
fix-line-endings: ## fix line endings
|
||||
|
|
25
README.md
25
README.md
|
@ -1,15 +1,28 @@
|
|||
# Notation
|
||||
A collection of libraries for supporting Notation sign, verify, push, pull of oci artifacts. Based on Notary V2 standard.
|
||||
# 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://pkg.go.dev/github.com/notaryproject/notation-go@main)
|
||||
[](https://scorecard.dev/viewer/?uri=github.com/notaryproject/notation-go)
|
||||
|
||||
notation-go contains libraries for signing and verification of artifacts as per [Notary Project specifications](https://github.com/notaryproject/specifications). notation-go is being used by [notation](https://github.com/notaryproject/notation) CLI for signing and verifying artifacts.
|
||||
|
||||
notation-go reached a stable release as of July 2023 and continues to be actively developed and maintained.
|
||||
|
||||
Please visit [README](https://github.com/notaryproject/.github/blob/main/README.md) to know more about Notary Project.
|
||||
|
||||
> [!NOTE]
|
||||
> The Notary Project documentation is available [here](https://notaryproject.dev/docs/).
|
||||
|
||||
## Table of Contents
|
||||
- [Core Documents](#core-documents)
|
||||
|
||||
- [Documentation](#documentation)
|
||||
- [Code of Conduct](#code-of-conduct)
|
||||
- [License](#license)
|
||||
|
||||
## Core Documents
|
||||
## Documentation
|
||||
|
||||
* [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.
|
|
@ -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 config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
)
|
||||
|
||||
// save stores the cfg struct to file
|
||||
func save(filePath string, cfg interface{}) error {
|
||||
dir := filepath.Dir(filePath)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(cfg)
|
||||
}
|
||||
|
||||
// load reads file, parses json and stores in cfg struct
|
||||
func load(filePath string, cfg interface{}) error {
|
||||
path, err := dir.ConfigFS().SysPath(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// throw error if path is a directory or is a symlink or does not exist.
|
||||
fileInfo, err := os.Lstat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mode := fileInfo.Mode()
|
||||
if mode.IsDir() || mode&fs.ModeSymlink != 0 {
|
||||
return fmt.Errorf("%q is not a regular file (symlinks are not supported)", path)
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return json.NewDecoder(file).Decode(cfg)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +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.
|
||||
|
||||
// Package config provides the ability to load and save config.json and
|
||||
// signingkeys.json.
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
)
|
||||
|
||||
// Config reflects the config.json file.
|
||||
// Specification: https://github.com/notaryproject/notation/pull/76
|
||||
type Config struct {
|
||||
InsecureRegistries []string `json:"insecureRegistries"`
|
||||
CredentialsStore string `json:"credsStore,omitempty"`
|
||||
CredentialHelpers map[string]string `json:"credHelpers,omitempty"`
|
||||
// SignatureFormat defines the signature envelope type for signing
|
||||
SignatureFormat string `json:"signatureFormat,omitempty"`
|
||||
}
|
||||
|
||||
// NewConfig creates a new config file
|
||||
func NewConfig() *Config {
|
||||
return &Config{}
|
||||
}
|
||||
|
||||
// Save stores the config to file
|
||||
func (c *Config) Save() error {
|
||||
path, err := dir.ConfigFS().SysPath(dir.PathConfigFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return save(path, c)
|
||||
}
|
||||
|
||||
// LoadConfig reads the config from file or return a default config if not found.
|
||||
func LoadConfig() (*Config, error) {
|
||||
var config Config
|
||||
|
||||
err := load(dir.PathConfigFile, &config)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return NewConfig(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
)
|
||||
|
||||
var sampleConfig = &Config{
|
||||
InsecureRegistries: []string{
|
||||
"registry.wabbit-networks.io",
|
||||
},
|
||||
SignatureFormat: "jws",
|
||||
}
|
||||
|
||||
func TestLoadFile(t *testing.T) {
|
||||
dir.UserConfigDir = "./testdata/valid"
|
||||
got, err := LoadConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error. err = %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, sampleConfig) {
|
||||
t.Errorf("loadFile() = %v, want %v", got, sampleConfig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveFile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
dir.UserConfigDir = root
|
||||
sampleConfig.Save()
|
||||
config, err := LoadConfig()
|
||||
if err != nil {
|
||||
t.Fatal("Load config file from temp dir failed")
|
||||
}
|
||||
if !reflect.DeepEqual(sampleConfig, config) {
|
||||
t.Fatal("save config file failed.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadNonExistedConfig(t *testing.T) {
|
||||
dir.UserConfigDir = "./testdata/non-existed"
|
||||
got, err := LoadConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig() error. err = %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(got, NewConfig()) {
|
||||
t.Errorf("loadFile() = %v, want %v", got, NewConfig())
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,251 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
|
||||
"github.com/notaryproject/notation-go/internal/slices"
|
||||
"github.com/notaryproject/notation-go/log"
|
||||
"github.com/notaryproject/notation-go/plugin"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
set "github.com/notaryproject/notation-go/internal/container"
|
||||
)
|
||||
|
||||
// X509KeyPair contains the paths of a public/private key pair files.
|
||||
type X509KeyPair struct {
|
||||
KeyPath string `json:"keyPath,omitempty"`
|
||||
CertificatePath string `json:"certPath,omitempty"`
|
||||
}
|
||||
|
||||
// ExternalKey contains the necessary information to delegate
|
||||
// the signing operation to the named plugin.
|
||||
type ExternalKey struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
PluginName string `json:"pluginName,omitempty"`
|
||||
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
|
||||
}
|
||||
|
||||
// KeySuite is a named key suite.
|
||||
type KeySuite struct {
|
||||
Name string `json:"name"`
|
||||
|
||||
*X509KeyPair
|
||||
*ExternalKey
|
||||
}
|
||||
|
||||
// SigningKeys reflects the signingkeys.json file.
|
||||
type SigningKeys struct {
|
||||
Default *string `json:"default,omitempty"`
|
||||
Keys []KeySuite `json:"keys"`
|
||||
}
|
||||
|
||||
// NewSigningKeys creates a new signingkeys config file
|
||||
func NewSigningKeys() *SigningKeys {
|
||||
return &SigningKeys{Keys: []KeySuite{}}
|
||||
}
|
||||
|
||||
// Add adds new signing key
|
||||
func (s *SigningKeys) Add(name, keyPath, certPath string, markDefault bool) error {
|
||||
if name == "" {
|
||||
return ErrKeyNameEmpty
|
||||
}
|
||||
_, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ks := KeySuite{
|
||||
Name: name,
|
||||
X509KeyPair: &X509KeyPair{
|
||||
KeyPath: keyPath,
|
||||
CertificatePath: certPath,
|
||||
},
|
||||
}
|
||||
return s.add(ks, markDefault)
|
||||
}
|
||||
|
||||
// AddPlugin adds new plugin based signing key
|
||||
func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName string, pluginConfig map[string]string, markDefault bool) error {
|
||||
logger := log.GetLogger(ctx)
|
||||
logger.Debugf("Adding key with name %v and plugin name %v", keyName, pluginName)
|
||||
if keyName == "" {
|
||||
return ErrKeyNameEmpty
|
||||
}
|
||||
if id == "" {
|
||||
return errors.New("missing key id")
|
||||
}
|
||||
if pluginName == "" {
|
||||
return errors.New("plugin name cannot be empty")
|
||||
}
|
||||
mgr := plugin.NewCLIManager(dir.PluginFS())
|
||||
_, err := mgr.Get(ctx, pluginName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ks := KeySuite{
|
||||
Name: keyName,
|
||||
ExternalKey: &ExternalKey{
|
||||
ID: id,
|
||||
PluginName: pluginName,
|
||||
PluginConfig: pluginConfig,
|
||||
},
|
||||
}
|
||||
if err = s.add(ks, markDefault); err != nil {
|
||||
logger.Error("Failed to add key with error: %v", err)
|
||||
return err
|
||||
}
|
||||
logger.Debugf("Added key with name %s - {%+v}", keyName, ks)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns signing key for the given name
|
||||
func (s *SigningKeys) Get(keyName string) (KeySuite, error) {
|
||||
if keyName == "" {
|
||||
return KeySuite{}, ErrKeyNameEmpty
|
||||
}
|
||||
idx := slices.IndexIsser(s.Keys, keyName)
|
||||
if idx < 0 {
|
||||
return KeySuite{}, KeyNotFoundError{KeyName: keyName}
|
||||
}
|
||||
return s.Keys[idx], nil
|
||||
}
|
||||
|
||||
// GetDefault returns default signing key
|
||||
func (s *SigningKeys) GetDefault() (KeySuite, error) {
|
||||
if s.Default == nil {
|
||||
return KeySuite{}, errors.New("default signing key not set." +
|
||||
" Please set default signing key or specify a key name")
|
||||
}
|
||||
return s.Get(*s.Default)
|
||||
}
|
||||
|
||||
// Remove deletes given signing keys and returns a slice of deleted key names
|
||||
func (s *SigningKeys) Remove(keyName ...string) ([]string, error) {
|
||||
var deletedNames []string
|
||||
for _, name := range keyName {
|
||||
if name == "" {
|
||||
return deletedNames, ErrKeyNameEmpty
|
||||
}
|
||||
idx := slices.IndexIsser(s.Keys, name)
|
||||
if idx < 0 {
|
||||
return deletedNames, KeyNotFoundError{KeyName: name}
|
||||
}
|
||||
s.Keys = slices.Delete(s.Keys, idx)
|
||||
deletedNames = append(deletedNames, name)
|
||||
if s.Default != nil && *s.Default == name {
|
||||
s.Default = nil
|
||||
}
|
||||
}
|
||||
return deletedNames, nil
|
||||
}
|
||||
|
||||
// UpdateDefault updates default signing key
|
||||
func (s *SigningKeys) UpdateDefault(keyName string) error {
|
||||
if keyName == "" {
|
||||
return ErrKeyNameEmpty
|
||||
}
|
||||
if !slices.ContainsIsser(s.Keys, keyName) {
|
||||
return KeyNotFoundError{KeyName: keyName}
|
||||
}
|
||||
s.Default = &keyName
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save SigningKeys to signingkeys.json file
|
||||
func (s *SigningKeys) Save() error {
|
||||
path, err := dir.ConfigFS().SysPath(dir.PathSigningKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validateKeys(s); err != nil {
|
||||
return err
|
||||
}
|
||||
return save(path, s)
|
||||
}
|
||||
|
||||
// LoadSigningKeys reads the signingkeys.json file
|
||||
// or return a default config if not found.
|
||||
func LoadSigningKeys() (*SigningKeys, error) {
|
||||
var config SigningKeys
|
||||
err := load(dir.PathSigningKeys, &config)
|
||||
if err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return NewSigningKeys(), nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := validateKeys(&config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// LoadExecSaveSigningKeys loads signing key, executes given function and
|
||||
// then saves the signing key
|
||||
func LoadExecSaveSigningKeys(fn func(keys *SigningKeys) error) error {
|
||||
// core process
|
||||
signingKeys, err := LoadSigningKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fn(signingKeys); err != nil {
|
||||
return err
|
||||
}
|
||||
return signingKeys.Save()
|
||||
}
|
||||
|
||||
// Is checks whether the given name is equal with the Name variable
|
||||
func (k KeySuite) Is(name string) bool {
|
||||
return k.Name == name
|
||||
}
|
||||
|
||||
func (s *SigningKeys) add(key KeySuite, markDefault bool) error {
|
||||
if slices.ContainsIsser(s.Keys, key.Name) {
|
||||
return fmt.Errorf("signing key with name %q already exists", key.Name)
|
||||
}
|
||||
s.Keys = append(s.Keys, key)
|
||||
if markDefault {
|
||||
s.Default = &key.Name
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateKeys(config *SigningKeys) error {
|
||||
keys := config.Keys
|
||||
uniqueKeyNames := set.NewWithSize[string](len(keys))
|
||||
for _, key := range keys {
|
||||
if len(key.Name) == 0 {
|
||||
return fmt.Errorf("malformed %s: key name cannot be empty", dir.PathSigningKeys)
|
||||
}
|
||||
if uniqueKeyNames.Contains(key.Name) {
|
||||
return fmt.Errorf("malformed %s: multiple keys with name '%s' found", dir.PathSigningKeys, key.Name)
|
||||
}
|
||||
uniqueKeyNames.Add(key.Name)
|
||||
}
|
||||
if config.Default != nil {
|
||||
defaultKey := *config.Default
|
||||
if len(defaultKey) == 0 {
|
||||
return fmt.Errorf("malformed %s: default key name cannot be empty", dir.PathSigningKeys)
|
||||
}
|
||||
if !uniqueKeyNames.Contains(defaultKey) {
|
||||
return fmt.Errorf("malformed %s: default key '%s' not found", dir.PathSigningKeys, defaultKey)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,456 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/testhelper"
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
)
|
||||
|
||||
var sampleSigningKeysInfo = SigningKeys{
|
||||
Default: Ptr("wabbit-networks"),
|
||||
Keys: []KeySuite{
|
||||
{
|
||||
Name: "wabbit-networks",
|
||||
X509KeyPair: &X509KeyPair{
|
||||
KeyPath: "/home/demo/.config/notation/localkeys/wabbit-networks.key",
|
||||
CertificatePath: "/home/demo/.config/notation/localkeys/wabbit-networks.crt",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "import.acme-rockets",
|
||||
X509KeyPair: &X509KeyPair{
|
||||
KeyPath: "/home/demo/.config/notation/localkeys/import.acme-rockets.key",
|
||||
CertificatePath: "/home/demo/.config/notation/localkeys/import.acme-rockets.crt",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "external-key",
|
||||
ExternalKey: &ExternalKey{
|
||||
|
||||
ID: "id1",
|
||||
PluginName: "pluginX",
|
||||
PluginConfig: map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestLoadSigningKeysInfo(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
dir.UserConfigDir = "./testdata/valid"
|
||||
got, err := LoadSigningKeys()
|
||||
if err != nil {
|
||||
t.Errorf("LoadSigningKeysInfo() error = \"%v\"", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(sampleSigningKeysInfo.Default, got.Default) {
|
||||
t.Fatal("signingKeysInfo test failed.")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(sampleSigningKeysInfo.Keys, got.Keys) {
|
||||
t.Fatal("signingKeysInfo test failed.")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DuplicateKeys", func(t *testing.T) {
|
||||
expectedErr := "malformed signingkeys.json: multiple keys with name 'wabbit-networks' found"
|
||||
dir.UserConfigDir = "./testdata/malformed-duplicate"
|
||||
_, err := LoadSigningKeys()
|
||||
if err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("LoadSigningKeysInfo() error expected = \"%v\" but found = \"%v\"", expectedErr, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidDefault", func(t *testing.T) {
|
||||
expectedErr := "malformed signingkeys.json: default key 'missing-default' not found"
|
||||
dir.UserConfigDir = "./testdata/malformed-invalid-default"
|
||||
_, err := LoadSigningKeys()
|
||||
if err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("LoadSigningKeysInfo() error expected = \"%v\" but found = \"%v\"", expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSaveSigningKeys(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
dir.UserConfigDir = root
|
||||
sampleSigningKeysInfo.Save()
|
||||
info, err := LoadSigningKeys()
|
||||
if err != nil {
|
||||
t.Fatal("Load signingkeys.json from temp dir failed.")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(sampleSigningKeysInfo.Default, info.Default) {
|
||||
t.Fatal("Save signingkeys.json failed.")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(sampleSigningKeysInfo.Keys, info.Keys) {
|
||||
t.Fatal("Save signingkeys.json failed.")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidWithoutDefault", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
dir.UserConfigDir = root
|
||||
sampleSigningKeysInfoNoDefault := deepCopySigningKeys(sampleSigningKeysInfo)
|
||||
sampleSigningKeysInfoNoDefault.Default = nil
|
||||
sampleSigningKeysInfoNoDefault.Save()
|
||||
info, err := LoadSigningKeys()
|
||||
if err != nil {
|
||||
t.Fatal("Load signingkeys.json from temp dir failed.")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(sampleSigningKeysInfoNoDefault.Default, info.Default) {
|
||||
t.Fatal("Save signingkeys.json failed.")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(sampleSigningKeysInfoNoDefault.Keys, info.Keys) {
|
||||
t.Fatal("Save signingkeys.json failed.")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DuplicateKeys", func(t *testing.T) {
|
||||
expectedErr := "malformed signingkeys.json: multiple keys with name 'import.acme-rockets' found"
|
||||
dir.UserConfigDir = t.TempDir()
|
||||
duplicateKeySignKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
|
||||
duplicateKeySignKeysInfo.Keys = append(duplicateKeySignKeysInfo.Keys, KeySuite{
|
||||
Name: "import.acme-rockets",
|
||||
X509KeyPair: &X509KeyPair{
|
||||
KeyPath: "/keypath",
|
||||
CertificatePath: "/CertificatePath",
|
||||
},
|
||||
})
|
||||
err := duplicateKeySignKeysInfo.Save()
|
||||
if err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Save signingkeys.json failed, error expected = \"%v\" but found = \"%v\"", expectedErr, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyKeyName", func(t *testing.T) {
|
||||
expectedErr := "malformed signingkeys.json: key name cannot be empty"
|
||||
dir.UserConfigDir = t.TempDir()
|
||||
emptyKeyNameSignKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
|
||||
emptyKeyNameSignKeysInfo.Keys[0].Name = ""
|
||||
|
||||
err := emptyKeyNameSignKeysInfo.Save()
|
||||
if err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Save signingkeys.json failed, error expected = \"%v\" but found = \"%v\"", expectedErr, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidDefault", func(t *testing.T) {
|
||||
expectedErr := "malformed signingkeys.json: default key 'missing-default' not found"
|
||||
dir.UserConfigDir = t.TempDir()
|
||||
invalidDefaultSignKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
|
||||
invalidDefaultSignKeysInfo.Default = Ptr("missing-default")
|
||||
err := invalidDefaultSignKeysInfo.Save()
|
||||
if err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Save signingkeys.json failed, error expected = \"%v\" but found = \"%v\"", expectedErr, err)
|
||||
}
|
||||
|
||||
expectedErr = "malformed signingkeys.json: default key name cannot be empty"
|
||||
invalidDefaultSignKeysInfo.Default = Ptr("")
|
||||
err = invalidDefaultSignKeysInfo.Save()
|
||||
if err == nil || err.Error() != expectedErr {
|
||||
t.Errorf("Save signingkeys.json failed, error expected = \"%v\" but found = \"%v\"", expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAdd(t *testing.T) {
|
||||
certPath, keyPath := createTempCertKey(t)
|
||||
t.Run("WithDefault", func(t *testing.T) {
|
||||
testSigningKeys := deepCopySigningKeys(sampleSigningKeysInfo)
|
||||
expectedTestKeyName := "name1"
|
||||
|
||||
if err := testSigningKeys.Add(expectedTestKeyName, keyPath, certPath, true); err != nil {
|
||||
t.Errorf("Add() failed with err= %v", err)
|
||||
}
|
||||
|
||||
expectedSigningKeys := append(deepCopySigningKeys(sampleSigningKeysInfo).Keys, KeySuite{
|
||||
Name: expectedTestKeyName,
|
||||
X509KeyPair: &X509KeyPair{
|
||||
KeyPath: keyPath,
|
||||
CertificatePath: certPath,
|
||||
},
|
||||
})
|
||||
|
||||
if expectedTestKeyName != *testSigningKeys.Default {
|
||||
t.Error("Add() failed, incorrect default key")
|
||||
}
|
||||
if !reflect.DeepEqual(testSigningKeys.Keys, expectedSigningKeys) {
|
||||
t.Error("Add() failed, KeySuite mismatch")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("WithoutDefault", func(t *testing.T) {
|
||||
dir.UserConfigDir = t.TempDir()
|
||||
|
||||
testSigningKeys := deepCopySigningKeys(sampleSigningKeysInfo)
|
||||
expectedTestKeyName := "name2"
|
||||
certPath, keyPath := createTempCertKey(t)
|
||||
if err := testSigningKeys.Add(expectedTestKeyName, keyPath, certPath, false); err != nil {
|
||||
t.Errorf("Add() failed with err= %v", err)
|
||||
}
|
||||
|
||||
expectedSigningKeys := append(deepCopySigningKeys(sampleSigningKeysInfo).Keys, KeySuite{
|
||||
Name: expectedTestKeyName,
|
||||
X509KeyPair: &X509KeyPair{
|
||||
KeyPath: keyPath,
|
||||
CertificatePath: certPath,
|
||||
},
|
||||
})
|
||||
|
||||
if *sampleSigningKeysInfo.Default != *testSigningKeys.Default {
|
||||
t.Error("Add() failed, default key changed")
|
||||
}
|
||||
if !reflect.DeepEqual(testSigningKeys.Keys, expectedSigningKeys) {
|
||||
t.Error("Add() failed, KeySuite mismatch")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidCertKeyLocation", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.Add("name1", "invalid", "invalid", true)
|
||||
if err == nil {
|
||||
t.Error("expected Add() to fail for invalid cert and key location")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidName", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.Add("", "invalid", "invalid", true)
|
||||
if err == nil {
|
||||
t.Error("expected Add() to fail for empty key name")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidName", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.Add("", "invalid", "invalid", true)
|
||||
if err == nil {
|
||||
t.Error("expected Add() to fail for empty key name")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("DuplicateKey", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.Add(sampleSigningKeysInfo.Keys[0].Name, "invalid", "invalid", true)
|
||||
if err == nil {
|
||||
t.Error("expected Add() to fail for duplicate name")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestPluginAdd(t *testing.T) {
|
||||
config := map[string]string{"key1": "value1"}
|
||||
name := "name1"
|
||||
id := "pluginId1"
|
||||
pluginName := "pluginName1"
|
||||
|
||||
t.Run("InvalidCertKeyLocation", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.Add("name1", "invalid", "invalid", true)
|
||||
if err == nil {
|
||||
t.Error("expected AddPlugin() to fail for invalid cert and key location")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidName", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.AddPlugin(context.Background(), "", id, pluginName, config, true)
|
||||
if err == nil {
|
||||
t.Error("expected AddPlugin() to fail for empty key name")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidId", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.AddPlugin(context.Background(), name, "", pluginName, config, true)
|
||||
if err == nil {
|
||||
t.Error("expected AddPlugin() to fail for empty key name")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidPluginName", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.AddPlugin(context.Background(), name, id, "", config, true)
|
||||
if err == nil {
|
||||
t.Error("AddPlugin AddPlugin() to fail for empty plugin name")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
key, err := sampleSigningKeysInfo.Get("external-key")
|
||||
if err != nil {
|
||||
t.Errorf("Get() failed with error= %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(key, sampleSigningKeysInfo.Keys[2]) {
|
||||
t.Errorf("Get() returned %v but expected %v", key, sampleSigningKeysInfo.Keys[2])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NonExistent", func(t *testing.T) {
|
||||
_, err := sampleSigningKeysInfo.Get("nonExistent")
|
||||
if err == nil {
|
||||
t.Error("expected Get() to fail for nonExistent key name")
|
||||
}
|
||||
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
|
||||
t.Error("expected Get() to return ErrorKeyNotFound")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyName", func(t *testing.T) {
|
||||
_, err := sampleSigningKeysInfo.Get("")
|
||||
if err == nil {
|
||||
t.Error("expected Get() to fail for empty key name")
|
||||
}
|
||||
if !errors.Is(err, ErrKeyNameEmpty) {
|
||||
t.Error("expected Get() to return ErrorKeyNameEmpty")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetDefault(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
key, err := sampleSigningKeysInfo.GetDefault()
|
||||
if err != nil {
|
||||
t.Errorf("GetDefault() failed with error= %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(key.Name, *sampleSigningKeysInfo.Default) {
|
||||
t.Errorf("GetDefault() returned %s but expected %s", key.Name, *sampleSigningKeysInfo.Default)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoDefault", func(t *testing.T) {
|
||||
testSigningKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
|
||||
testSigningKeysInfo.Default = nil
|
||||
if _, err := testSigningKeysInfo.GetDefault(); err == nil {
|
||||
t.Error("GetDefault Get() to fail there is no defualt key")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestUpdateDefault(t *testing.T) {
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
testSigningKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
|
||||
newDefault := sampleSigningKeysInfo.Keys[1].Name
|
||||
err := testSigningKeysInfo.UpdateDefault(newDefault)
|
||||
if err != nil {
|
||||
t.Errorf("UpdateDefault() failed with error= %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(newDefault, *testSigningKeysInfo.Default) {
|
||||
t.Errorf("UpdateDefault() didn't update default key")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NonExistent", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.UpdateDefault("nonExistent")
|
||||
if err == nil {
|
||||
t.Error("expected Get() to fail for nonExistent key name")
|
||||
}
|
||||
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
|
||||
t.Error("expected Get() to return ErrorKeyNotFound")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyName", func(t *testing.T) {
|
||||
err := sampleSigningKeysInfo.UpdateDefault("")
|
||||
if err == nil {
|
||||
t.Error("expected Get() to fail for empty key name")
|
||||
}
|
||||
if !errors.Is(err, ErrKeyNameEmpty) {
|
||||
t.Error("expected Get() to return ErrorKeyNameEmpty")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRemove(t *testing.T) {
|
||||
testKeyName := "wabbit-networks"
|
||||
testSigningKeysInfo := deepCopySigningKeys(sampleSigningKeysInfo)
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
keys, err := testSigningKeysInfo.Remove(testKeyName)
|
||||
if err != nil {
|
||||
t.Errorf("testSigningKeysInfo() failed with error= %v", err)
|
||||
}
|
||||
|
||||
if _, err := testSigningKeysInfo.Get(testKeyName); err == nil {
|
||||
t.Error("Delete() filed to delete key")
|
||||
}
|
||||
if keys[0] != testKeyName {
|
||||
t.Error("Delete() deleted key name mismatch")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NonExistent", func(t *testing.T) {
|
||||
_, err := testSigningKeysInfo.Remove("nonExistent")
|
||||
if err == nil {
|
||||
t.Error("expected Get() to fail for nonExistent key name")
|
||||
}
|
||||
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
|
||||
t.Error("expected Get() to return ErrorKeyNotFound")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyName", func(t *testing.T) {
|
||||
_, err := testSigningKeysInfo.Remove("")
|
||||
if err == nil {
|
||||
t.Error("expected Get() to fail for empty key name")
|
||||
}
|
||||
if !errors.Is(err, ErrKeyNameEmpty) {
|
||||
t.Error("expected Get() to return ErrorKeyNameEmpty")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func deepCopySigningKeys(keys SigningKeys) SigningKeys {
|
||||
cpyKeys := make([]KeySuite, len(sampleSigningKeysInfo.Keys))
|
||||
copy(cpyKeys, keys.Keys)
|
||||
cpyDefault := *keys.Default
|
||||
cpySignKeys := keys
|
||||
cpySignKeys.Default = &cpyDefault
|
||||
cpySignKeys.Keys = cpyKeys
|
||||
return cpySignKeys
|
||||
}
|
||||
|
||||
func Ptr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
|
||||
func createTempCertKey(t *testing.T) (string, string) {
|
||||
certTuple := testhelper.GetRSARootCertificate()
|
||||
certPath := filepath.Join(t.TempDir(), "cert.tmp")
|
||||
certData := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certTuple.Cert.Raw})
|
||||
if err := os.WriteFile(certPath, certData, 0600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
keyPath := filepath.Join(t.TempDir(), "key.tmp")
|
||||
keyBytes, _ := x509.MarshalPKCS8PrivateKey(certTuple.PrivateKey)
|
||||
keyData := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
|
||||
if err := os.WriteFile(keyPath, keyData, 0600); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return certPath, keyPath
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"default": "wabbit-networks",
|
||||
"keys": [
|
||||
{
|
||||
"name": "wabbit-networks",
|
||||
"keyPath": "/home/demo/.config/notation/localkeys/wabbit-networks.key",
|
||||
"certPath": "/home/demo/.config/notation/localkeys/wabbit-networks.crt"
|
||||
},
|
||||
{
|
||||
"name": "wabbit-networks",
|
||||
"keyPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.key",
|
||||
"certPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.crt"
|
||||
},
|
||||
{
|
||||
"name": "external-key",
|
||||
"id": "id1",
|
||||
"pluginName": "pluginX",
|
||||
"pluginConfig": {
|
||||
"key": "value"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"default": "missing-default",
|
||||
"keys": [
|
||||
{
|
||||
"name": "wabbit-networks",
|
||||
"keyPath": "/home/demo/.config/notation/localkeys/wabbit-networks.key",
|
||||
"certPath": "/home/demo/.config/notation/localkeys/wabbit-networks.crt"
|
||||
},
|
||||
{
|
||||
"name": "import.acme-rockets",
|
||||
"keyPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.key",
|
||||
"certPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.crt"
|
||||
},
|
||||
{
|
||||
"name": "external-key",
|
||||
"id": "id1",
|
||||
"pluginName": "pluginX",
|
||||
"pluginConfig": {
|
||||
"key": "value"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"insecureRegistries": [
|
||||
"registry.wabbit-networks.io"
|
||||
],
|
||||
"signatureFormat": "jws"
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"default": "wabbit-networks",
|
||||
"keys": [
|
||||
{
|
||||
"name": "wabbit-networks",
|
||||
"keyPath": "/home/demo/.config/notation/localkeys/wabbit-networks.key",
|
||||
"certPath": "/home/demo/.config/notation/localkeys/wabbit-networks.crt"
|
||||
},
|
||||
{
|
||||
"name": "import.acme-rockets",
|
||||
"keyPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.key",
|
||||
"certPath": "/home/demo/.config/notation/localkeys/import.acme-rockets.crt"
|
||||
},
|
||||
{
|
||||
"name": "external-key",
|
||||
"id": "id1",
|
||||
"pluginName": "pluginX",
|
||||
"pluginConfig": {
|
||||
"key": "value"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ReadCertificateFile reads a certificate PEM file.
|
||||
func ReadCertificateFile(path string) ([]*x509.Certificate, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseCertificatePEM(data)
|
||||
}
|
||||
|
||||
// ParseCertificatePEM parses a certificate PEM.
|
||||
func ParseCertificatePEM(data []byte) ([]*x509.Certificate, error) {
|
||||
var certs []*x509.Certificate
|
||||
block, rest := pem.Decode(data)
|
||||
for block != nil {
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
block, rest = pem.Decode(rest)
|
||||
}
|
||||
return certs, nil
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ReadPrivateKeyFile reads a key PEM file as a signing key.
|
||||
func ReadPrivateKeyFile(path string) (crypto.PrivateKey, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParsePrivateKeyPEM(data)
|
||||
}
|
||||
|
||||
// ParsePrivateKeyPEM parses a PEM as a signing key.
|
||||
func ParsePrivateKeyPEM(data []byte) (crypto.PrivateKey, error) {
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil {
|
||||
return nil, errors.New("no PEM data found")
|
||||
}
|
||||
switch block.Type {
|
||||
case "PRIVATE KEY":
|
||||
return x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
return x509.ParseECPrivateKey(block.Bytes)
|
||||
case "RSA PRIVATE KEY":
|
||||
return x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type)
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package jwsutil
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Envelope contains a common payload signed by multiple signatures.
|
||||
type Envelope struct {
|
||||
Payload string `json:"payload,omitempty"`
|
||||
Signatures []Signature `json:"signatures,omitempty"`
|
||||
}
|
||||
|
||||
// Size returns the number of enclosed signatures.
|
||||
func (e Envelope) Size() int {
|
||||
return len(e.Signatures)
|
||||
}
|
||||
|
||||
// Open opens the evelope and returns the first or default complete signature.
|
||||
func (e Envelope) Open() CompleteSignature {
|
||||
if len(e.Signatures) == 0 {
|
||||
return CompleteSignature{
|
||||
Payload: e.Payload,
|
||||
}
|
||||
}
|
||||
return CompleteSignature{
|
||||
Payload: e.Payload,
|
||||
Signature: e.Signatures[0],
|
||||
}
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the JSON serialized JWS.
|
||||
// Reference: RFC 7515 7.2 JWS JSON Serialization.
|
||||
func (e *Envelope) UnmarshalJSON(data []byte) error {
|
||||
var combined struct {
|
||||
CompleteSignature
|
||||
Signatures []Signature `json:"signatures"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &combined); err != nil {
|
||||
return ErrInvalidJSONSerialization
|
||||
}
|
||||
if len(combined.Signatures) == 0 {
|
||||
*e = Envelope{
|
||||
Payload: combined.Payload,
|
||||
Signatures: []Signature{
|
||||
combined.Signature,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
*e = Envelope{
|
||||
Payload: combined.Payload,
|
||||
Signatures: combined.Signatures,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,102 +0,0 @@
|
|||
package jwsutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnvelope_UnmarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data string
|
||||
want Envelope
|
||||
}{
|
||||
{
|
||||
name: "General JWS JSON Serialization Syntax (multiple signatures)",
|
||||
data: `{
|
||||
"payload": "test payload",
|
||||
"signatures": [
|
||||
{
|
||||
"protected": "protected foo",
|
||||
"header": {"unprotected": "foo"},
|
||||
"signature": "signature foo"
|
||||
},
|
||||
{
|
||||
"protected": "protected bar",
|
||||
"header": {"unprotected": "bar"},
|
||||
"signature": "signature bar"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
want: Envelope{
|
||||
Payload: "test payload",
|
||||
Signatures: []Signature{
|
||||
{
|
||||
Protected: "protected foo",
|
||||
Unprotected: []byte(`{"unprotected": "foo"}`),
|
||||
Signature: "signature foo",
|
||||
},
|
||||
{
|
||||
Protected: "protected bar",
|
||||
Unprotected: []byte(`{"unprotected": "bar"}`),
|
||||
Signature: "signature bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "General JWS JSON Serialization Syntax (single signature)",
|
||||
data: `{
|
||||
"payload": "test payload",
|
||||
"signatures": [
|
||||
{
|
||||
"protected": "protected foo",
|
||||
"header": {"unprotected": "foo"},
|
||||
"signature": "signature foo"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
want: Envelope{
|
||||
Payload: "test payload",
|
||||
Signatures: []Signature{
|
||||
{
|
||||
Protected: "protected foo",
|
||||
Unprotected: []byte(`{"unprotected": "foo"}`),
|
||||
Signature: "signature foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Flattened JWS JSON Serialization Syntax",
|
||||
data: `{
|
||||
"payload": "test payload",
|
||||
"protected": "protected foo",
|
||||
"header": {"unprotected": "foo"},
|
||||
"signature": "signature foo"
|
||||
}`,
|
||||
want: Envelope{
|
||||
Payload: "test payload",
|
||||
Signatures: []Signature{
|
||||
{
|
||||
Protected: "protected foo",
|
||||
Unprotected: []byte(`{"unprotected": "foo"}`),
|
||||
Signature: "signature foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got Envelope
|
||||
if err := json.Unmarshal([]byte(tt.data), &got); err != nil {
|
||||
t.Fatalf("Envelope.UnmarshalJSON() error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Envelope.UnmarshalJSON() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package jwsutil
|
||||
|
||||
import "errors"
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrInvalidCompactSerialization = errors.New("invalid compact serialization")
|
||||
ErrInvalidJSONSerialization = errors.New("invalid JSON serialization")
|
||||
)
|
|
@ -1,56 +0,0 @@
|
|||
// Package jwsutil provides serialization utilities for JWT libraries to comfort JWS.
|
||||
// Reference: RFC 7515 JSON Web Signature (JWS).
|
||||
package jwsutil
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Signature represents a detached signature.
|
||||
type Signature struct {
|
||||
Protected string `json:"protected,omitempty"`
|
||||
Unprotected json.RawMessage `json:"header,omitempty"`
|
||||
Signature string `json:"signature,omitempty"`
|
||||
}
|
||||
|
||||
// CompleteSignature represents a clear signed signature.
|
||||
// A CompleteSignature can be viewed as an envelope with a single signature in
|
||||
// flattened JWS JSON serialization syntax.
|
||||
// Reference: RFC 7515 7.2 JWS JSON Serialization.
|
||||
type CompleteSignature struct {
|
||||
Payload string `json:"payload,omitempty"`
|
||||
Signature
|
||||
}
|
||||
|
||||
// Parse parses the compact serialized JWS.
|
||||
// Reference: RFC 7515 7.1 JWS Compact Serialization.
|
||||
func ParseCompact(serialized string) (CompleteSignature, error) {
|
||||
parts := strings.Split(serialized, ".")
|
||||
if len(parts) != 3 {
|
||||
return CompleteSignature{}, ErrInvalidCompactSerialization
|
||||
}
|
||||
return CompleteSignature{
|
||||
Payload: parts[1],
|
||||
Signature: Signature{
|
||||
Protected: parts[0],
|
||||
Signature: parts[2],
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SerializeCompact serialize the signature in JWS Compact Serialization
|
||||
// Reference: RFC 7515 7.1 JWS Compact Serialization.
|
||||
func (s CompleteSignature) SerializeCompact() string {
|
||||
return strings.Join([]string{s.Protected, s.Payload, s.Signature.Signature}, ".")
|
||||
}
|
||||
|
||||
// Enclose packs the signature into an envelope.
|
||||
func (s CompleteSignature) Enclose() Envelope {
|
||||
return Envelope{
|
||||
Payload: s.Payload,
|
||||
Signatures: []Signature{
|
||||
s.Signature,
|
||||
},
|
||||
}
|
||||
}
|
|
@ -1,78 +0,0 @@
|
|||
package timestamp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// maxBodyLength specifies the max content can be received from the possibly malicious
|
||||
// remote server.
|
||||
// The legnth of a regular TSA response with certificates is usually less than 10 KiB.
|
||||
const maxBodyLength = 1 * 1024 * 1024 // 1 MiB
|
||||
|
||||
// httpTimestamper is a HTTP-based timestamper.
|
||||
type httpTimestamper struct {
|
||||
rt http.RoundTripper
|
||||
endpoint string
|
||||
}
|
||||
|
||||
// NewHTTPTimestamper creates a HTTP-based timestamper with the endpoint provided by the TSA.
|
||||
// http.DefaultTransport is used if nil RoundTripper is passed.
|
||||
func NewHTTPTimestamper(rt http.RoundTripper, endpoint string) (Timestamper, error) {
|
||||
if rt == nil {
|
||||
rt = http.DefaultTransport
|
||||
}
|
||||
if _, err := url.Parse(endpoint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &httpTimestamper{
|
||||
rt: rt,
|
||||
endpoint: endpoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Timestamp sends the request to the remote TSA server for timestamping.
|
||||
// Reference: RFC 3161 3.4 Time-Stamp Protocol via HTTP
|
||||
func (ts *httpTimestamper) Timestamp(ctx context.Context, req *Request) (*Response, error) {
|
||||
// prepare for http request
|
||||
reqBytes, err := req.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hReq, err := http.NewRequestWithContext(ctx, http.MethodPost, ts.endpoint, bytes.NewReader(reqBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hReq.Header.Set("Content-Type", "application/timestamp-query")
|
||||
|
||||
// send the request to the remote TSA server
|
||||
hResp, err := ts.rt.RoundTrip(hReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer hResp.Body.Close()
|
||||
|
||||
// verify HTTP response
|
||||
if hResp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status: %s", hResp.Status)
|
||||
}
|
||||
if contentType := hResp.Header.Get("Content-Type"); contentType != "application/timestamp-reply" {
|
||||
return nil, fmt.Errorf("unexpected response content type: %s", contentType)
|
||||
}
|
||||
|
||||
// read response
|
||||
body := io.LimitReader(hResp.Body, maxBodyLength)
|
||||
respBytes, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var resp Response
|
||||
if err := resp.UnmarshalBinary(respBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resp, nil
|
||||
}
|
|
@ -1,233 +0,0 @@
|
|||
package timestamp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/asn1"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/pki"
|
||||
)
|
||||
|
||||
var testRequest = []byte{
|
||||
// Request
|
||||
0x30, 0x37,
|
||||
|
||||
// Version
|
||||
0x02, 0x01, 0x01,
|
||||
|
||||
// MessageImprint
|
||||
0x30, 0x2f,
|
||||
|
||||
// MessageImprint.HashAlgorithm
|
||||
0x30, 0x0b,
|
||||
|
||||
// MessageImprint.HashAlgorithm.Algorithm
|
||||
0x06, 0x09,
|
||||
0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
|
||||
|
||||
// MessageImprint.HashedMessage
|
||||
0x04, 0x20,
|
||||
0x83, 0x26, 0xf4, 0x70, 0x9d, 0x40, 0x1d, 0xfa, 0xbf, 0xa7, 0x83, 0x02, 0xfb, 0x1c, 0xde, 0xa0,
|
||||
0xf1, 0x80, 0x48, 0xa4, 0x40, 0x40, 0xc2, 0x12, 0xbd, 0x8e, 0x28, 0xda, 0x6b, 0xc6, 0x51, 0xc7,
|
||||
|
||||
// CertReq
|
||||
0x01, 0x01, 0xff,
|
||||
}
|
||||
|
||||
func TestHTTPTimestampGranted(t *testing.T) {
|
||||
// setup test server
|
||||
testResp, err := os.ReadFile("testdata/granted.tsq")
|
||||
if err != nil {
|
||||
t.Fatal("failed to read test response:", err)
|
||||
}
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
const wantContentType = "application/timestamp-query"
|
||||
if got := r.Header.Get("Content-Type"); got != wantContentType {
|
||||
t.Fatalf("TimeStampRequest.ContentType = %v, want %v", err, wantContentType)
|
||||
}
|
||||
if got, err := io.ReadAll(r.Body); err != nil {
|
||||
t.Fatalf("TimeStampRequest.Body read error = %v", err)
|
||||
} else if !bytes.Equal(got, testRequest) {
|
||||
t.Fatalf("TimeStampRequest.Body = %v, want %v", got, testRequest)
|
||||
}
|
||||
|
||||
// write reply
|
||||
w.Header().Set("Content-Type", "application/timestamp-reply")
|
||||
if _, err := w.Write(testResp); err != nil {
|
||||
t.Error("failed to write response:", err)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// do timestamp
|
||||
tsa, err := NewHTTPTimestamper(nil, ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPTimestamper() error = %v", err)
|
||||
}
|
||||
message := []byte("notation")
|
||||
req, err := NewRequestFromBytes(message)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequestFromString() error = %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
resp, err := tsa.Timestamp(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("httpTimestamper.Timestamp() error = %v", err)
|
||||
}
|
||||
wantStatus := pki.StatusGranted
|
||||
if got := resp.Status.Status; got != wantStatus {
|
||||
t.Fatalf("Response.Status = %v, want %v", got, wantStatus)
|
||||
}
|
||||
|
||||
// verify timestamp token
|
||||
token, err := resp.SignedToken()
|
||||
if err != nil {
|
||||
t.Fatalf("Response.SignedToken() error = %v", err)
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
rootCABytes, err := os.ReadFile("testdata/GlobalSignRootCA.crt")
|
||||
if err != nil {
|
||||
t.Fatal("failed to read root CA certificate:", err)
|
||||
}
|
||||
if ok := roots.AppendCertsFromPEM(rootCABytes); !ok {
|
||||
t.Fatal("failed to load root CA certificate")
|
||||
}
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
CurrentTime: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
if _, err := token.Verify(opts); err != nil {
|
||||
t.Fatal("SignedToken.Verify() error =", err)
|
||||
}
|
||||
info, err := token.Info()
|
||||
if err != nil {
|
||||
t.Fatal("SignedToken.Info() error =", err)
|
||||
}
|
||||
if err := info.Verify(message); err != nil {
|
||||
t.Errorf("TSTInfo.Verify() error = %v", err)
|
||||
}
|
||||
timestamp, accuracy := info.Timestamp()
|
||||
wantTimestamp := time.Date(2021, 9, 18, 11, 54, 34, 0, time.UTC)
|
||||
if timestamp != wantTimestamp {
|
||||
t.Errorf("TSTInfo.Timestamp() Timestamp = %v, want %v", timestamp, wantTimestamp)
|
||||
}
|
||||
wantAccuracy := time.Second
|
||||
if accuracy != wantAccuracy {
|
||||
t.Errorf("TSTInfo.Timestamp() Accuracy = %v, want %v", accuracy, wantAccuracy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTimestampRejection(t *testing.T) {
|
||||
// setup test server
|
||||
testResp, err := os.ReadFile("testdata/rejection.tsq")
|
||||
if err != nil {
|
||||
t.Fatal("failed to read test response:", err)
|
||||
}
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
const wantContentType = "application/timestamp-query"
|
||||
if got := r.Header.Get("Content-Type"); got != wantContentType {
|
||||
t.Fatalf("TimeStampRequest.ContentType = %v, want %v", err, wantContentType)
|
||||
}
|
||||
if got, err := io.ReadAll(r.Body); err != nil {
|
||||
t.Fatalf("TimeStampRequest.Body read error = %v", err)
|
||||
} else if !bytes.Equal(got, testRequest) {
|
||||
t.Fatalf("TimeStampRequest.Body = %v, want %v", got, testRequest)
|
||||
}
|
||||
|
||||
// write reply
|
||||
w.Header().Set("Content-Type", "application/timestamp-reply")
|
||||
if _, err := w.Write(testResp); err != nil {
|
||||
t.Error("failed to write response:", err)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// do timestamp
|
||||
tsa, err := NewHTTPTimestamper(nil, ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPTimestamper() error = %v", err)
|
||||
}
|
||||
message := []byte("notation")
|
||||
req, err := NewRequestFromBytes(message)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequestFromString() error = %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
resp, err := tsa.Timestamp(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("httpTimestamper.Timestamp() error = %v", err)
|
||||
}
|
||||
wantStatus := pki.StatusRejection
|
||||
if got := resp.Status.Status; got != wantStatus {
|
||||
t.Fatalf("Response.Status = %v, want %v", got, wantStatus)
|
||||
}
|
||||
wantStatusString := []string{"request contains unknown algorithm"}
|
||||
if got := resp.Status.StatusString; !reflect.DeepEqual(got, wantStatusString) {
|
||||
t.Fatalf("Response.StatusString = %v, want %v", got, wantStatusString)
|
||||
}
|
||||
wantFailInfo := asn1.BitString{
|
||||
Bytes: []byte{0x80},
|
||||
BitLength: 1,
|
||||
}
|
||||
if got := resp.Status.FailInfo; !reflect.DeepEqual(got, wantFailInfo) {
|
||||
t.Fatalf("Response.FailInfo = %v, want %v", got, wantFailInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTimestampBadEndpoint(t *testing.T) {
|
||||
// setup test server
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// write reply
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
if _, err := w.Write([]byte("{}")); err != nil {
|
||||
t.Error("failed to write response:", err)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// do timestamp
|
||||
tsa, err := NewHTTPTimestamper(nil, ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPTimestamper() error = %v", err)
|
||||
}
|
||||
req, err := NewRequestFromString("notation")
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequestFromString() error = %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, err = tsa.Timestamp(ctx, req)
|
||||
if err == nil {
|
||||
t.Fatalf("httpTimestamper.Timestamp() error = %v, wantErr %v", err, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPTimestampEndpointNotFound(t *testing.T) {
|
||||
// setup test server
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// do timestamp
|
||||
tsa, err := NewHTTPTimestamper(nil, ts.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHTTPTimestamper() error = %v", err)
|
||||
}
|
||||
req, err := NewRequestFromString("notation")
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequestFromString() error = %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
_, err = tsa.Timestamp(ctx, req)
|
||||
if err == nil {
|
||||
t.Fatalf("httpTimestamper.Timestamp() error = %v, wantErr %v", err, true)
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package timestamp
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/oid"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
)
|
||||
|
||||
// MessageImprint contains the hash of the datum to be time-stamped.
|
||||
// MessageImprint ::= SEQUENCE {
|
||||
// hashAlgorithm AlgorithmIdentifier,
|
||||
// hashedMessage OCTET STRING }
|
||||
type MessageImprint struct {
|
||||
HashAlgorithm pkix.AlgorithmIdentifier
|
||||
HashedMessage []byte
|
||||
}
|
||||
|
||||
// Request is a time-stamping request.
|
||||
// TimeStampReq ::= SEQUENCE {
|
||||
// version INTEGER { v1(1) },
|
||||
// messageImprint MessageImprint,
|
||||
// reqPolicy TSAPolicyID OPTIONAL,
|
||||
// nonce INTEGER OPTIONAL,
|
||||
// certReq BOOLEAN DEFAULT FALSE,
|
||||
// extensions [0] IMPLICIT Extensions OPTIONAL }
|
||||
type Request struct {
|
||||
Version int // fixed to 1 as defined in RFC 3161 2.4.1 Request Format
|
||||
MessageImprint MessageImprint
|
||||
ReqPolicy asn1.ObjectIdentifier `asn1:"optional"`
|
||||
Nonce *big.Int `asn1:"optional"`
|
||||
CertReq bool `asn1:"optional,default:false"`
|
||||
Extensions []pkix.Extension `asn1:"optional,tag:0"`
|
||||
}
|
||||
|
||||
// NewRequest creates a request based on the given digest.
|
||||
func NewRequest(contentDigest digest.Digest) (*Request, error) {
|
||||
hashAlgorithm, err := getOIDFromDigestAlgorithm(contentDigest.Algorithm())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hashedMessage, err := hex.DecodeString(contentDigest.Encoded())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Request{
|
||||
Version: 1,
|
||||
MessageImprint: MessageImprint{
|
||||
HashAlgorithm: pkix.AlgorithmIdentifier{
|
||||
Algorithm: hashAlgorithm,
|
||||
},
|
||||
HashedMessage: hashedMessage,
|
||||
},
|
||||
CertReq: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewRequestFromBytes creates a request based on the given byte slice.
|
||||
func NewRequestFromBytes(content []byte) (*Request, error) {
|
||||
return NewRequest(digest.FromBytes(content))
|
||||
}
|
||||
|
||||
// NewRequestFromString creates a request based on the given string.
|
||||
func NewRequestFromString(content string) (*Request, error) {
|
||||
return NewRequest(digest.FromString(content))
|
||||
}
|
||||
|
||||
// MarshalBinary encodes the request to binary form.
|
||||
// This method implements encoding.BinaryMarshaler
|
||||
func (r *Request) MarshalBinary() ([]byte, error) {
|
||||
if r == nil {
|
||||
return nil, errors.New("nil request")
|
||||
}
|
||||
return asn1.Marshal(*r)
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes the request from binary form.
|
||||
// This method implements encoding.BinaryUnmarshaler
|
||||
func (r *Request) UnmarshalBinary(data []byte) error {
|
||||
_, err := asn1.Unmarshal(data, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// getOIDFromDigestAlgorithm returns corresponding ASN.1 OID for the given digest algorithm.
|
||||
func getOIDFromDigestAlgorithm(alg digest.Algorithm) (asn1.ObjectIdentifier, error) {
|
||||
switch alg {
|
||||
case digest.SHA256:
|
||||
return oid.SHA256, nil
|
||||
case digest.SHA384:
|
||||
return oid.SHA384, nil
|
||||
case digest.SHA512:
|
||||
return oid.SHA512, nil
|
||||
}
|
||||
return nil, digest.ErrDigestUnsupported
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package timestamp
|
||||
|
||||
import (
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/pki"
|
||||
)
|
||||
|
||||
// Response is a time-stamping response.
|
||||
// TimeStampResp ::= SEQUENCE {
|
||||
// status PKIStatusInfo,
|
||||
// timeStampToken TimeStampToken OPTIONAL }
|
||||
type Response struct {
|
||||
Status pki.StatusInfo
|
||||
TimeStampToken asn1.RawValue `asn1:"optional"`
|
||||
}
|
||||
|
||||
// MarshalBinary encodes the response to binary form.
|
||||
// This method implements encoding.BinaryMarshaler
|
||||
func (r *Response) MarshalBinary() ([]byte, error) {
|
||||
if r == nil {
|
||||
return nil, errors.New("nil response")
|
||||
}
|
||||
return asn1.Marshal(r)
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes the response from binary form.
|
||||
// This method implements encoding.BinaryUnmarshaler
|
||||
func (r *Response) UnmarshalBinary(data []byte) error {
|
||||
_, err := asn1.Unmarshal(data, r)
|
||||
return err
|
||||
}
|
||||
|
||||
// TokenBytes returns the bytes of the timestamp token.
|
||||
func (r *Response) TokenBytes() []byte {
|
||||
return r.TimeStampToken.FullBytes
|
||||
}
|
||||
|
||||
// SignedToken returns the timestamp token with signatures.
|
||||
// Callers should invoke Verify to verify the content before comsumption.
|
||||
func (r *Response) SignedToken() (*SignedToken, error) {
|
||||
return ParseSignedToken(r.TokenBytes())
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
0/0-0$"request contains unknown algorithm€
|
|
@ -1,11 +0,0 @@
|
|||
// Package timestamp generates timestamping requests to TSA servers,
|
||||
// and fetches the responses according to RFC 3161.
|
||||
package timestamp
|
||||
|
||||
import "context"
|
||||
|
||||
// Timestamper stamps the time.
|
||||
type Timestamper interface {
|
||||
// Timestamp stamps the time with the given request.
|
||||
Timestamp(context.Context, *Request) (*Response, error)
|
||||
}
|
|
@ -1,269 +0,0 @@
|
|||
// Package timestamptest provides utilities for timestamp testing
|
||||
package timestamptest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"math"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-go-lib/crypto/timestamp"
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/cms"
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/hashutil"
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/oid"
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/pki"
|
||||
)
|
||||
|
||||
// responseRejection is a general response for request rejection.
|
||||
var responseRejection = ×tamp.Response{
|
||||
Status: pki.StatusInfo{
|
||||
Status: pki.StatusRejection,
|
||||
},
|
||||
}
|
||||
|
||||
// TSA is a Timestamping Authority for testing purpose.
|
||||
type TSA struct {
|
||||
// key is the TSA signing key.
|
||||
key *rsa.PrivateKey
|
||||
|
||||
// cert is the self-signed certificate by the TSA signing key.
|
||||
cert *x509.Certificate
|
||||
|
||||
// NowFunc provides the current time. time.Now() is used if nil.
|
||||
NowFunc func() time.Time
|
||||
}
|
||||
|
||||
// NewTSA creates a TSA with random credentials.
|
||||
func NewTSA() (*TSA, error) {
|
||||
// generate key
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// generate certificate
|
||||
serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "timestamp test",
|
||||
},
|
||||
NotBefore: now,
|
||||
NotAfter: now.Add(365 * 24 * time.Hour), // 1 year
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TSA{
|
||||
key: key,
|
||||
cert: cert,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Certificate returns the certificate used by the server.
|
||||
func (tsa *TSA) Certificate() *x509.Certificate {
|
||||
return tsa.cert
|
||||
}
|
||||
|
||||
// Timestamp stamps the time with the given request.
|
||||
func (tsa *TSA) Timestamp(_ context.Context, req *timestamp.Request) (*timestamp.Response, error) {
|
||||
// validate request
|
||||
if req.Version != 1 {
|
||||
return responseRejection, nil
|
||||
}
|
||||
hash, ok := oid.ConvertToHash(req.MessageImprint.HashAlgorithm.Algorithm)
|
||||
if !ok {
|
||||
return responseRejection, nil
|
||||
}
|
||||
if hashedMessage := req.MessageImprint.HashedMessage; len(hashedMessage) != hash.Size() {
|
||||
return responseRejection, nil
|
||||
}
|
||||
|
||||
// generate token info
|
||||
policy := asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 4146, 2} // time-stamp-policies
|
||||
switch hash {
|
||||
case crypto.SHA1:
|
||||
policy = append(policy, 2)
|
||||
case crypto.SHA256, crypto.SHA384, crypto.SHA512:
|
||||
policy = append(policy, 3)
|
||||
default:
|
||||
return responseRejection, nil
|
||||
}
|
||||
infoBytes, err := tsa.generateTokenInfo(req, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// generate signed data
|
||||
signed, err := tsa.generateSignedData(infoBytes, req.CertReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := convertToRawASN1(signed, "explicit,tag:0")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// generate content info
|
||||
contentInfo := cms.ContentInfo{
|
||||
ContentType: oid.SignedData,
|
||||
Content: content,
|
||||
}
|
||||
token, err := convertToRawASN1(contentInfo, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// generate response
|
||||
return ×tamp.Response{
|
||||
Status: pki.StatusInfo{
|
||||
Status: pki.StatusGranted,
|
||||
},
|
||||
TimeStampToken: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateTokenInfo generate timestamp token info.
|
||||
func (tsa *TSA) generateTokenInfo(req *timestamp.Request, policy asn1.ObjectIdentifier) ([]byte, error) {
|
||||
serialNumber, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nowFunc := tsa.NowFunc
|
||||
if nowFunc == nil {
|
||||
nowFunc = time.Now
|
||||
}
|
||||
info := timestamp.TSTInfo{
|
||||
Version: 1,
|
||||
Policy: policy,
|
||||
MessageImprint: req.MessageImprint,
|
||||
SerialNumber: serialNumber,
|
||||
GenTime: nowFunc().UTC().Truncate(time.Second),
|
||||
Accuracy: timestamp.Accuracy{
|
||||
Seconds: 1,
|
||||
},
|
||||
}
|
||||
return asn1.Marshal(info)
|
||||
}
|
||||
|
||||
// generateSignedData generate signed data according to
|
||||
func (tsa *TSA) generateSignedData(infoBytes []byte, requestCert bool) (cms.SignedData, error) {
|
||||
var issuer asn1.RawValue
|
||||
_, err := asn1.Unmarshal(tsa.cert.RawIssuer, &issuer)
|
||||
if err != nil {
|
||||
return cms.SignedData{}, err
|
||||
}
|
||||
contentType, err := convertToRawASN1([]interface{}{oid.TSTInfo}, "set")
|
||||
if err != nil {
|
||||
return cms.SignedData{}, err
|
||||
}
|
||||
infoDigest, err := hashutil.ComputeHash(crypto.SHA256, infoBytes)
|
||||
if err != nil {
|
||||
return cms.SignedData{}, err
|
||||
}
|
||||
messageDigest, err := convertToRawASN1([]interface{}{infoDigest}, "set")
|
||||
if err != nil {
|
||||
return cms.SignedData{}, err
|
||||
}
|
||||
signingTime, err := convertToRawASN1([]interface{}{time.Now().UTC()}, "set")
|
||||
if err != nil {
|
||||
return cms.SignedData{}, err
|
||||
}
|
||||
signed := cms.SignedData{
|
||||
Version: 3,
|
||||
DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{
|
||||
{
|
||||
Algorithm: oid.SHA256,
|
||||
},
|
||||
},
|
||||
EncapsulatedContentInfo: cms.EncapsulatedContentInfo{
|
||||
ContentType: oid.TSTInfo,
|
||||
Content: infoBytes,
|
||||
},
|
||||
SignerInfos: []cms.SignerInfo{
|
||||
{
|
||||
Version: 1,
|
||||
SignerIdentifier: cms.IssuerAndSerialNumber{
|
||||
Issuer: issuer,
|
||||
SerialNumber: tsa.cert.SerialNumber,
|
||||
},
|
||||
DigestAlgorithm: pkix.AlgorithmIdentifier{
|
||||
Algorithm: oid.SHA256,
|
||||
},
|
||||
SignedAttributes: cms.Attributes{
|
||||
{
|
||||
Type: oid.ContentType,
|
||||
Values: contentType,
|
||||
},
|
||||
{
|
||||
Type: oid.MessageDigest,
|
||||
Values: messageDigest,
|
||||
},
|
||||
{
|
||||
Type: oid.SigningTime,
|
||||
Values: signingTime,
|
||||
},
|
||||
},
|
||||
SignatureAlgorithm: pkix.AlgorithmIdentifier{
|
||||
Algorithm: oid.SHA256WithRSA,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
if requestCert {
|
||||
certs, err := convertToRawASN1(tsa.cert.Raw, "tag:0")
|
||||
if err != nil {
|
||||
return cms.SignedData{}, err
|
||||
}
|
||||
signed.Certificates = certs
|
||||
}
|
||||
|
||||
// sign data
|
||||
signer := &signed.SignerInfos[0]
|
||||
encodedAttributes, err := asn1.MarshalWithParams(signer.SignedAttributes, "set")
|
||||
if err != nil {
|
||||
return cms.SignedData{}, err
|
||||
}
|
||||
hashedAttributes, err := hashutil.ComputeHash(crypto.SHA256, encodedAttributes)
|
||||
if err != nil {
|
||||
return cms.SignedData{}, err
|
||||
}
|
||||
signer.Signature, err = rsa.SignPKCS1v15(rand.Reader, tsa.key, crypto.SHA256, hashedAttributes)
|
||||
if err != nil {
|
||||
return cms.SignedData{}, err
|
||||
}
|
||||
return signed, nil
|
||||
}
|
||||
|
||||
// convertToRawASN1 convert any data ASN.1 data structure to asn1.RawValue.
|
||||
func convertToRawASN1(val interface{}, params string) (asn1.RawValue, error) {
|
||||
b, err := asn1.MarshalWithParams(val, params)
|
||||
if err != nil {
|
||||
return asn1.NullRawValue, err
|
||||
}
|
||||
var raw asn1.RawValue
|
||||
_, err = asn1.UnmarshalWithParams(b, &raw, params)
|
||||
if err != nil {
|
||||
return asn1.NullRawValue, err
|
||||
}
|
||||
return raw, nil
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package timestamptest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-go-lib/crypto/timestamp"
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/oid"
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/pki"
|
||||
)
|
||||
|
||||
func TestTSATimestampGranted(t *testing.T) {
|
||||
// prepare TSA
|
||||
now := time.Date(2021, 9, 18, 11, 54, 34, 0, time.UTC)
|
||||
tsa, err := NewTSA()
|
||||
if err != nil {
|
||||
t.Fatalf("NewTSA() error = %v", err)
|
||||
}
|
||||
tsa.NowFunc = func() time.Time {
|
||||
return now
|
||||
}
|
||||
|
||||
// do timestamp
|
||||
message := []byte("notation")
|
||||
req, err := timestamp.NewRequestFromBytes(message)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequestFromString() error = %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
resp, err := tsa.Timestamp(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("TSA.Timestamp() error = %v", err)
|
||||
}
|
||||
wantStatus := pki.StatusGranted
|
||||
if got := resp.Status.Status; got != wantStatus {
|
||||
t.Fatalf("Response.Status = %v, want %v", got, wantStatus)
|
||||
}
|
||||
|
||||
// verify timestamp token
|
||||
token, err := resp.SignedToken()
|
||||
if err != nil {
|
||||
t.Fatalf("Response.SignedToken() error = %v", err)
|
||||
}
|
||||
roots := x509.NewCertPool()
|
||||
roots.AddCert(tsa.Certificate())
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
}
|
||||
if _, err := token.Verify(opts); err != nil {
|
||||
t.Fatal("SignedToken.Verify() error =", err)
|
||||
}
|
||||
info, err := token.Info()
|
||||
if err != nil {
|
||||
t.Fatal("SignedToken.Info() error =", err)
|
||||
}
|
||||
if err := info.Verify(message); err != nil {
|
||||
t.Errorf("TSTInfo.Verify() error = %v", err)
|
||||
}
|
||||
timestamp, accuracy := info.Timestamp()
|
||||
wantTimestamp := now
|
||||
if timestamp != wantTimestamp {
|
||||
t.Errorf("TSTInfo.Timestamp() Timestamp = %v, want %v", timestamp, wantTimestamp)
|
||||
}
|
||||
wantAccuracy := time.Second
|
||||
if accuracy != wantAccuracy {
|
||||
t.Errorf("TSTInfo.Timestamp() Accuracy = %v, want %v", accuracy, wantAccuracy)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTSATimestampRejection(t *testing.T) {
|
||||
// prepare TSA
|
||||
tsa, err := NewTSA()
|
||||
if err != nil {
|
||||
t.Fatalf("NewTSA() error = %v", err)
|
||||
}
|
||||
|
||||
// do timestamp
|
||||
message := []byte("notation")
|
||||
req, err := timestamp.NewRequestFromBytes(message)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequestFromString() error = %v", err)
|
||||
}
|
||||
req.MessageImprint.HashAlgorithm.Algorithm = oid.SHA1WithRSA // set bad algorithm
|
||||
ctx := context.Background()
|
||||
resp, err := tsa.Timestamp(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("TSA.Timestamp() error = %v", err)
|
||||
}
|
||||
wantStatus := pki.StatusRejection
|
||||
if got := resp.Status.Status; got != wantStatus {
|
||||
t.Fatalf("Response.Status = %v, want %v", got, wantStatus)
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
package timestamp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/cms"
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/hashutil"
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/oid"
|
||||
asn1util "github.com/notaryproject/notation-go-lib/internal/encoding/asn1"
|
||||
)
|
||||
|
||||
// SignedToken is a parsed timestamp token with signatures.
|
||||
type SignedToken cms.ParsedSignedData
|
||||
|
||||
// ParseSignedToken parses ASN.1 BER-encoded structure to SignedToken
|
||||
// without verification.
|
||||
// Callers should invoke Verify to verify the content before comsumption.
|
||||
func ParseSignedToken(data []byte) (*SignedToken, error) {
|
||||
data, err := asn1util.ConvertToDER(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signed, err := cms.ParseSignedData(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !oid.TSTInfo.Equal(signed.ContentType) {
|
||||
return nil, fmt.Errorf("unexpected content type: %v", signed.ContentType)
|
||||
}
|
||||
return (*SignedToken)(signed), nil
|
||||
}
|
||||
|
||||
// Verify verifies the signed token as CMS SignedData.
|
||||
// An empty list of KeyUsages in VerifyOptions implies ExtKeyUsageTimeStamping.
|
||||
func (t *SignedToken) Verify(opts x509.VerifyOptions) ([]cms.SignerInfo, error) {
|
||||
if len(opts.KeyUsages) == 0 {
|
||||
opts.KeyUsages = []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}
|
||||
}
|
||||
signed := (*cms.ParsedSignedData)(t)
|
||||
return signed.Verify(opts)
|
||||
}
|
||||
|
||||
// Info returns the timestamping information.
|
||||
func (t *SignedToken) Info() (*TSTInfo, error) {
|
||||
var info TSTInfo
|
||||
if _, err := asn1.Unmarshal(t.Content, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// Accuracy ::= SEQUENCE {
|
||||
// seconds INTEGER OPTIONAL,
|
||||
// millis [0] INTEGER (1..999) OPTIONAL,
|
||||
// micros [1] INTEGER (1..999) OPTIONAL }
|
||||
type Accuracy struct {
|
||||
Seconds int `asn1:"optional"`
|
||||
Milliseconds int `asn1:"optional,tag:0"`
|
||||
Microseconds int `asn1:"optional,tag:1"`
|
||||
}
|
||||
|
||||
// TSTInfo ::= SEQUENCE {
|
||||
// version INTEGER { v1(1) },
|
||||
// policy TSAPolicyId,
|
||||
// messageImprint MessageImprint,
|
||||
// serialNumber INTEGER,
|
||||
// genTime GeneralizedTime,
|
||||
// accuracy Accuracy OPTIONAL,
|
||||
// ordering BOOLEAN DEFAULT FALSE,
|
||||
// nonce INTEGER OPTIONAL,
|
||||
// tsa [0] GeneralName OPTIONAL,
|
||||
// extensions [1] IMPLICIT Extensions OPTIONAL }
|
||||
type TSTInfo struct {
|
||||
Version int // fixed to 1 as defined in RFC 3161 2.4.2 Response Format
|
||||
Policy asn1.ObjectIdentifier
|
||||
MessageImprint MessageImprint
|
||||
SerialNumber *big.Int
|
||||
GenTime time.Time `asn1:"generalized"`
|
||||
Accuracy Accuracy `asn1:"optional"`
|
||||
Ordering bool `asn1:"optional,default:false"`
|
||||
Nonce *big.Int `asn1:"optional"`
|
||||
TSA asn1.RawValue `asn1:"optional,tag:0"`
|
||||
Extensions []pkix.Extension `asn1:"optional,tag:1"`
|
||||
}
|
||||
|
||||
// Verify verifies the message against the timestamp token information.
|
||||
func (tst *TSTInfo) Verify(message []byte) error {
|
||||
hashAlg := tst.MessageImprint.HashAlgorithm.Algorithm
|
||||
hash, ok := oid.ConvertToHash(hashAlg)
|
||||
if !ok {
|
||||
return fmt.Errorf("unrecognized hash algorithm: %v", hashAlg)
|
||||
}
|
||||
messageDigest, err := hashutil.ComputeHash(hash, message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(tst.MessageImprint.HashedMessage, messageDigest) {
|
||||
return errors.New("mismatch message digest")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Timestamp returns the timestamp by TSA and its accuracy.
|
||||
func (tst *TSTInfo) Timestamp() (time.Time, time.Duration) {
|
||||
accuracy := time.Duration(tst.Accuracy.Seconds)*time.Second +
|
||||
time.Duration(tst.Accuracy.Milliseconds)*time.Millisecond +
|
||||
time.Duration(tst.Accuracy.Microseconds)*time.Microsecond
|
||||
return tst.GenTime, accuracy
|
||||
}
|
|
@ -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 dir
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// SysFS is virtual file system interface that support fs.FS and SysPath method.
|
||||
type SysFS interface {
|
||||
fs.FS
|
||||
|
||||
// SysPath returns the real system path of the given path items in the SysFS.
|
||||
SysPath(items ...string) (string, error)
|
||||
}
|
||||
|
||||
type sysFS struct {
|
||||
fs.FS
|
||||
root string
|
||||
}
|
||||
|
||||
// SysPath returns the real system path of the given name in the SysFS.
|
||||
func (s sysFS) SysPath(items ...string) (string, error) {
|
||||
pathItems := []string{s.root}
|
||||
pathItems = append(pathItems, items...)
|
||||
return filepath.Join(pathItems...), nil
|
||||
}
|
||||
|
||||
// NewSysFS returns the SysFS for the given root directory.
|
||||
//
|
||||
// Support one root directory for rc.1, and may support union directories FS
|
||||
// after rc.1.
|
||||
func NewSysFS(root string) SysFS {
|
||||
return sysFS{
|
||||
FS: os.DirFS(root),
|
||||
root: root}
|
||||
}
|
||||
|
||||
// ConfigFS is the config SysFS
|
||||
func ConfigFS() SysFS {
|
||||
return NewSysFS(userConfigDirPath())
|
||||
}
|
||||
|
||||
// PluginFS is the plugin SysFS
|
||||
func PluginFS() SysFS {
|
||||
return NewSysFS(filepath.Join(userLibexecDirPath(), PathPlugins))
|
||||
}
|
||||
|
||||
// CacheFS is the cache SysFS.
|
||||
//
|
||||
// To get the root of crl file cache, use `CacheFS().SysFS(PathCRLCache)`.
|
||||
func CacheFS() SysFS {
|
||||
return NewSysFS(userCacheDirPath())
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dir
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSysFS_SysPath(t *testing.T) {
|
||||
wantPath := filepath.FromSlash("/path/notation/config.json")
|
||||
fsys := NewSysFS("/path/notation")
|
||||
path, err := fsys.SysPath(PathConfigFile)
|
||||
if err != nil {
|
||||
t.Fatalf("SysPath() failed. err = %v", err)
|
||||
}
|
||||
if path != wantPath {
|
||||
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, wantPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOsFs(t *testing.T) {
|
||||
wantData := []byte("data")
|
||||
fsys := NewSysFS("./testdata")
|
||||
|
||||
// read test file
|
||||
path, err := fsys.Open("data.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Open() failed. err = %v", err)
|
||||
}
|
||||
data := make([]byte, 4)
|
||||
_, err = path.Read(data)
|
||||
if err != nil {
|
||||
t.Fatalf("Read() failed. err = %v", err)
|
||||
}
|
||||
if !bytes.Equal(data, wantData) {
|
||||
t.Fatalf("SysFS read failed. got data = %v, want %v", data, wantData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigFS(t *testing.T) {
|
||||
configFS := ConfigFS()
|
||||
path, err := configFS.SysPath(PathConfigFile)
|
||||
if err != nil {
|
||||
t.Fatalf("SysPath() failed. err = %v", err)
|
||||
}
|
||||
if path != filepath.Join(UserConfigDir, PathConfigFile) {
|
||||
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(UserConfigDir, PathConfigFile))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginFS(t *testing.T) {
|
||||
pluginFS := PluginFS()
|
||||
path, err := pluginFS.SysPath("plugin")
|
||||
if err != nil {
|
||||
t.Fatalf("SysPath() failed. err = %v", err)
|
||||
}
|
||||
if path != filepath.Join(userLibexecDirPath(), PathPlugins, "plugin") {
|
||||
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(userLibexecDirPath(), PathPlugins, "plugin"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRLFileCacheFS(t *testing.T) {
|
||||
cacheFS := CacheFS()
|
||||
path, err := cacheFS.SysPath(PathCRLCache)
|
||||
if err != nil {
|
||||
t.Fatalf("SysPath() failed. err = %v", err)
|
||||
}
|
||||
if path != filepath.Join(UserCacheDir, PathCRLCache) {
|
||||
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, UserConfigDir)
|
||||
}
|
||||
}
|
|
@ -0,0 +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 dir implements Notation directory structure.
|
||||
// [directory spec]: https://notaryproject.dev/docs/user-guides/how-to/directory-structure/
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// - Read config.json:
|
||||
// file, err := dir.ConfigFS().Open(dir.PathConfigFile)
|
||||
//
|
||||
// - Get the path of config.json:
|
||||
// path, err := dir.ConfigFS().SysPath(dir.PathConfigFile)
|
||||
//
|
||||
// - Read trustpolicy.json:
|
||||
// file, err := dir.ConfigFS().Open(dir.PathTrustPolicy)
|
||||
//
|
||||
// - Get the path of trustpolicy.json:
|
||||
// path, err := dir.ConfigFS().SysPath(dir.PathTrustPolicy)
|
||||
//
|
||||
// - Set custom configurations directory:
|
||||
// dir.UserConfigDir = '/path/to/configurations/'
|
||||
//
|
||||
// Only user level directory is supported, and system level directory
|
||||
// may be added later.
|
||||
package dir
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
UserConfigDir string // Absolute path of user level {NOTATION_CONFIG}
|
||||
UserLibexecDir string // Absolute path of user level {NOTATION_LIBEXEC}
|
||||
UserCacheDir string // Absolute path of user level {NOTATION_CACHE}
|
||||
)
|
||||
|
||||
const (
|
||||
// notation is the directory name for notation configurations.
|
||||
notation = "notation"
|
||||
)
|
||||
|
||||
// The relative path to {NOTATION_CONFIG}
|
||||
const (
|
||||
// PathConfigFile is the config.json file relative path.
|
||||
PathConfigFile = "config.json"
|
||||
// PathSigningKeys is the signingkeys file relative path.
|
||||
PathSigningKeys = "signingkeys.json"
|
||||
// PathTrustPolicy is the OCI trust policy file relative path.
|
||||
//
|
||||
// Deprecated: PathTrustPolicy exists for historical compatibility and should not be used.
|
||||
// To get OCI trust policy path, use PathOCITrustPolicy.
|
||||
PathTrustPolicy = "trustpolicy.json"
|
||||
// PathOCITrustPolicy is the OCI trust policy file relative path.
|
||||
PathOCITrustPolicy = "trustpolicy.oci.json"
|
||||
// PathBlobTrustPolicy is the Blob trust policy file relative path.
|
||||
PathBlobTrustPolicy = "trustpolicy.blob.json"
|
||||
// LocalKeysDir is the directory name for local key relative path.
|
||||
LocalKeysDir = "localkeys"
|
||||
// LocalCertificateExtension defines the extension of the certificate files.
|
||||
LocalCertificateExtension = ".crt"
|
||||
// LocalKeyExtension defines the extension of the key files.
|
||||
LocalKeyExtension = ".key"
|
||||
// TrustStoreDir is the directory name of trust store.
|
||||
TrustStoreDir = "truststore"
|
||||
)
|
||||
|
||||
// The relative path to {NOTATION_LIBEXEC}
|
||||
const (
|
||||
// PathPlugins is the plugins directory relative path.
|
||||
PathPlugins = "plugins"
|
||||
)
|
||||
|
||||
// The relative path to {NOTATION_CACHE}
|
||||
const (
|
||||
// PathCRLCache is the crl file cache directory relative path.
|
||||
PathCRLCache = "crl"
|
||||
)
|
||||
|
||||
// for unit tests
|
||||
var (
|
||||
userConfigDir = os.UserConfigDir
|
||||
|
||||
userCacheDir = os.UserCacheDir
|
||||
)
|
||||
|
||||
// userConfigDirPath returns the user level {NOTATION_CONFIG} path.
|
||||
func userConfigDirPath() string {
|
||||
if UserConfigDir == "" {
|
||||
userDir, err := userConfigDir()
|
||||
if err != nil {
|
||||
// fallback to current directory
|
||||
UserConfigDir = "." + notation
|
||||
return UserConfigDir
|
||||
}
|
||||
// set user config
|
||||
UserConfigDir = filepath.Join(userDir, notation)
|
||||
}
|
||||
return UserConfigDir
|
||||
}
|
||||
|
||||
// userLibexecDirPath returns the user level {NOTATION_LIBEXEC} path.
|
||||
func userLibexecDirPath() string {
|
||||
if UserLibexecDir == "" {
|
||||
// set user libexec
|
||||
UserLibexecDir = userConfigDirPath()
|
||||
}
|
||||
return UserLibexecDir
|
||||
}
|
||||
|
||||
// userCacheDirPath returns the user level {NOTATION_CACHE} path.
|
||||
func userCacheDirPath() string {
|
||||
if UserCacheDir == "" {
|
||||
userDir, err := userCacheDir()
|
||||
if err != nil {
|
||||
// fallback to current directory
|
||||
UserCacheDir = filepath.Join("."+notation, "cache")
|
||||
return UserCacheDir
|
||||
}
|
||||
// set user cache
|
||||
UserCacheDir = filepath.Join(userDir, notation)
|
||||
}
|
||||
return UserCacheDir
|
||||
}
|
||||
|
||||
// LocalKeyPath returns the local key and local cert relative paths.
|
||||
func LocalKeyPath(name string) (keyPath, certPath string) {
|
||||
basePath := path.Join(LocalKeysDir, name)
|
||||
return basePath + LocalKeyExtension, basePath + LocalCertificateExtension
|
||||
}
|
||||
|
||||
// X509TrustStoreDir returns the trust store relative path.
|
||||
//
|
||||
// items includes named-store and cert-file names.
|
||||
// the directory follows the pattern of
|
||||
// {NOTATION_CONFIG}/truststore/x509/{store-type}/{named-store}/{cert-file}
|
||||
func X509TrustStoreDir(items ...string) string {
|
||||
pathItems := []string{TrustStoreDir, "x509"}
|
||||
pathItems = append(pathItems, items...)
|
||||
return path.Join(pathItems...)
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package dir
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func mockUserPath() (string, error) {
|
||||
return "/path/", nil
|
||||
}
|
||||
|
||||
func setup() {
|
||||
UserConfigDir = ""
|
||||
UserLibexecDir = ""
|
||||
UserCacheDir = ""
|
||||
}
|
||||
|
||||
func Test_UserConfigDirPath(t *testing.T) {
|
||||
userConfigDir = mockUserPath
|
||||
setup()
|
||||
got := userConfigDirPath()
|
||||
if got != "/path/notation" {
|
||||
t.Fatalf(`UserConfigDirPath() = %q, want "/path/notation"`, got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_NoHomeVariable(t *testing.T) {
|
||||
t.Setenv("HOME", "")
|
||||
t.Setenv("XDG_CONFIG_HOME", "")
|
||||
t.Setenv("XDG_CACHE_HOME", "")
|
||||
setup()
|
||||
userConfigDir = os.UserConfigDir
|
||||
got := userConfigDirPath()
|
||||
if got != ".notation" {
|
||||
t.Fatalf(`userConfigDirPath() = %q, want ".notation"`, got)
|
||||
}
|
||||
got = userCacheDirPath()
|
||||
want := filepath.Join("."+notation, "cache")
|
||||
if got != want {
|
||||
t.Fatalf(`userCacheDirPath() = %q, want %q`, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UserLibexecDirPath(t *testing.T) {
|
||||
userConfigDir = mockUserPath
|
||||
setup()
|
||||
got := userLibexecDirPath()
|
||||
if got != "/path/notation" {
|
||||
t.Fatalf(`UserConfigDirPath() = %q, want "/path/notation"`, got)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_UserCacheDirPath(t *testing.T) {
|
||||
userCacheDir = mockUserPath
|
||||
setup()
|
||||
got := userCacheDirPath()
|
||||
if got != "/path/notation" {
|
||||
t.Fatalf(`UserCacheDirPath() = %q, want "/path/notation"`, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalKeyPath(t *testing.T) {
|
||||
userConfigDir = mockUserPath
|
||||
setup()
|
||||
_ = userConfigDirPath()
|
||||
_ = userLibexecDirPath()
|
||||
gotKeyPath, gotCertPath := LocalKeyPath("web")
|
||||
if gotKeyPath != "localkeys/web.key" {
|
||||
t.Fatalf(`LocalKeyPath() gotKeyPath = %q, want "localkeys/web.key"`, gotKeyPath)
|
||||
}
|
||||
if gotCertPath != "localkeys/web.crt" {
|
||||
t.Fatalf(`LocalKeyPath() gotCertPath = %q, want "localkeys/web.crt"`, gotCertPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestX509TrustStoreDir(t *testing.T) {
|
||||
userConfigDir = mockUserPath
|
||||
setup()
|
||||
_ = userConfigDirPath()
|
||||
_ = userLibexecDirPath()
|
||||
if got := X509TrustStoreDir("ca", "web"); got != "truststore/x509/ca/web" {
|
||||
t.Fatalf(`X509TrustStoreDir() = %q, want "truststore/x509/ca/web"`, got)
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
data
|
130
errors.go
130
errors.go
|
@ -1,8 +1,128 @@
|
|||
// 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 "errors"
|
||||
// ErrorPushSignatureFailed is used when failed to push signature to the
|
||||
// target registry.
|
||||
//
|
||||
// Deprecated: Use PushSignatureFailedError instead.
|
||||
type ErrorPushSignatureFailed = PushSignatureFailedError
|
||||
|
||||
// SignOptions errors
|
||||
var (
|
||||
ErrExpiryNotSpecified = errors.New("expiry not specified")
|
||||
)
|
||||
// PushSignatureFailedError is used when failed to push signature to the
|
||||
// target registry.
|
||||
type PushSignatureFailedError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e PushSignatureFailedError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return "failed to push signature to registry with error: " + e.Msg
|
||||
}
|
||||
return "failed to push signature to registry"
|
||||
}
|
||||
|
||||
// ErrorVerificationInconclusive is used when signature verification fails due
|
||||
// to a runtime error (e.g. a network error)
|
||||
//
|
||||
// Deprecated: Use VerificationInconclusiveError instead.
|
||||
type ErrorVerificationInconclusive = VerificationInconclusiveError
|
||||
|
||||
// VerificationInconclusiveError is used when signature verification fails due
|
||||
// to a runtime error (e.g. a network error)
|
||||
type VerificationInconclusiveError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e VerificationInconclusiveError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return "signature verification was inclusive due to an unexpected error"
|
||||
}
|
||||
|
||||
// ErrorNoApplicableTrustPolicy is used when there is no trust policy that
|
||||
// applies to the given artifact
|
||||
//
|
||||
// Deprecated: Use NoApplicableTrustPolicyError instead.
|
||||
type ErrorNoApplicableTrustPolicy = NoApplicableTrustPolicyError
|
||||
|
||||
// NoApplicableTrustPolicyError is used when there is no trust policy that
|
||||
// applies to the given artifact
|
||||
type NoApplicableTrustPolicyError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e NoApplicableTrustPolicyError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return "there is no applicable trust policy for the given artifact"
|
||||
}
|
||||
|
||||
// ErrorSignatureRetrievalFailed is used when notation is unable to retrieve the
|
||||
// digital signature/s for the given artifact
|
||||
//
|
||||
// Deprecated: Use SignatureRetrievalFailedError instead.
|
||||
type ErrorSignatureRetrievalFailed = SignatureRetrievalFailedError
|
||||
|
||||
// SignatureRetrievalFailedError is used when notation is unable to retrieve the
|
||||
// digital signature/s for the given artifact
|
||||
type SignatureRetrievalFailedError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e SignatureRetrievalFailedError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return "unable to retrieve the digital signature from the registry"
|
||||
}
|
||||
|
||||
// ErrorVerificationFailed is used when it is determined that the digital
|
||||
// signature/s is not valid for the given artifact
|
||||
//
|
||||
// Deprecated: Use VerificationFailedError instead.
|
||||
type ErrorVerificationFailed = VerificationFailedError
|
||||
|
||||
// VerificationFailedError is used when it is determined that the digital
|
||||
// signature/s is not valid for the given artifact
|
||||
type VerificationFailedError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e VerificationFailedError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return "signature verification failed"
|
||||
}
|
||||
|
||||
// ErrorUserMetadataVerificationFailed is used when the signature does not
|
||||
// contain the user specified metadata
|
||||
//
|
||||
// Deprecated: Use UserMetadataVerificationFailedError instead.
|
||||
type ErrorUserMetadataVerificationFailed = UserMetadataVerificationFailedError
|
||||
|
||||
// UserMetadataVerificationFailedError is used when the signature does not
|
||||
// contain the user specified metadata
|
||||
type UserMetadataVerificationFailedError struct {
|
||||
Msg string
|
||||
}
|
||||
|
||||
func (e UserMetadataVerificationFailedError) Error() string {
|
||||
if e.Msg != "" {
|
||||
return e.Msg
|
||||
}
|
||||
return "unable to find specified metadata in the signature"
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-core-go/signature/cose"
|
||||
"github.com/notaryproject/notation-core-go/testhelper"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/signer"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
// exampleDesc is an example of the target manifest descriptor.
|
||||
exampleDesc = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||
Digest: "sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c",
|
||||
Size: 528,
|
||||
}
|
||||
|
||||
// exampleCertTuple contains a RSA privateKey and a self-signed X509
|
||||
// certificate generated for demo purpose ONLY.
|
||||
exampleCertTuple = testhelper.GetRSASelfSignedSigningCertTuple("Notation Example self-signed")
|
||||
exampleCerts = []*x509.Certificate{exampleCertTuple.Cert}
|
||||
)
|
||||
|
||||
// ExampleLocalSign demonstrates how to use signer.Sign to sign an artifact
|
||||
// at local (without using a registry.Repository).
|
||||
func Example_localSign() {
|
||||
// exampleSigner is a notation.Signer given key and X509 certificate chain.
|
||||
// Users should replace `exampleCertTuple.PrivateKey` with their own private
|
||||
// key and replace `exampleCerts` with the corresponding full certificate
|
||||
// chain, following the Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// Both COSE ("application/cose") and JWS ("application/jose+json")
|
||||
// signature mediaTypes are supported.
|
||||
exampleSignatureMediaType := cose.MediaTypeEnvelope
|
||||
|
||||
// exampleSignOptions is an example of notation.SignerSignOptions.
|
||||
exampleSignOptions := notation.SignerSignOptions{
|
||||
SignatureMediaType: exampleSignatureMediaType,
|
||||
SigningAgent: "example signing agent",
|
||||
}
|
||||
|
||||
// local sign core process
|
||||
// upon successful signing, signature envelope and signerInfo are returned.
|
||||
// signatureEnvelope can be used in a verification process later on.
|
||||
signatureEnvelope, signerInfo, err := exampleSigner.Sign(context.Background(), exampleDesc, exampleSignOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully signed")
|
||||
|
||||
// a peek of the signature envelope generated from Sign
|
||||
sigBlob, err := signature.ParseEnvelope(exampleSignatureMediaType, signatureEnvelope)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
sigContent, err := sigBlob.Content()
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
fmt.Println("signature Payload ContentType:", sigContent.Payload.ContentType)
|
||||
fmt.Println("signature Payload Content:", string(sigContent.Payload.Content))
|
||||
fmt.Println("signerInfo SigningAgent:", signerInfo.UnsignedAttributes.SigningAgent)
|
||||
|
||||
// Output:
|
||||
// Successfully signed
|
||||
// signature Payload ContentType: application/vnd.cncf.notary.payload.v1+json
|
||||
// signature Payload Content: {"targetArtifact":{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c","size":528}}
|
||||
// signerInfo SigningAgent: example signing agent
|
||||
}
|
|
@ -0,0 +1,203 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature/cose"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-go/verifier"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// examplePolicyDocument is an example of a valid trust policy document.
|
||||
// trust policy document should follow this spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy
|
||||
var examplePolicyDocument = trustpolicy.OCIDocument{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"example/software"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelStrict.Name, Override: map[trustpolicy.ValidationType]trustpolicy.ValidationAction{trustpolicy.TypeRevocation: trustpolicy.ActionSkip}},
|
||||
TrustStores: []string{"ca:valid-trust-store"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ExampleLocalVerify demonstrates how to use verifier.Verify to verify a
|
||||
// signature of the target artifact at local (without using a
|
||||
// registry.Repository).
|
||||
func Example_localVerify() {
|
||||
// exampleArtifactReference is an example of the target artifact reference
|
||||
exampleArtifactReference := "example/software@sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c"
|
||||
|
||||
// Both COSE ("application/cose") and JWS ("application/jose+json")
|
||||
// signature mediaTypes are supported.
|
||||
exampleSignatureMediaType := cose.MediaTypeEnvelope
|
||||
|
||||
// exampleTargetDescriptor is an example of the target manifest descriptor.
|
||||
exampleTargetDescriptor := ocispec.Descriptor{
|
||||
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||
Digest: "sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c",
|
||||
Size: 528,
|
||||
}
|
||||
|
||||
// exampleSignatureEnvelope is a valid signature envelope.
|
||||
exampleSignatureEnvelope := generateExampleSignatureEnvelope()
|
||||
|
||||
// exampleVerifyOptions is an example of notation.VerifierVerifyOptions
|
||||
exampleVerifyOptions := notation.VerifierVerifyOptions{
|
||||
ArtifactReference: exampleArtifactReference,
|
||||
SignatureMediaType: exampleSignatureMediaType,
|
||||
}
|
||||
|
||||
// createTrustStore creates a trust store directory for demo purpose.
|
||||
// Users could use the default trust store from Notary Project and
|
||||
// add trusted certificates into it following the trust store spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
|
||||
if err := createTrustStore(); err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleVerifier is an example of notation.Verifier given
|
||||
// trust policy document and X509 trust store.
|
||||
exampleVerifier, err := verifier.New(&examplePolicyDocument, truststore.NewX509TrustStore(dir.ConfigFS()), nil)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// local verify core process
|
||||
// upon successful verification, the signature verification outcome is
|
||||
// returned.
|
||||
outcome, err := exampleVerifier.Verify(context.Background(), exampleTargetDescriptor, exampleSignatureEnvelope, exampleVerifyOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully verified")
|
||||
|
||||
// a peek of the payload inside the signature envelope
|
||||
fmt.Println("payload ContentType:", outcome.EnvelopeContent.Payload.ContentType)
|
||||
|
||||
// Note, upon successful verification, payload.TargetArtifact from the
|
||||
// signature envelope matches exactly with our exampleTargetDescriptor.
|
||||
// (This check has been done for the user inside verifier.Verify.)
|
||||
fmt.Println("payload Content:", string(outcome.EnvelopeContent.Payload.Content))
|
||||
|
||||
// Output:
|
||||
// Successfully verified
|
||||
// payload ContentType: application/vnd.cncf.notary.payload.v1+json
|
||||
// payload Content: {"targetArtifact":{"mediaType":"application/vnd.docker.distribution.manifest.v2+json","digest":"sha256:c0d488a800e4127c334ad20d61d7bc21b4097540327217dfab52262adc02380c","size":528}}
|
||||
}
|
||||
|
||||
func generateExampleSignatureEnvelope() []byte {
|
||||
// exampleSignatureEnvelopePem is a valid signature envelope in COSE format
|
||||
// wrapped by a PEM block.
|
||||
// The signature envelope is generated in a previous Sign process.
|
||||
// Users should replace it with their own signature envelope.
|
||||
// Regarding how to generate such signature envelopes, users could refer to
|
||||
// `example_localSign_test.go`.
|
||||
exampleSignatureEnvelopePem := `-----BEGIN EXAMPLE SIGNATURE ENVELOPE-----
|
||||
0oRYnqUBOCQCgXgcaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZQN4K2FwcGxp
|
||||
Y2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS5wYXlsb2FkLnYxK2pzb254GmlvLmNuY2Yu
|
||||
bm90YXJ5LnNpZ25pbmdUaW1lwRpju22GeBxpby5jbmNmLm5vdGFyeS5zaWduaW5n
|
||||
U2NoZW1la25vdGFyeS54NTA5ohghgVkDRDCCA0AwggIooAMCAQICAVEwDQYJKoZI
|
||||
hvcNAQELBQAwTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdT
|
||||
ZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxDzANBgNVBAMTBmFscGluZTAgFw0wMDA4
|
||||
MjkxMzUwMDBaGA8yMTIzMDgyOTEzNTAwMFowTjELMAkGA1UEBhMCVVMxCzAJBgNV
|
||||
BAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxDzANBgNV
|
||||
BAMTBmFscGluZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKHIN6hL
|
||||
MjQwy3wfDhw+HYYvjNTTytQLMuTV/OHPoL2nGbqDy08JqTn5upz7exjzwfRu0usc
|
||||
YRTW0cU2H2FIyvpGgo9/F4wUX+ZRnsG0iSlZMUPNv2sVO9HHkltyaWs62oHjfSVE
|
||||
2fyX4uDH54qSE8HJjKIoo/Hhqx7JI8lcmWyXdFjDCkQ1lYSz1IjzFVrf+He2LWSL
|
||||
c2nGkCrV5l4LEwk1qSKJbN4H7TWI60KDLFHpVHQ/LzgFjfSdvP0sgvrkofytSn8l
|
||||
JW6rn5+HYvAxAcZ7T+cJ12GyS9Y7Y7FIBMQFY0MU9cyOfV9+pt7d2CqgkIdXLndN
|
||||
i+aJzm2Os4+ezekCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoG
|
||||
CCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAvAIwS8uxLEaYuqILgnRUm+p0R
|
||||
o7xdezfm+pg297AEZwfE9acb8009usmvgMlkfo46HRMGzCEvtd5Vfvak9i8KpWzl
|
||||
DWOdfT2thkUWO9iY3qMFiN4ipCmjM32VAe5UUxl3RLmQKS20zv9yVXEfX37tNDdV
|
||||
GgD/WBhJUreCQyWAPTPnf0LaPh4iLBNCo/o3Z8CGKLzzzpa8iji3xW/69RhKu5+t
|
||||
8RWc/N4sljWmXbCeTd2B8XTqZGwWwmpThAQyU40CqngGAS6ADTVNDgbJZqhwkgUx
|
||||
J4W6iRzekCshdPUnDpeS8DNULE5dFGObIhiwH4/40n/Th/VS0zxzkvPzdCmueBtp
|
||||
by5jbmNmLm5vdGFyeS5zaWduaW5nQWdlbnRuTm90YXRpb24vMS4wLjBYtXsidGFy
|
||||
Z2V0QXJ0aWZhY3QiOnsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tl
|
||||
ci5kaXN0cmlidXRpb24ubWFuaWZlc3QudjIranNvbiIsImRpZ2VzdCI6InNoYTI1
|
||||
NjpjMGQ0ODhhODAwZTQxMjdjMzM0YWQyMGQ2MWQ3YmMyMWI0MDk3NTQwMzI3MjE3
|
||||
ZGZhYjUyMjYyYWRjMDIzODBjIiwic2l6ZSI6NTI4fX1ZAQBUIYPA45B/iFylmloW
|
||||
s/NpTVsheuedJb6nnXK0XR46BYs4AeCXVVYSRDK2Bq+tA9C7BXHoeCMcqnAHa8qs
|
||||
ZR/fcMa9FrEPI6Pl8QVE/6QRMkT+Drn+9CSFxzHk6CU9S1vRsUVYNcibyejnuVEE
|
||||
RPYaORrnfTc5wIs4XxeqprmrLimMMNn+u82Uadtc57tbHbY8Vh4XEKP++hBJJNvQ
|
||||
E60X5aWKIS2RnOEc4n9T7LdN0bOL1OoM1lW4iTFMhzfcy/VmF8PrOStFS9LllX3J
|
||||
69V0WwHbmD33cjtVBDCF44UXRWgLQGbE6yaaVmdxEUBGKqSUeHf8Gp7WoZ/YaFmz
|
||||
xQr/
|
||||
-----END EXAMPLE SIGNATURE ENVELOPE-----`
|
||||
|
||||
block, _ := pem.Decode([]byte(exampleSignatureEnvelopePem))
|
||||
if block == nil {
|
||||
panic(errors.New("invalid signature envelope pem block"))
|
||||
}
|
||||
|
||||
// block.Bytes contains the binary of the signature envelope.
|
||||
return block.Bytes
|
||||
}
|
||||
|
||||
func createTrustStore() error {
|
||||
// changing the path of the trust store for demo purpose.
|
||||
// Users could keep the default value, i.e. os.UserConfigDir.
|
||||
dir.UserConfigDir = "tmp"
|
||||
|
||||
// an example of a valid X509 self-signed certificate for demo purpose ONLY.
|
||||
// (This self-signed cert is paired with the private key used to
|
||||
// generate the `exampleSignatureEnvelopePem` above.)
|
||||
// Users should replace `exampleX509Certificate` with their own trusted
|
||||
// certificate and add to the trust store, following the
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
|
||||
MA0GA1UEAxMGYWxwaW5lMCAXDTAwMDgyOTEzNTAwMFoYDzIxMjMwODI5MTM1MDAw
|
||||
WjBOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUx
|
||||
DzANBgNVBAoTBk5vdGFyeTEPMA0GA1UEAxMGYWxwaW5lMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEAocg3qEsyNDDLfB8OHD4dhi+M1NPK1Asy5NX84c+g
|
||||
vacZuoPLTwmpOfm6nPt7GPPB9G7S6xxhFNbRxTYfYUjK+kaCj38XjBRf5lGewbSJ
|
||||
KVkxQ82/axU70ceSW3JpazrageN9JUTZ/Jfi4MfnipITwcmMoiij8eGrHskjyVyZ
|
||||
bJd0WMMKRDWVhLPUiPMVWt/4d7YtZItzacaQKtXmXgsTCTWpIols3gftNYjrQoMs
|
||||
UelUdD8vOAWN9J28/SyC+uSh/K1KfyUlbqufn4di8DEBxntP5wnXYbJL1jtjsUgE
|
||||
xAVjQxT1zI59X36m3t3YKqCQh1cud02L5onObY6zj57N6QIDAQABoycwJTAOBgNV
|
||||
HQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQAD
|
||||
ggEBAC8AjBLy7EsRpi6oguCdFSb6nRGjvF17N+b6mDb3sARnB8T1pxvzTT26ya+A
|
||||
yWR+jjodEwbMIS+13lV+9qT2LwqlbOUNY519Pa2GRRY72JjeowWI3iKkKaMzfZUB
|
||||
7lRTGXdEuZApLbTO/3JVcR9ffu00N1UaAP9YGElSt4JDJYA9M+d/Qto+HiIsE0Kj
|
||||
+jdnwIYovPPOlryKOLfFb/r1GEq7n63xFZz83iyWNaZdsJ5N3YHxdOpkbBbCalOE
|
||||
BDJTjQKqeAYBLoANNU0OBslmqHCSBTEnhbqJHN6QKyF09ScOl5LwM1QsTl0UY5si
|
||||
GLAfj/jSf9OH9VLTPHOS8/N0Ka4=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// Adding the certificate into the trust store.
|
||||
if err := os.MkdirAll("tmp/truststore/x509/ca/valid-trust-store", 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile("tmp/truststore/x509/ca/valid-trust-store/NotationLocalExample.pem", []byte(exampleX509Certificate), 0600)
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature/cose"
|
||||
"github.com/notaryproject/notation-core-go/testhelper"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/signer"
|
||||
)
|
||||
|
||||
// Both COSE ("application/cose") and JWS ("application/jose+json")
|
||||
// signature mediaTypes are supported.
|
||||
var exampleSignatureMediaType = cose.MediaTypeEnvelope
|
||||
|
||||
// ExampleRemoteSign demonstrates how to use notation.Sign to sign an artifact
|
||||
// in the remote registry and push the signature to the remote.
|
||||
func Example_remoteSign() {
|
||||
// exampleArtifactReference is an example of the target artifact reference
|
||||
var exampleArtifactReference = "localhost:5000/software@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e"
|
||||
|
||||
// exampleCertTuple contains a RSA privateKey and a self-signed X509
|
||||
// certificate generated for demo purpose ONLY.
|
||||
exampleCertTuple := testhelper.GetRSASelfSignedSigningCertTuple("Notation Example self-signed")
|
||||
exampleCerts := []*x509.Certificate{exampleCertTuple.Cert}
|
||||
|
||||
// exampleSigner is a notation.Signer given key and X509 certificate chain.
|
||||
// Users should replace `exampleCertTuple.PrivateKey` with their own private
|
||||
// key and replace `exampleCerts` with the corresponding full certificate
|
||||
// chain, following the Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleRepo is an example of registry.Repository.
|
||||
remoteRepo, err := remote.NewRepository(exampleArtifactReference)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
exampleRepo := registry.NewRepository(remoteRepo)
|
||||
|
||||
// exampleSignOptions is an example of notation.SignOptions.
|
||||
exampleSignOptions := notation.SignOptions{
|
||||
SignerSignOptions: notation.SignerSignOptions{
|
||||
SignatureMediaType: exampleSignatureMediaType,
|
||||
},
|
||||
ArtifactReference: exampleArtifactReference,
|
||||
}
|
||||
|
||||
// remote sign core process
|
||||
// upon successful signing, descriptor of the sign content is returned and
|
||||
// the generated signature is pushed into remote registry.
|
||||
targetManifestDesc, sigManifestDesc, err := notation.SignOCI(context.Background(), exampleSigner, exampleRepo, exampleSignOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully signed")
|
||||
fmt.Println("targetManifestDesc.MediaType:", targetManifestDesc.MediaType)
|
||||
fmt.Println("targetManifestDesc.Digest:", targetManifestDesc.Digest)
|
||||
fmt.Println("targetManifestDesc.Size:", targetManifestDesc.Size)
|
||||
fmt.Println("sigManifestDesc.MediaType:", sigManifestDesc.MediaType)
|
||||
fmt.Println("sigManifestDesc.Digest:", sigManifestDesc.Digest)
|
||||
fmt.Println("sigManifestDesc.Size:", sigManifestDesc.Size)
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package notation_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
_ "github.com/notaryproject/notation-core-go/signature/cose"
|
||||
_ "github.com/notaryproject/notation-core-go/signature/jws"
|
||||
"github.com/notaryproject/notation-go"
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
"github.com/notaryproject/notation-go/registry"
|
||||
"github.com/notaryproject/notation-go/verifier"
|
||||
"github.com/notaryproject/notation-go/verifier/trustpolicy"
|
||||
"github.com/notaryproject/notation-go/verifier/truststore"
|
||||
"oras.land/oras-go/v2/registry/remote"
|
||||
)
|
||||
|
||||
// ExampleRemoteVerify demonstrates how to use notation.Verify to verify
|
||||
// signatures of an artifact in the remote registry.
|
||||
func Example_remoteVerify() {
|
||||
// exampleArtifactReference is an example of the target artifact reference
|
||||
exampleArtifactReference := "localhost:5000/software@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e"
|
||||
|
||||
// examplePolicyDocument is an example of a valid trust policy document.
|
||||
// trust policy document should follow this spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy
|
||||
examplePolicyDocument := trustpolicy.OCIDocument{
|
||||
Version: "1.0",
|
||||
TrustPolicies: []trustpolicy.OCITrustPolicy{
|
||||
{
|
||||
Name: "test-statement-name",
|
||||
RegistryScopes: []string{"*"},
|
||||
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: trustpolicy.LevelStrict.Name},
|
||||
TrustStores: []string{"ca:valid-trust-store"},
|
||||
TrustedIdentities: []string{"*"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// generateTrustStore generates a trust store directory for demo purpose.
|
||||
// Users should configure their own trust store and add trusted certificates
|
||||
// into it following the trust store spec:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
|
||||
if err := generateTrustStore(); err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleVerifier is an example of notation.Verifier given
|
||||
// trust policy document and X509 trust store.
|
||||
exampleVerifier, err := verifier.New(&examplePolicyDocument, truststore.NewX509TrustStore(dir.ConfigFS()), nil)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
// exampleRepo is an example of registry.Repository.
|
||||
remoteRepo, err := remote.NewRepository(exampleArtifactReference)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
exampleRepo := registry.NewRepository(remoteRepo)
|
||||
|
||||
// exampleVerifyOptions is an example of notation.VerifyOptions.
|
||||
exampleVerifyOptions := notation.VerifyOptions{
|
||||
ArtifactReference: exampleArtifactReference,
|
||||
MaxSignatureAttempts: 50,
|
||||
}
|
||||
|
||||
// remote verify core process
|
||||
// upon successful verification, the target manifest descriptor
|
||||
// and signature verification outcome are returned.
|
||||
targetDesc, _, err := notation.Verify(context.Background(), exampleVerifier, exampleRepo, exampleVerifyOptions)
|
||||
if err != nil {
|
||||
panic(err) // Handle error
|
||||
}
|
||||
|
||||
fmt.Println("Successfully verified")
|
||||
fmt.Println("targetDesc MediaType:", targetDesc.MediaType)
|
||||
fmt.Println("targetDesc Digest:", targetDesc.Digest)
|
||||
fmt.Println("targetDesc Size:", targetDesc.Size)
|
||||
}
|
||||
|
||||
func generateTrustStore() error {
|
||||
// changing the path of the trust store for demo purpose.
|
||||
// Users could keep the default value, i.e. os.UserConfigDir.
|
||||
dir.UserConfigDir = "tmp"
|
||||
|
||||
// an example of a valid X509 self-signed certificate for demo purpose ONLY.
|
||||
// Users should replace `exampleX509Certificate` with their own trusted
|
||||
// certificate and add to the trust store, following the
|
||||
// Notary Project certificate requirements:
|
||||
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
|
||||
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
|
||||
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
|
||||
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
|
||||
MA0GA1UEAxMGYWxwaW5lMCAXDTAwMDgyOTEzNTAwMFoYDzIxMjMwODI5MTM1MDAw
|
||||
WjBOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUx
|
||||
DzANBgNVBAoTBk5vdGFyeTEPMA0GA1UEAxMGYWxwaW5lMIIBIjANBgkqhkiG9w0B
|
||||
AQEFAAOCAQ8AMIIBCgKCAQEAocg3qEsyNDDLfB8OHD4dhi+M1NPK1Asy5NX84c+g
|
||||
vacZuoPLTwmpOfm6nPt7GPPB9G7S6xxhFNbRxTYfYUjK+kaCj38XjBRf5lGewbSJ
|
||||
KVkxQ82/axU70ceSW3JpazrageN9JUTZ/Jfi4MfnipITwcmMoiij8eGrHskjyVyZ
|
||||
bJd0WMMKRDWVhLPUiPMVWt/4d7YtZItzacaQKtXmXgsTCTWpIols3gftNYjrQoMs
|
||||
UelUdD8vOAWN9J28/SyC+uSh/K1KfyUlbqufn4di8DEBxntP5wnXYbJL1jtjsUgE
|
||||
xAVjQxT1zI59X36m3t3YKqCQh1cud02L5onObY6zj57N6QIDAQABoycwJTAOBgNV
|
||||
HQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQAD
|
||||
ggEBAC8AjBLy7EsRpi6oguCdFSb6nRGjvF17N+b6mDb3sARnB8T1pxvzTT26ya+A
|
||||
yWR+jjodEwbMIS+13lV+9qT2LwqlbOUNY519Pa2GRRY72JjeowWI3iKkKaMzfZUB
|
||||
7lRTGXdEuZApLbTO/3JVcR9ffu00N1UaAP9YGElSt4JDJYA9M+d/Qto+HiIsE0Kj
|
||||
+jdnwIYovPPOlryKOLfFb/r1GEq7n63xFZz83iyWNaZdsJ5N3YHxdOpkbBbCalOE
|
||||
BDJTjQKqeAYBLoANNU0OBslmqHCSBTEnhbqJHN6QKyF09ScOl5LwM1QsTl0UY5si
|
||||
GLAfj/jSf9OH9VLTPHOS8/N0Ka4=
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
// Adding the certificate into the trust store.
|
||||
if err := os.MkdirAll("tmp/truststore/x509/ca/valid-trust-store", 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile("tmp/truststore/x509/ca/valid-trust-store/NotationExample.pem", []byte(exampleX509Certificate), 0600)
|
||||
}
|
|
@ -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-----
|
||||
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
|
||||
-----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)
|
||||
}
|
24
go.mod
24
go.mod
|
@ -1,8 +1,26 @@
|
|||
module github.com/notaryproject/notation-go-lib
|
||||
module github.com/notaryproject/notation-go
|
||||
|
||||
go 1.17
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v4 v4.1.0
|
||||
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.1
|
||||
github.com/veraison/go-cose v1.3.0
|
||||
golang.org/x/crypto v0.39.0
|
||||
golang.org/x/mod v0.25.0
|
||||
oras.land/oras-go/v2 v2.6.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
)
|
||||
|
|
60
go.sum
60
go.sum
|
@ -1,4 +1,60 @@
|
|||
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
|
||||
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
|
||||
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
|
||||
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
|
||||
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
|
||||
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
|
||||
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
|
||||
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
|
||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||
github.com/notaryproject/notation-core-go v1.3.0 h1:mWJaw1QBpBxpjLSiKOjzbZvB+xh2Abzk14FHWQ+9Kfs=
|
||||
github.com/notaryproject/notation-core-go v1.3.0/go.mod h1:hzvEOit5lXfNATGNBT8UQRx2J6Fiw/dq/78TQL8aE64=
|
||||
github.com/notaryproject/notation-plugin-framework-go v1.0.0 h1:6Qzr7DGXoCgXEQN+1gTZWuJAZvxh3p8Lryjn5FaLzi4=
|
||||
github.com/notaryproject/notation-plugin-framework-go v1.0.0/go.mod h1:RqWSrTOtEASCrGOEffq0n8pSg2KOgKYiWqFWczRSics=
|
||||
github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4=
|
||||
github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
|
||||
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7rk=
|
||||
github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
|
||||
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package set
|
||||
|
||||
// Set is a map as a set data structure.
|
||||
type Set[T comparable] map[T]struct{}
|
||||
|
||||
// Add adds the element of type T into the Set.
|
||||
func (s Set[T]) Add(elem T) {
|
||||
s[elem] = struct{}{}
|
||||
}
|
||||
|
||||
// Contains checks if element exists in the Set.
|
||||
func (s Set[T]) Contains(elem T) bool {
|
||||
_, ok := s[elem]
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// New creates an empty Set for elements of type T.
|
||||
func New[T comparable]() Set[T] {
|
||||
return make(map[T]struct{})
|
||||
}
|
||||
|
||||
// NewWithSize creates an empty Set of fixed size for elements of type T.
|
||||
func NewWithSize[T comparable](size int) Set[T] {
|
||||
return make(map[T]struct{}, size)
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
// Package cms verifies signatures in Cryptographic Message Syntax (CMS) / PKCS7
|
||||
// defined in RFC 5652.
|
||||
package cms
|
||||
|
||||
import (
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// ContentInfo ::= SEQUENCE {
|
||||
// contentType ContentType,
|
||||
// content [0] EXPLICIT ANY DEFINED BY contentType }
|
||||
type ContentInfo struct {
|
||||
ContentType asn1.ObjectIdentifier
|
||||
Content asn1.RawValue `asn1:"explicit,tag:0"`
|
||||
}
|
||||
|
||||
// SignedData ::= SEQUENCE {
|
||||
// version CMSVersion,
|
||||
// digestAlgorithms DigestAlgorithmIdentifiers,
|
||||
// encapContentInfo EncapsulatedContentInfo,
|
||||
// certificates [0] IMPLICIT CertificateSet OPTIONAL,
|
||||
// crls [1] IMPLICIT CertificateRevocationLists OPTIONAL,
|
||||
// signerInfos SignerInfos }
|
||||
type SignedData struct {
|
||||
Version int
|
||||
DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"`
|
||||
EncapsulatedContentInfo EncapsulatedContentInfo
|
||||
Certificates asn1.RawValue `asn1:"optional,tag:0"`
|
||||
CRLs []pkix.CertificateList `asn1:"optional,tag:1"`
|
||||
SignerInfos []SignerInfo `asn1:"set"`
|
||||
}
|
||||
|
||||
// EncapsulatedContentInfo ::= SEQUENCE {
|
||||
// eContentType ContentType,
|
||||
// eContent [0] EXPLICIT OCTET STRING OPTIONAL }
|
||||
type EncapsulatedContentInfo struct {
|
||||
ContentType asn1.ObjectIdentifier
|
||||
Content []byte `asn1:"explicit,optional,tag:0"`
|
||||
}
|
||||
|
||||
// SignerInfo ::= SEQUENCE {
|
||||
// version CMSVersion,
|
||||
// sid SignerIdentifier,
|
||||
// digestAlgorithm DigestAlgorithmIdentifier,
|
||||
// signedAttrs [0] IMPLICIT SignedAttributes OPTIONAL,
|
||||
// signatureAlgorithm SignatureAlgorithmIdentifier,
|
||||
// signature SignatureValue,
|
||||
// unsignedAttrs [1] IMPLICIT UnsignedAttributes OPTIONAL }
|
||||
// Only version 1 is supported. As defined in RFC 5652 5.3, SignerIdentifier
|
||||
// is IssuerAndSerialNumber when version is 1.
|
||||
type SignerInfo struct {
|
||||
Version int
|
||||
SignerIdentifier IssuerAndSerialNumber
|
||||
DigestAlgorithm pkix.AlgorithmIdentifier
|
||||
SignedAttributes Attributes `asn1:"optional,tag:0"`
|
||||
SignatureAlgorithm pkix.AlgorithmIdentifier
|
||||
Signature []byte
|
||||
UnsignedAttributes Attributes `asn1:"optional,tag:1"`
|
||||
}
|
||||
|
||||
// IssuerAndSerialNumber ::= SEQUENCE {
|
||||
// issuer Name,
|
||||
// serialNumber CertificateSerialNumber }
|
||||
type IssuerAndSerialNumber struct {
|
||||
Issuer asn1.RawValue
|
||||
SerialNumber *big.Int
|
||||
}
|
||||
|
||||
// Attribute ::= SEQUENCE {
|
||||
// attrType OBJECT IDENTIFIER,
|
||||
// attrValues SET OF AttributeValue }
|
||||
type Attribute struct {
|
||||
Type asn1.ObjectIdentifier
|
||||
Values asn1.RawValue `asn1:"set"`
|
||||
}
|
||||
|
||||
// Attribute ::= SET SIZE (1..MAX) OF Attribute
|
||||
type Attributes []Attribute
|
||||
|
||||
// TryGet tries to find the attribute by the given identifier, parse and store
|
||||
// the result in the value pointed to by out.
|
||||
func (a Attributes) TryGet(identifier asn1.ObjectIdentifier, out interface{}) error {
|
||||
for _, attribute := range a {
|
||||
if identifier.Equal(attribute.Type) {
|
||||
_, err := asn1.Unmarshal(attribute.Values.Bytes, out)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return ErrAttributeNotFound
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
package cms
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrExpectSignedData is returned if wrong content is provided when signed
|
||||
// data is expected.
|
||||
var ErrExpectSignedData = errors.New("cms: signed data expected")
|
||||
|
||||
// ErrAttributeNotFound is returned if attribute is not found in a given set.
|
||||
var ErrAttributeNotFound = errors.New("attribute not found")
|
||||
|
||||
// Verification errors
|
||||
var (
|
||||
ErrSignerNotFound = VerificationError{Message: "signer not found"}
|
||||
ErrCertificateNotFound = VerificationError{Message: "certificate not found"}
|
||||
)
|
||||
|
||||
// SyntaxError indicates that the ASN.1 data is invalid.
|
||||
type SyntaxError struct {
|
||||
Message string
|
||||
Detail error
|
||||
}
|
||||
|
||||
// Error returns error message.
|
||||
func (e SyntaxError) Error() string {
|
||||
msg := "cms: syntax error"
|
||||
if e.Message != "" {
|
||||
msg += ": " + e.Message
|
||||
}
|
||||
if e.Detail != nil {
|
||||
msg += ": " + e.Detail.Error()
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// Unwrap returns the internal error.
|
||||
func (e SyntaxError) Unwrap() error {
|
||||
return e.Detail
|
||||
}
|
||||
|
||||
// VerificationError indicates verification failures.
|
||||
type VerificationError struct {
|
||||
Message string
|
||||
Detail error
|
||||
}
|
||||
|
||||
// Error returns error message.
|
||||
func (e VerificationError) Error() string {
|
||||
msg := "cms: verification failure"
|
||||
if e.Message != "" {
|
||||
msg += ": " + e.Message
|
||||
}
|
||||
if e.Detail != nil {
|
||||
msg += ": " + e.Detail.Error()
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
// Unwrap returns the internal error.
|
||||
func (e VerificationError) Unwrap() error {
|
||||
return e.Detail
|
||||
}
|
|
@ -1,216 +0,0 @@
|
|||
package cms
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/asn1"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/hashutil"
|
||||
"github.com/notaryproject/notation-go-lib/internal/crypto/oid"
|
||||
)
|
||||
|
||||
// ParsedSignedData is a parsed SignedData structure for golang friendly types.
|
||||
type ParsedSignedData struct {
|
||||
Content []byte
|
||||
ContentType asn1.ObjectIdentifier
|
||||
Certificates []*x509.Certificate
|
||||
CRLs []pkix.CertificateList
|
||||
Signers []SignerInfo
|
||||
}
|
||||
|
||||
// ParseSignedData parses ASN.1 DER-encoded SignedData structure to golang friendly types.
|
||||
func ParseSignedData(data []byte) (*ParsedSignedData, error) {
|
||||
var contentInfo ContentInfo
|
||||
if _, err := asn1.Unmarshal(data, &contentInfo); err != nil {
|
||||
return nil, SyntaxError{Message: "invalid content info", Detail: err}
|
||||
}
|
||||
if !oid.SignedData.Equal(contentInfo.ContentType) {
|
||||
return nil, ErrExpectSignedData
|
||||
}
|
||||
|
||||
var signedData SignedData
|
||||
if _, err := asn1.Unmarshal(contentInfo.Content.Bytes, &signedData); err != nil {
|
||||
return nil, SyntaxError{Message: "invalid signed data", Detail: err}
|
||||
}
|
||||
certs, err := x509.ParseCertificates(signedData.Certificates.Bytes)
|
||||
if err != nil {
|
||||
return nil, SyntaxError{Message: "invalid signed data", Detail: err}
|
||||
}
|
||||
|
||||
return &ParsedSignedData{
|
||||
Content: signedData.EncapsulatedContentInfo.Content,
|
||||
ContentType: signedData.EncapsulatedContentInfo.ContentType,
|
||||
Certificates: certs,
|
||||
CRLs: signedData.CRLs,
|
||||
Signers: signedData.SignerInfos,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Verify attempts to verify the content in the parsed signed data against the signer
|
||||
// information. The `Intermediates` in the verify options will be ignored and
|
||||
// re-contrusted using the certificates in the parsed signed data.
|
||||
// If more than one signature is present, the successful validation of any signature
|
||||
// implies that the content in the parsed signed data is valid.
|
||||
// On successful verification, the list of signers that successfully verify is returned.
|
||||
// If all signatures fail to verify, the last error is returned.
|
||||
// References:
|
||||
// - RFC 5652 5 Signed-data Content Type
|
||||
// - RFC 5652 5.4 Message Digest Calculation Process
|
||||
// - RFC 5652 5.6 Signature Verification Process
|
||||
// WARNING: this function doesn't do any revocation checking.
|
||||
func (d *ParsedSignedData) Verify(opts x509.VerifyOptions) ([]SignerInfo, error) {
|
||||
if len(d.Signers) == 0 {
|
||||
return nil, ErrSignerNotFound
|
||||
}
|
||||
if len(d.Certificates) == 0 {
|
||||
return nil, ErrCertificateNotFound
|
||||
}
|
||||
|
||||
intermediates := x509.NewCertPool()
|
||||
for _, cert := range d.Certificates {
|
||||
intermediates.AddCert(cert)
|
||||
}
|
||||
opts.Intermediates = intermediates
|
||||
var verifiedSigners []SignerInfo
|
||||
var lastErr error
|
||||
for _, signer := range d.Signers {
|
||||
if err := d.verify(signer, opts); err != nil {
|
||||
lastErr = err
|
||||
} else {
|
||||
verifiedSigners = append(verifiedSigners, signer)
|
||||
}
|
||||
}
|
||||
if len(verifiedSigners) == 0 {
|
||||
return nil, lastErr
|
||||
}
|
||||
return verifiedSigners, nil
|
||||
}
|
||||
|
||||
// verify verifies the trust in a top-down manner.
|
||||
// References:
|
||||
// - RFC 5652 5.4 Message Digest Calculation Process
|
||||
// - RFC 5652 5.6 Signature Verification Process
|
||||
func (d *ParsedSignedData) verify(signer SignerInfo, opts x509.VerifyOptions) error {
|
||||
// find signer certificate
|
||||
cert := d.getCertificate(signer.SignerIdentifier)
|
||||
if cert == nil {
|
||||
return ErrCertificateNotFound
|
||||
}
|
||||
|
||||
// verify signer certificate
|
||||
if _, err := cert.Verify(opts); err != nil {
|
||||
return VerificationError{Detail: err}
|
||||
}
|
||||
|
||||
// verify signature
|
||||
algorithm := getSignatureAlgorithmFromOID(
|
||||
signer.DigestAlgorithm.Algorithm,
|
||||
signer.SignatureAlgorithm.Algorithm,
|
||||
)
|
||||
if algorithm == x509.UnknownSignatureAlgorithm {
|
||||
return VerificationError{Message: "unknown signature algorithm"}
|
||||
}
|
||||
signed := d.Content
|
||||
if len(signer.SignedAttributes) > 0 {
|
||||
encoded, err := asn1.MarshalWithParams(signer.SignedAttributes, "set")
|
||||
if err != nil {
|
||||
return VerificationError{Message: "invalid signed attributes", Detail: err}
|
||||
}
|
||||
signed = encoded
|
||||
}
|
||||
if err := cert.CheckSignature(algorithm, signed, signer.Signature); err != nil {
|
||||
return VerificationError{Detail: err}
|
||||
}
|
||||
|
||||
// verify attributes if present
|
||||
if len(signer.SignedAttributes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var contentType asn1.ObjectIdentifier
|
||||
if err := signer.SignedAttributes.TryGet(oid.ContentType, &contentType); err != nil {
|
||||
return VerificationError{Message: "invalid content type", Detail: err}
|
||||
}
|
||||
if !d.ContentType.Equal(contentType) {
|
||||
return VerificationError{Message: "mismatch content type"}
|
||||
}
|
||||
|
||||
var expectedDigest []byte
|
||||
if err := signer.SignedAttributes.TryGet(oid.MessageDigest, &expectedDigest); err != nil {
|
||||
return VerificationError{Message: "invalid message digest", Detail: err}
|
||||
}
|
||||
hash, ok := oid.ConvertToHash(signer.DigestAlgorithm.Algorithm)
|
||||
if !ok {
|
||||
return VerificationError{Message: "unsupported digest algorithm"}
|
||||
}
|
||||
actualDigest, err := hashutil.ComputeHash(hash, d.Content)
|
||||
if err != nil {
|
||||
return VerificationError{Message: "hash failure", Detail: err}
|
||||
}
|
||||
if !bytes.Equal(expectedDigest, actualDigest) {
|
||||
return VerificationError{Message: "mismatch message digest"}
|
||||
}
|
||||
|
||||
// sanity check on signing time
|
||||
var signingTime time.Time
|
||||
if err := signer.SignedAttributes.TryGet(oid.SigningTime, &signingTime); err != nil {
|
||||
if err == ErrAttributeNotFound {
|
||||
return nil
|
||||
}
|
||||
return VerificationError{Message: "invalid signing time", Detail: err}
|
||||
}
|
||||
if signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter) {
|
||||
return VerificationError{Message: "signature signed when cert is inactive"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCertificate finds the certificate by issuer name and issuer-specific
|
||||
// serial number.
|
||||
// Reference: RFC 5652 5 Signed-data Content Type
|
||||
func (d *ParsedSignedData) getCertificate(ref IssuerAndSerialNumber) *x509.Certificate {
|
||||
for _, cert := range d.Certificates {
|
||||
if bytes.Equal(cert.RawIssuer, ref.Issuer.FullBytes) && cert.SerialNumber.Cmp(ref.SerialNumber) == 0 {
|
||||
return cert
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getSignatureAlgorithmFromOID converts ASN.1 digest and signature algorithm identifiers
|
||||
// to golang signature algorithms.
|
||||
func getSignatureAlgorithmFromOID(digestAlg, sigAlg asn1.ObjectIdentifier) x509.SignatureAlgorithm {
|
||||
switch {
|
||||
case oid.RSA.Equal(sigAlg):
|
||||
switch {
|
||||
case oid.SHA1.Equal(digestAlg):
|
||||
return x509.SHA1WithRSA
|
||||
case oid.SHA256.Equal(digestAlg):
|
||||
return x509.SHA256WithRSA
|
||||
case oid.SHA384.Equal(digestAlg):
|
||||
return x509.SHA384WithRSA
|
||||
case oid.SHA512.Equal(digestAlg):
|
||||
return x509.SHA512WithRSA
|
||||
}
|
||||
case oid.SHA1WithRSA.Equal(sigAlg):
|
||||
return x509.SHA1WithRSA
|
||||
case oid.SHA256WithRSA.Equal(sigAlg):
|
||||
return x509.SHA256WithRSA
|
||||
case oid.SHA384WithRSA.Equal(sigAlg):
|
||||
return x509.SHA384WithRSA
|
||||
case oid.SHA512WithRSA.Equal(sigAlg):
|
||||
return x509.SHA512WithRSA
|
||||
case oid.ECDSAWithSHA1.Equal(sigAlg):
|
||||
return x509.ECDSAWithSHA1
|
||||
case oid.ECDSAWithSHA256.Equal(sigAlg):
|
||||
return x509.ECDSAWithSHA256
|
||||
case oid.ECDSAWithSHA384.Equal(sigAlg):
|
||||
return x509.ECDSAWithSHA384
|
||||
case oid.ECDSAWithSHA512.Equal(sigAlg):
|
||||
return x509.ECDSAWithSHA512
|
||||
}
|
||||
return x509.UnknownSignatureAlgorithm
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package cms
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestVerifySignedData(t *testing.T) {
|
||||
// parse signed data
|
||||
sigBytes, err := os.ReadFile("testdata/TimeStampToken.p7s")
|
||||
if err != nil {
|
||||
t.Fatal("failed to read test signature:", err)
|
||||
}
|
||||
signed, err := ParseSignedData(sigBytes)
|
||||
if err != nil {
|
||||
t.Fatal("ParseSignedData() error =", err)
|
||||
}
|
||||
|
||||
// basic check on parsed signed data
|
||||
if got := len(signed.Certificates); got != 4 {
|
||||
t.Fatalf("len(Certificates) = %v, want %v", got, 4)
|
||||
}
|
||||
if got := len(signed.Signers); got != 1 {
|
||||
t.Fatalf("len(Signers) = %v, want %v", got, 1)
|
||||
}
|
||||
|
||||
// verify with no root CAs and should fail
|
||||
roots := x509.NewCertPool()
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping},
|
||||
CurrentTime: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
if _, err := signed.Verify(opts); err == nil {
|
||||
t.Errorf("ParseSignedData.Verify() error = %v, wantErr %v", err, true)
|
||||
} else if vErr, ok := err.(VerificationError); !ok {
|
||||
t.Errorf("ParseSignedData.Verify() error = %v, want VerificationError", err)
|
||||
} else if _, ok := vErr.Detail.(x509.UnknownAuthorityError); !ok {
|
||||
t.Errorf("ParseSignedData.Verify() VerificationError.Detail = %v, want UnknownAuthorityError", err)
|
||||
}
|
||||
|
||||
// verify with proper root CA
|
||||
rootCABytes, err := os.ReadFile("testdata/GlobalSignRootCA.crt")
|
||||
if err != nil {
|
||||
t.Fatal("failed to read root CA certificate:", err)
|
||||
}
|
||||
if ok := roots.AppendCertsFromPEM(rootCABytes); !ok {
|
||||
t.Fatal("failed to load root CA certificate")
|
||||
}
|
||||
verifiedSigners, err := signed.Verify(opts)
|
||||
if err != nil {
|
||||
t.Fatal("ParseSignedData.Verify() error =", err)
|
||||
}
|
||||
if !reflect.DeepEqual(verifiedSigners, signed.Signers) {
|
||||
t.Fatalf("ParseSignedData.Verify() = %v, want %v", verifiedSigners, signed.Signers)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyCorruptedSignedData(t *testing.T) {
|
||||
// parse signed data
|
||||
sigBytes, err := os.ReadFile("testdata/TimeStampToken.p7s")
|
||||
if err != nil {
|
||||
t.Fatal("failed to read test signature:", err)
|
||||
}
|
||||
signed, err := ParseSignedData(sigBytes)
|
||||
if err != nil {
|
||||
t.Fatal("ParseSignedData() error =", err)
|
||||
}
|
||||
|
||||
// corrupt the content
|
||||
signed.Content = []byte("corrupted data")
|
||||
|
||||
// verify with no root CAs and should fail
|
||||
roots := x509.NewCertPool()
|
||||
rootCABytes, err := os.ReadFile("testdata/GlobalSignRootCA.crt")
|
||||
if err != nil {
|
||||
t.Fatal("failed to read root CA certificate:", err)
|
||||
}
|
||||
if ok := roots.AppendCertsFromPEM(rootCABytes); !ok {
|
||||
t.Fatal("failed to load root CA certificate")
|
||||
}
|
||||
opts := x509.VerifyOptions{
|
||||
Roots: roots,
|
||||
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping},
|
||||
CurrentTime: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
if _, err := signed.Verify(opts); err == nil {
|
||||
t.Errorf("ParseSignedData.Verify() error = %v, wantErr %v", err, true)
|
||||
} else if _, ok := err.(VerificationError); !ok {
|
||||
t.Errorf("ParseSignedData.Verify() error = %v, want VerificationError", err)
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
// Package hashutil provides utilities for hash.
|
||||
package hashutil
|
||||
|
||||
import "crypto"
|
||||
|
||||
// ComputeHash computes the digest of the message with the given hash algorithm.
|
||||
// Callers should check the availability of the hash algorithm before invoking.
|
||||
func ComputeHash(hash crypto.Hash, message []byte) ([]byte, error) {
|
||||
h := hash.New()
|
||||
_, err := h.Write(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.Sum(nil), nil
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package oid
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"encoding/asn1"
|
||||
)
|
||||
|
||||
// ConvertToHash converts ASN.1 digest algorithm identifier to golang crypto hash
|
||||
// if it is available.
|
||||
func ConvertToHash(alg asn1.ObjectIdentifier) (crypto.Hash, bool) {
|
||||
var hash crypto.Hash
|
||||
switch {
|
||||
case SHA1.Equal(alg):
|
||||
hash = crypto.SHA1
|
||||
case SHA256.Equal(alg):
|
||||
hash = crypto.SHA256
|
||||
case SHA384.Equal(alg):
|
||||
hash = crypto.SHA384
|
||||
case SHA512.Equal(alg):
|
||||
hash = crypto.SHA512
|
||||
default:
|
||||
return hash, false
|
||||
}
|
||||
return hash, hash.Available()
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
// Package oid collects object identifiers for crypto algorithms.
|
||||
package oid
|
||||
|
||||
import "encoding/asn1"
|
||||
|
||||
// OIDs for hash algorithms
|
||||
var (
|
||||
// SHA1 (id-sha1) is defined in RFC 8017 B.1 Hash Functions
|
||||
SHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}
|
||||
|
||||
// SHA256 (id-sha256) is defined in RFC 8017 B.1 Hash Functions
|
||||
SHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}
|
||||
|
||||
// SHA384 (id-sha384) is defined in RFC 8017 B.1 Hash Functions
|
||||
SHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2}
|
||||
|
||||
// SHA512 (id-sha512) is defined in RFC 8017 B.1 Hash Functions
|
||||
SHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3}
|
||||
)
|
||||
|
||||
// OIDs for signature algorithms
|
||||
var (
|
||||
// RSA is defined in RFC 8017 C ASN.1 Module
|
||||
RSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
|
||||
|
||||
// SHA1WithRSA is defined in RFC 8017 C ASN.1 Module
|
||||
SHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5}
|
||||
|
||||
// SHA256WithRSA is defined in RFC 8017 C ASN.1 Module
|
||||
SHA256WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 11}
|
||||
|
||||
// SHA384WithRSA is defined in RFC 8017 C ASN.1 Module
|
||||
SHA384WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 12}
|
||||
|
||||
// SHA512WithRSA is defined in RFC 8017 C ASN.1 Module
|
||||
SHA512WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 13}
|
||||
|
||||
// ECDSAWithSHA1 is defined in ANSI X9.62
|
||||
ECDSAWithSHA1 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 1}
|
||||
|
||||
// ECDSAWithSHA256 is defined in RFC 5758 3.2 ECDSA Signature Algorithm
|
||||
ECDSAWithSHA256 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 2}
|
||||
|
||||
// ECDSAWithSHA384 is defined in RFC 5758 3.2 ECDSA Signature Algorithm
|
||||
ECDSAWithSHA384 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 3}
|
||||
|
||||
// ECDSAWithSHA512 is defined in RFC 5758 3.2 ECDSA Signature Algorithm
|
||||
ECDSAWithSHA512 = asn1.ObjectIdentifier{1, 2, 840, 10045, 4, 3, 4}
|
||||
)
|
||||
|
||||
// OIDs defined in RFC 5652 Cryptographic Message Syntax (CMS)
|
||||
var (
|
||||
// SignedData (id-signedData) is defined in RFC 5652 5.1 SignedData Type
|
||||
SignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
|
||||
|
||||
// ContentType (id-ct-contentType) is defined in RFC 5652 3 General Syntax
|
||||
ContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3}
|
||||
|
||||
// MessageDigest (id-messageDigest) is defined in RFC 5652 11.2 Message Digest
|
||||
MessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4}
|
||||
|
||||
// SigningTime (id-signingTime) is defined in RFC 5652 11.3 Signing Time
|
||||
SigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5}
|
||||
)
|
||||
|
||||
// TSTInfo (id-ct-TSTInfo) is defined in RFC 3161 2.4.2 Response Format
|
||||
var TSTInfo = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4}
|
|
@ -1,50 +0,0 @@
|
|||
// Package pki contains certificate management protocol structures
|
||||
// defined in RFC 2510.
|
||||
package pki
|
||||
|
||||
import "encoding/asn1"
|
||||
|
||||
// PKIStatus is defined in RFC 2510 3.2.3.
|
||||
const (
|
||||
StatusGranted = 0 // you got exactly what you asked for
|
||||
StatusGrantedWithMods = 1 // you got something like what you asked for
|
||||
StatusRejection = 2 // you don't get it, more information elsewhere in the message
|
||||
StatusWaiting = 3 // the request body part has not yet been processed, expect to hear more later
|
||||
StatusRevocationWarning = 4 // this message contains a warning that a revocation is imminent
|
||||
StatusRevocationNotification = 5 // notification that a revocation has occurred
|
||||
StatusKeyUpdateWarning = 6 // update already done for the oldCertId specified in the key update request message
|
||||
)
|
||||
|
||||
// PKIFailureInfo is defined in RFC 2510 3.2.3 and RFC 3161 2.4.2.
|
||||
const (
|
||||
FailureInfoBadAlg = 0 // unrecognized or unsupported Algorithm Identifier
|
||||
FailureInfoBadMessageCheck = 1 // integrity check failed (e.g., signature did not verify)
|
||||
FailureInfoBadRequest = 2 // transaction not permitted or supported
|
||||
FailureInfoBadTime = 3 // messageTime was not sufficiently close to the system time, as defined by local policy
|
||||
FailureInfoBadCertID = 4 // no certificate could be found matching the provided criteria
|
||||
FailureInfoBadDataFormat = 5 // the data submitted has the wrong format
|
||||
FailureInfoWrongAuthority = 6 // the authority indicated in the request is different from the one creating the response token
|
||||
FailureInfoIncorrectData = 7 // the requester's data is incorrect (used for notary services)
|
||||
FailureInfoMissingTimeStamp = 8 // the timestamp is missing but should be there (by policy)
|
||||
FailureInfoBadPOP = 9 // the proof-of-possession failed
|
||||
FailureInfoTimeNotAvailable = 14 // the TSA's time source is not available
|
||||
FailureInfoUnacceptedPolicy = 15 // the requested TSA policy is not supported by the TSA.
|
||||
FailureInfoUnacceptedExtension = 16 // the requested extension is not supported by the TSA.
|
||||
FailureInfoAddInfoNotAvailable = 17 // the additional information requested could not be understood or is not available
|
||||
FailureInfoSystemFailure = 25 // the request cannot be handled due to system failure
|
||||
)
|
||||
|
||||
// StatusInfo contains status codes and failure information for PKI messages.
|
||||
// PKIStatusInfo ::= SEQUENCE {
|
||||
// status PKIStatus,
|
||||
// statusString PKIFreeText OPTIONAL,
|
||||
// failInfo PKIFailureInfo OPTIONAL }
|
||||
// PKIStatus ::= INTEGER
|
||||
// PKIFreeText ::= SEQUENCE SIZE (1..MAX) OF UTF8String
|
||||
// PKIFailureInfo ::= BIT STRING
|
||||
// Reference: RFC 2510 3.2.3 Status codes and Failure Information for PKI messages.
|
||||
type StatusInfo struct {
|
||||
Status int
|
||||
StatusString []string `asn1:"optional,utf8"`
|
||||
FailInfo asn1.BitString `asn1:"optional"`
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
// Package asn1 decodes BER-encoded ASN.1 data structures and encodes in DER.
|
||||
// Note: DER is a subset of BER.
|
||||
// Reference: http://luca.ntop.org/Teaching/Appunti/asn1.html
|
||||
package asn1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/asn1"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var (
|
||||
ErrEarlyEOF = asn1.SyntaxError{Msg: "early EOF"}
|
||||
ErrExpectConstructed = asn1.SyntaxError{Msg: "constructed value expected"}
|
||||
ErrExpectPrimitive = asn1.SyntaxError{Msg: "primitive value expected"}
|
||||
ErrUnsupportedLength = asn1.StructuralError{Msg: "length method not supported"}
|
||||
)
|
||||
|
||||
// Value represents an ASN.1 value.
|
||||
type Value interface {
|
||||
// Encode encodes the value to the value writer in DER.
|
||||
Encode(ValueWriter) error
|
||||
|
||||
// EncodedLen returns the length in bytes of the encoded data.
|
||||
EncodedLen() int
|
||||
}
|
||||
|
||||
// Decode decodes BER-encoded ASN.1 data structures.
|
||||
func Decode(r ValueReader) (Value, error) {
|
||||
peekIdentifier, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err = r.UnreadByte(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isPrimitive(peekIdentifier) {
|
||||
return DecodePrimitive(r)
|
||||
}
|
||||
return DecodeConstructed(r)
|
||||
}
|
||||
|
||||
// ConvertToDER converts BER-encoded ASN.1 data structures to DER-encoded.
|
||||
func ConvertToDER(ber []byte) ([]byte, error) {
|
||||
v, err := Decode(bytes.NewReader(ber))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf := bytes.NewBuffer(make([]byte, 0, v.EncodedLen()))
|
||||
if err = v.Encode(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package asn1
|
||||
|
||||
import (
|
||||
"encoding/asn1"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConvertToDER(t *testing.T) {
|
||||
type data struct {
|
||||
Type asn1.ObjectIdentifier
|
||||
Value []byte
|
||||
}
|
||||
|
||||
want := data{
|
||||
Type: asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1},
|
||||
Value: []byte{
|
||||
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
|
||||
0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
|
||||
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
|
||||
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
|
||||
},
|
||||
}
|
||||
|
||||
ber := []byte{
|
||||
// Constructed value
|
||||
0x30,
|
||||
// Constructed value length
|
||||
0x2e,
|
||||
|
||||
// Type identifier
|
||||
0x06,
|
||||
// Type length
|
||||
0x09,
|
||||
// Type content
|
||||
0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
|
||||
|
||||
// Value identifier
|
||||
0x04,
|
||||
// Value length in BER
|
||||
0x81, 0x20,
|
||||
// Value content
|
||||
0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14,
|
||||
0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24,
|
||||
0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c,
|
||||
0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55,
|
||||
}
|
||||
|
||||
der, err := ConvertToDER(ber)
|
||||
if err != nil {
|
||||
t.Errorf("ConvertToDER() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var got data
|
||||
rest, err := asn1.Unmarshal(der, &got)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to decode converted data: %v", err)
|
||||
return
|
||||
}
|
||||
if len(rest) > 0 {
|
||||
t.Errorf("Unexpected rest data: %v", rest)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got = %v, want %v", got, want)
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
package asn1
|
||||
|
||||
import "io"
|
||||
|
||||
// isPrimitive checks the primitive flag in the identifier.
|
||||
// Returns true if the value is primitive.
|
||||
func isPrimitive(identifier byte) bool {
|
||||
return identifier&0x20 == 0
|
||||
}
|
||||
|
||||
// encodedLengthSize gives the number of octets used for encoding the length.
|
||||
func encodedLengthSize(length int) int {
|
||||
if length < 0x80 {
|
||||
return 1
|
||||
}
|
||||
|
||||
lengthSize := 1
|
||||
for ; length > 0; lengthSize++ {
|
||||
length >>= 8
|
||||
}
|
||||
return lengthSize
|
||||
}
|
||||
|
||||
// encodeLength encodes length octets in DER.
|
||||
func encodeLength(w io.ByteWriter, length int) error {
|
||||
// DER restriction: short form must be used for length less than 128
|
||||
if length < 0x80 {
|
||||
return w.WriteByte(byte(length))
|
||||
}
|
||||
|
||||
// DER restriction: long form must be encoded in the minimum number of octets
|
||||
lengthSize := encodedLengthSize(length)
|
||||
err := w.WriteByte(0x80 | byte(lengthSize-1))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := lengthSize - 1; i > 0; i-- {
|
||||
if err = w.WriteByte(byte(length >> (8 * (i - 1)))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decodeIdentifier decodes identifier octets.
|
||||
func decodeIdentifier(r io.ByteReader) ([]byte, error) {
|
||||
b, err := r.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// low-tag-number form
|
||||
identifier := []byte{b}
|
||||
|
||||
// high-tag-number form
|
||||
if b&0x1f == 0x1f {
|
||||
for {
|
||||
b, err = r.ReadByte()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, ErrEarlyEOF
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
identifier = append(identifier, b)
|
||||
if b&0x80 != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return identifier, nil
|
||||
}
|
||||
|
||||
// decodeLength decodes length octets.
|
||||
// Indefinite length is not supported
|
||||
func decodeLength(r io.ByteReader) (int, error) {
|
||||
b, err := r.ReadByte()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return 0, ErrEarlyEOF
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
switch {
|
||||
case b < 0x80:
|
||||
// short form
|
||||
return int(b), nil
|
||||
case b == 0x80:
|
||||
// Indefinite-length method is not supported.
|
||||
return 0, ErrUnsupportedLength
|
||||
}
|
||||
|
||||
// long form
|
||||
n := int(b & 0x7f)
|
||||
if n > 4 {
|
||||
// length must fit the memory space of the int type.
|
||||
return 0, ErrUnsupportedLength
|
||||
}
|
||||
var length int
|
||||
for i := 0; i < n; i++ {
|
||||
b, err = r.ReadByte()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return 0, ErrEarlyEOF
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
length = (length << 8) | int(b)
|
||||
}
|
||||
if length < 0 {
|
||||
// double check in case that length is over 31 bits.
|
||||
return 0, ErrUnsupportedLength
|
||||
}
|
||||
return length, nil
|
||||
}
|
|
@ -1,172 +0,0 @@
|
|||
package asn1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_encodeLength(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
length int
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "zero length",
|
||||
length: 0,
|
||||
want: []byte{0x00},
|
||||
},
|
||||
{
|
||||
name: "short form",
|
||||
length: 42,
|
||||
want: []byte{0x2a},
|
||||
},
|
||||
{
|
||||
name: "short form in max",
|
||||
length: 127,
|
||||
want: []byte{0x7f},
|
||||
},
|
||||
{
|
||||
name: "long form in min",
|
||||
length: 128,
|
||||
want: []byte{0x81, 0x80},
|
||||
},
|
||||
{
|
||||
name: "long form",
|
||||
length: 1234567890,
|
||||
want: []byte{0x84, 0x49, 0x96, 0x02, 0xd2},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
if err := encodeLength(buf, tt.length); (err != nil) != tt.wantErr {
|
||||
t.Errorf("encodeLength() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got := buf.Bytes(); !bytes.Equal(got, tt.want) {
|
||||
t.Errorf("encoded length = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_decodeIdentifier(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
encoded []byte
|
||||
want []byte
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty identifier",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "low-tag-number form",
|
||||
encoded: []byte{0x0b},
|
||||
want: []byte{0x0b},
|
||||
},
|
||||
{
|
||||
name: "no extra read in low-tag-number form",
|
||||
encoded: []byte{0x0b, 0x42},
|
||||
want: []byte{0x0b},
|
||||
},
|
||||
{
|
||||
name: "high-tag-number form",
|
||||
encoded: []byte{0x1f, 0x17, 0xdf},
|
||||
want: []byte{0x1f, 0x17, 0xdf}, // tag: 0x012345
|
||||
},
|
||||
{
|
||||
name: "no extra read in high-tag-number form",
|
||||
encoded: []byte{0x1f, 0x17, 0xdf, 0x42},
|
||||
want: []byte{0x1f, 0x17, 0xdf}, // tag: 0x012345
|
||||
},
|
||||
{
|
||||
name: "high-tag-number form (no termination)",
|
||||
encoded: []byte{0x1f, 0x17, 0x5f},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "high-tag-number form (EOF)",
|
||||
encoded: []byte{0x1f, 0x17},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := bytes.NewReader(tt.encoded)
|
||||
got, err := decodeIdentifier(r)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("decodeIdentifier() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(got, tt.want) {
|
||||
t.Errorf("decodeIdentifier() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_decodeLength(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
encoded []byte
|
||||
want int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty length",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "short form",
|
||||
encoded: []byte{0x2a},
|
||||
want: 42,
|
||||
},
|
||||
{
|
||||
name: "no extra read in short form",
|
||||
encoded: []byte{0x2a, 0x42},
|
||||
want: 42,
|
||||
},
|
||||
{
|
||||
name: "long form",
|
||||
encoded: []byte{0x84, 0x49, 0x96, 0x02, 0xd2},
|
||||
want: 1234567890,
|
||||
},
|
||||
{
|
||||
name: "long form in BER",
|
||||
encoded: []byte{0x81, 0x2a},
|
||||
want: 42,
|
||||
},
|
||||
{
|
||||
name: "no extra read in long form",
|
||||
encoded: []byte{0x84, 0x49, 0x96, 0x02, 0xd2, 0x42},
|
||||
want: 1234567890,
|
||||
},
|
||||
{
|
||||
name: "long form (indefinite)",
|
||||
encoded: []byte{0x80, 0x42, 0x00, 0x00},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "long form (EOF)",
|
||||
encoded: []byte{0x84, 0x49, 0x96, 0x02},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := bytes.NewReader(tt.encoded)
|
||||
got, err := decodeLength(r)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("decodeLength() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("decodeLength() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package asn1
|
||||
|
||||
import "io"
|
||||
|
||||
// ConstructedValue represents a value in constructed encoding.
|
||||
type ConstructedValue struct {
|
||||
identifier []byte
|
||||
length int
|
||||
members []Value
|
||||
}
|
||||
|
||||
// Encode encodes the constructed value to the value writer in DER.
|
||||
func (v ConstructedValue) Encode(w ValueWriter) error {
|
||||
_, err := w.Write(v.identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = encodeLength(w, v.length); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, value := range v.members {
|
||||
if err = value.Encode(w); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodedLen returns the length in bytes of the encoded data.
|
||||
func (v ConstructedValue) EncodedLen() int {
|
||||
return len(v.identifier) + encodedLengthSize(v.length) + v.length
|
||||
}
|
||||
|
||||
// DecodeConstructed decodes a constructed value in BER.
|
||||
func DecodeConstructed(r ValueReader) (Value, error) {
|
||||
identifier, err := decodeIdentifier(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isPrimitive(identifier[0]) {
|
||||
return nil, ErrExpectConstructed
|
||||
}
|
||||
expectedLength, err := decodeLength(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var members []Value
|
||||
encodedLength := 0
|
||||
r = LimitValueReader(r, int64(expectedLength))
|
||||
for {
|
||||
value, err := Decode(r)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
members = append(members, value)
|
||||
encodedLength += value.EncodedLen()
|
||||
}
|
||||
|
||||
return ConstructedValue{
|
||||
identifier: identifier,
|
||||
length: encodedLength,
|
||||
members: members,
|
||||
}, nil
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
package asn1
|
||||
|
||||
import "io"
|
||||
|
||||
// ValueReader is the interface for reading a value.
|
||||
type ValueReader interface {
|
||||
io.Reader
|
||||
io.ByteScanner
|
||||
}
|
||||
|
||||
// ValueWriter is the interface for writing a value.
|
||||
type ValueWriter interface {
|
||||
io.Writer
|
||||
io.ByteWriter
|
||||
}
|
||||
|
||||
// limitedValueReader limits the amount of data returned.
|
||||
type limitedValueReader struct {
|
||||
io.LimitedReader
|
||||
S io.ByteScanner
|
||||
}
|
||||
|
||||
// LimitValueReader returns a ValueReader, which limits the amount of data returned.
|
||||
func LimitValueReader(r ValueReader, n int64) ValueReader {
|
||||
return &limitedValueReader{
|
||||
LimitedReader: io.LimitedReader{
|
||||
R: r,
|
||||
N: n,
|
||||
},
|
||||
S: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *limitedValueReader) ReadByte() (c byte, err error) {
|
||||
if l.N <= 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
c, err = l.S.ReadByte()
|
||||
if err == nil {
|
||||
l.N--
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (l *limitedValueReader) UnreadByte() (err error) {
|
||||
err = l.S.UnreadByte()
|
||||
if err == nil {
|
||||
l.N++
|
||||
}
|
||||
return
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
package asn1
|
||||
|
||||
import "io"
|
||||
|
||||
// PrimitiveValue represents a value in primitive encoding.
|
||||
type PrimitiveValue struct {
|
||||
identifier []byte
|
||||
content []byte
|
||||
}
|
||||
|
||||
// Encode encodes the primitive value to the value writer in DER.
|
||||
func (v PrimitiveValue) Encode(w ValueWriter) error {
|
||||
_, err := w.Write(v.identifier)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = encodeLength(w, len(v.content)); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(v.content)
|
||||
return err
|
||||
}
|
||||
|
||||
// EncodedLen returns the length in bytes of the encoded data.
|
||||
func (v PrimitiveValue) EncodedLen() int {
|
||||
return len(v.identifier) + encodedLengthSize(len(v.content)) + len(v.content)
|
||||
}
|
||||
|
||||
// DecodePrimitive decodes a primitive value in BER.
|
||||
func DecodePrimitive(r ValueReader) (Value, error) {
|
||||
identifier, err := decodeIdentifier(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isPrimitive(identifier[0]) {
|
||||
return nil, ErrExpectPrimitive
|
||||
}
|
||||
length, err := decodeLength(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content := make([]byte, length)
|
||||
_, err = io.ReadFull(r, content)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return nil, ErrEarlyEOF
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return PrimitiveValue{
|
||||
identifier: identifier,
|
||||
content: content,
|
||||
}, nil
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package envelope
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// MediaTypePayloadV1 is the supported content type for signature's payload.
|
||||
const (
|
||||
MediaTypePayloadV1 = "application/vnd.cncf.notary.payload.v1+json"
|
||||
AnnotationX509ChainThumbprint = "io.cncf.notary.x509chain.thumbprint#S256"
|
||||
)
|
||||
|
||||
// Payload describes the content that gets signed.
|
||||
type Payload struct {
|
||||
TargetArtifact ocispec.Descriptor `json:"targetArtifact"`
|
||||
}
|
||||
|
||||
// ValidatePayloadContentType validates signature payload's content type.
|
||||
func ValidatePayloadContentType(payload *signature.Payload) error {
|
||||
switch payload.ContentType {
|
||||
case MediaTypePayloadV1:
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("payload content type %q not supported", payload.ContentType)
|
||||
}
|
||||
}
|
||||
|
||||
// SanitizeTargetArtifact filters out unrelated ocispec.Descriptor fields based
|
||||
// on notation spec (https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#payload).
|
||||
func SanitizeTargetArtifact(targetArtifact ocispec.Descriptor) ocispec.Descriptor {
|
||||
return ocispec.Descriptor{
|
||||
MediaType: targetArtifact.MediaType,
|
||||
Digest: targetArtifact.Digest,
|
||||
Size: targetArtifact.Size,
|
||||
Annotations: targetArtifact.Annotations,
|
||||
}
|
||||
}
|
||||
|
||||
// SigningTime returns the signing time of a signature envelope blob
|
||||
func SigningTime(signerInfo *signature.SignerInfo) (time.Time, error) {
|
||||
// sanity check
|
||||
if signerInfo == nil {
|
||||
return time.Time{}, errors.New("failed to generate annotations: signerInfo cannot be nil")
|
||||
}
|
||||
signingTime := signerInfo.SignedAttributes.SigningTime
|
||||
if signingTime.IsZero() {
|
||||
return time.Time{}, errors.New("signing time is missing")
|
||||
}
|
||||
return signingTime.UTC(), nil
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package envelope
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-core-go/signature/cose"
|
||||
"github.com/notaryproject/notation-core-go/signature/jws"
|
||||
gcose "github.com/veraison/go-cose"
|
||||
)
|
||||
|
||||
var (
|
||||
validCoseSignatureEnvelope []byte
|
||||
)
|
||||
|
||||
func init() {
|
||||
msg := gcose.Sign1Message{
|
||||
Headers: gcose.NewSign1Message().Headers,
|
||||
Payload: []byte("valid"),
|
||||
Signature: []byte("valid"),
|
||||
}
|
||||
validCoseSignatureEnvelope, _ = msg.MarshalCBOR()
|
||||
}
|
||||
|
||||
const invalidMediaType = "invalid"
|
||||
|
||||
func checkErrorEqual(expected, got error) bool {
|
||||
if expected == nil && got == nil {
|
||||
return true
|
||||
}
|
||||
if expected != nil && got != nil {
|
||||
return expected.Error() == got.Error()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestValidateEnvelopeMediaType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mediaType string
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "jws signature media type",
|
||||
mediaType: jws.MediaTypeEnvelope,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "cose signature media type",
|
||||
mediaType: cose.MediaTypeEnvelope,
|
||||
expectedErr: nil,
|
||||
},
|
||||
{
|
||||
name: "invalid media type",
|
||||
mediaType: invalidMediaType,
|
||||
expectedErr: errors.New("invalid envelope media type"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := validateEnvelopeMediaType(tt.mediaType); !checkErrorEqual(tt.expectedErr, err) {
|
||||
t.Fatalf("expected validate envelope media type err: %v, got: %v", tt.expectedErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePayloadContentType(t *testing.T) {
|
||||
payload := &signature.Payload{
|
||||
ContentType: MediaTypePayloadV1,
|
||||
}
|
||||
err := ValidatePayloadContentType(payload)
|
||||
if !isErrEqual(nil, err) {
|
||||
t.Fatalf("ValidatePayloadContentType() expects error: %v, but got: %v.", nil, err)
|
||||
}
|
||||
|
||||
payload = &signature.Payload{
|
||||
ContentType: "invalid",
|
||||
}
|
||||
err = ValidatePayloadContentType(payload)
|
||||
expect := errors.New("payload content type \"invalid\" not supported")
|
||||
if !isErrEqual(expect, err) {
|
||||
t.Fatalf("ValidatePayloadContentType() expects error: %v, but got: %v.", expect, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigningTime(t *testing.T) {
|
||||
testTime, err := time.Parse(time.RFC3339, "2023-03-14T04:45:22Z")
|
||||
if err != nil {
|
||||
t.Fatal("failed to generate time")
|
||||
}
|
||||
signerInfo := signature.SignerInfo{
|
||||
SignedAttributes: signature.SignedAttributes{
|
||||
SigningTime: testTime,
|
||||
},
|
||||
}
|
||||
signingTime, err := SigningTime(&signerInfo)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get signing time from signature: %v", err)
|
||||
}
|
||||
expectedSigningTime := "2023-03-14T04:45:22Z"
|
||||
if signingTime.Format(time.RFC3339) != expectedSigningTime {
|
||||
t.Fatalf("expected signing time: %q, got: %q", expectedSigningTime, signingTime.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
|
||||
func isErrEqual(wanted, got error) bool {
|
||||
if wanted == nil && got == nil {
|
||||
return true
|
||||
}
|
||||
if wanted != nil && got != nil {
|
||||
return wanted.Error() == got.Error()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// validateEnvelopeMediaType validetes envelope media type is supported by
|
||||
// notation-core-go.
|
||||
func validateEnvelopeMediaType(mediaType string) error {
|
||||
for _, types := range signature.RegisteredEnvelopeTypes() {
|
||||
if mediaType == types {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("invalid envelope media type")
|
||||
}
|
|
@ -0,0 +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 (
|
||||
"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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mockfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/notaryproject/notation-go/dir"
|
||||
)
|
||||
|
||||
type sysFSMock struct {
|
||||
fs.FS
|
||||
root string
|
||||
}
|
||||
|
||||
// SysPath returns the system path of the FS.
|
||||
func (s sysFSMock) SysPath(items ...string) (string, error) {
|
||||
pathItems := []string{s.root}
|
||||
pathItems = append(pathItems, items...)
|
||||
return filepath.Join(pathItems...), nil
|
||||
}
|
||||
|
||||
// NewSysFSMock returns a SysFS mock of the given FS.
|
||||
func NewSysFSMock(fsys fs.FS) dir.SysFS {
|
||||
return sysFSMock{
|
||||
FS: fsys,
|
||||
root: ""}
|
||||
}
|
||||
|
||||
// NewSysFSWithRootMock returns a SysFS mock of the given fs and
|
||||
// a root directory
|
||||
func NewSysFSWithRootMock(fsys fs.FS, root string) dir.SysFS {
|
||||
return sysFSMock{
|
||||
FS: fsys,
|
||||
root: root}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
// Copyright The Notary Project Authors.
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
|
||||
"github.com/notaryproject/notation-core-go/signature"
|
||||
"github.com/notaryproject/notation-plugin-framework-go/plugin"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
//go:embed testdata/ca_valid_sig_env.json
|
||||
var MockCaValidSigEnv []byte
|
||||
|
||||
//go:embed testdata/ca_invalid_sig_env.json
|
||||
var MockCaInvalidSigEnv []byte
|
||||
|
||||
//go:embed testdata/sa_valid_sig_env.json
|
||||
var MockSaValidSigEnv []byte
|
||||
|
||||
//go:embed testdata/ca_plugin_sig_env.json
|
||||
var MockCaPluginSigEnv []byte // extended attributes are "SomeKey":"SomeValue", "io.cncf.notary.verificationPlugin":"plugin-name"
|
||||
|
||||
//go:embed testdata/sa_invalid_sig_env.json
|
||||
var MockSaInvalidSigEnv []byte
|
||||
|
||||
//go:embed testdata/ca_expired_sig_env.json
|
||||
var MockCaExpiredSigEnv []byte
|
||||
|
||||
//go:embed testdata/sa_expired_sig_env.json
|
||||
var MockSaExpiredSigEnv []byte
|
||||
|
||||
//go:embed testdata/sa_plugin_sig_env.json
|
||||
var MockSaPluginSigEnv []byte // extended attributes are "SomeKey":"SomeValue", "io.cncf.notary.verificationPlugin":"plugin-name"
|
||||
|
||||
//go:embed testdata/sig_env_with_metadata.json
|
||||
var MockSigEnvWithMetadata []byte
|
||||
|
||||
//go:embed testdata/ca_incompatible_pluginver_sig_env_1.0.9.json
|
||||
var MockCaIncompatiblePluginVerSigEnv_1_0_9 []byte
|
||||
|
||||
//go:embed testdata/ca_incompatible_pluginver_sig_env_1.0.1.json
|
||||
var MockCaIncompatiblePluginVerSigEnv_1_0_1 []byte
|
||||
|
||||
//go:embed testdata/ca_incompatible_pluginver_sig_env_1.2.3.json
|
||||
var MockCaIncompatiblePluginVerSigEnv_1_2_3 []byte
|
||||
|
||||
//go:embed testdata/ca_incompatible_pluginver_sig_env_1.1.0-alpha.json
|
||||
var MockCaIncompatiblePluginVerSigEnv_1_1_0_alpha []byte
|
||||
|
||||
//go:embed testdata/ca_compatible_pluginver_sig_env_0.0.9.json
|
||||
var MockCaCompatiblePluginVerSigEnv_0_0_9 []byte
|
||||
|
||||
//go:embed testdata/ca_compatible_pluginver_sig_env_1.0.0-alpha.json
|
||||
var MockCaCompatiblePluginVerSigEnv_1_0_0_alpha []byte
|
||||
|
||||
//go:embed testdata/ca_compatible_pluginver_sig_env_1.0.0-alpha.beta.json
|
||||
var MockCaCompatiblePluginVerSigEnv_1_0_0_alpha_beta []byte
|
||||
|
||||
//go:embed testdata/ca_compatible_pluginver_sig_env_1.0.0.json
|
||||
var MockCaCompatiblePluginVerSigEnv_1_0_0 []byte
|
||||
|
||||
var (
|
||||
SampleArtifactUri = "registry.acme-rockets.io/software/net-monitor@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e"
|
||||
SampleDigest = digest.Digest("sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e")
|
||||
ZeroDigest = digest.Digest("sha256:0000000000000000000000000000000000000000000000000000000000000000")
|
||||
Annotations = map[string]string{"key": "value"}
|
||||
ImageDescriptor = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||
Digest: SampleDigest,
|
||||
Size: 528,
|
||||
Annotations: Annotations,
|
||||
}
|
||||
SigManfiestDescriptor = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: SampleDigest,
|
||||
Size: 300,
|
||||
Annotations: Annotations,
|
||||
}
|
||||
TestImageDescriptor = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||
Digest: digest.Digest("sha256:fe7e9333395060c2f5e63cf36a38fba10176f183b4163a5794e081a480abba5f"),
|
||||
Size: 942,
|
||||
Annotations: nil,
|
||||
}
|
||||
JwsSigEnvDescriptor = ocispec.Descriptor{
|
||||
MediaType: "application/jose+json",
|
||||
Digest: SampleDigest,
|
||||
Size: 100,
|
||||
Annotations: Annotations,
|
||||
}
|
||||
PluginExtendedCriticalAttribute = signature.Attribute{
|
||||
Key: "SomeKey",
|
||||
Critical: true,
|
||||
Value: "SomeValue",
|
||||
}
|
||||
MetadataSigEnvDescriptor = ocispec.Descriptor{
|
||||
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||
Digest: digest.Digest("sha256:5a07385af4e6b6af81b0ebfd435aedccdfa3507f0609c658209e1aba57159b2b"),
|
||||
Size: 942,
|
||||
Annotations: map[string]string{"io.wabbit-networks.buildId": "123", "io.wabbit-networks.buildTime": "1672944615"},
|
||||
}
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
ResolveResponse ocispec.Descriptor
|
||||
ResolveError error
|
||||
ListSignaturesResponse []ocispec.Descriptor
|
||||
ListSignaturesError error
|
||||
FetchSignatureBlobResponse []byte
|
||||
FetchSignatureBlobError error
|
||||
MissMatchDigest bool
|
||||
ExceededNumOfSignatures bool
|
||||
PushSignatureError error
|
||||
}
|
||||
|
||||
func NewRepository() Repository {
|
||||
return Repository{
|
||||
ResolveResponse: ImageDescriptor,
|
||||
ListSignaturesResponse: []ocispec.Descriptor{SigManfiestDescriptor},
|
||||
FetchSignatureBlobResponse: MockCaValidSigEnv,
|
||||
}
|
||||
}
|
||||
|
||||
func (t Repository) Resolve(ctx context.Context, reference string) (ocispec.Descriptor, error) {
|
||||
if t.MissMatchDigest {
|
||||
return ocispec.Descriptor{
|
||||
MediaType: "application/vnd.docker.distribution.manifest.v2+json",
|
||||
Digest: ZeroDigest,
|
||||
Size: 528,
|
||||
Annotations: Annotations,
|
||||
}, nil
|
||||
}
|
||||
return t.ResolveResponse, t.ResolveError
|
||||
}
|
||||
|
||||
func (t Repository) ListSignatures(ctx context.Context, desc ocispec.Descriptor, fn func(signatureManifests []ocispec.Descriptor) error) error {
|
||||
if t.ExceededNumOfSignatures {
|
||||
t.ListSignaturesResponse = []ocispec.Descriptor{SigManfiestDescriptor, SigManfiestDescriptor}
|
||||
}
|
||||
err := fn(t.ListSignaturesResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.ListSignaturesError
|
||||
}
|
||||
|
||||
func (t Repository) FetchSignatureBlob(ctx context.Context, desc ocispec.Descriptor) ([]byte, ocispec.Descriptor, error) {
|
||||
return t.FetchSignatureBlobResponse, JwsSigEnvDescriptor, t.FetchSignatureBlobError
|
||||
}
|
||||
|
||||
func (t Repository) PushSignature(ctx context.Context, mediaType string, blob []byte, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) {
|
||||
if t.PushSignatureError != nil {
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, t.PushSignatureError
|
||||
}
|
||||
|
||||
return ocispec.Descriptor{}, ocispec.Descriptor{}, nil
|
||||
}
|
||||
|
||||
type PluginMock struct {
|
||||
Metadata plugin.GetMetadataResponse
|
||||
ExecuteResponse interface{}
|
||||
ExecuteError error
|
||||
}
|
||||
|
||||
func (p *PluginMock) GetMetadata(ctx context.Context, req *plugin.GetMetadataRequest) (*plugin.GetMetadataResponse, error) {
|
||||
return &p.Metadata, nil
|
||||
}
|
||||
|
||||
func (p *PluginMock) VerifySignature(ctx context.Context, req *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) {
|
||||
if resp, ok := p.ExecuteResponse.(*plugin.VerifySignatureResponse); ok {
|
||||
return resp, nil
|
||||
}
|
||||
return nil, p.ExecuteError
|
||||
}
|
||||
|
||||
func (p *PluginMock) DescribeKey(ctx context.Context, req *plugin.DescribeKeyRequest) (*plugin.DescribeKeyResponse, error) {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (p *PluginMock) GenerateSignature(ctx context.Context, req *plugin.GenerateSignatureRequest) (*plugin.GenerateSignatureResponse, error) {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (p *PluginMock) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) {
|
||||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
type PluginManager struct {
|
||||
PluginCapabilities []plugin.Capability
|
||||
GetPluginError error
|
||||
PluginRunnerLoadError error
|
||||
PluginRunnerExecuteResponse interface{}
|
||||
PluginRunnerExecuteError error
|
||||
}
|
||||
|
||||
func (pm PluginManager) Get(ctx context.Context, name string) (plugin.Plugin, error) {
|
||||
return &PluginMock{
|
||||
Metadata: plugin.GetMetadataResponse{
|
||||
Name: "plugin-name",
|
||||
Description: "for mocking in unit tests",
|
||||
Version: "1.0.0",
|
||||
URL: ".",
|
||||
SupportedContractVersions: []string{"1.0"},
|
||||
Capabilities: pm.PluginCapabilities,
|
||||
},
|
||||
ExecuteResponse: pm.PluginRunnerExecuteResponse,
|
||||
ExecuteError: pm.PluginRunnerExecuteError,
|
||||
}, pm.GetPluginError
|
||||
}
|
||||
|
||||
func (pm PluginManager) List(ctx context.Context) ([]string, error) {
|
||||
panic("not implemented")
|
||||
}
|
|
@ -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 @@
|
|||
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQwMDoyMTozNi0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjAuMC45In0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"VXWolnwKhCSYn1x1_0CUpvUxEihiuKCJ9Ae2Lm--gkW_tfbBlTzkq4TciXM4u4V9MBtbDczQ8k748tmEb7qbzPPT6CEPGMBX8WN7kDStqXGILmpIE5M7Z1nYVIYkgQPk_w6FyC291bluQQGu0yqNrAO3pT1Ym5DoHAyRHLROdDRChntI4Qrz5DGrjBsiibo_GAOxw1jY1ENvo5dlSTAgnZm9jkfbY0gsYTXuNGYk2atS0H1W_MVRdgDSI9gbQ6amLUf-qy_gcbl5UT8Pa5fWb_1KZPtAqoh4hA5PW4UKkxFE0Wz2pUAs9RpYI-xpw1B6KGtgiI9MuTYDFMmTXHBRig"}
|
|
@ -0,0 +1 @@
|
|||
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMzowMzoyMy0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMC4wLWFscGhhLmJldGEifQ","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"iJthtqbz0O5nFuo5Z9nRddEjyZp3RG-KOY6SSB3sc8AgDBdT5Fjp9yltIoqTl-BLZhrGOAFeO0T_1JVsPbZZMxzJq4fb3gPaIPItrendkpit1m2RaB8fK1D_I6Vqu1_rGiYaxDcNpaqn1T_isxr4MVRekcLSNQnG3iMdJ0k-Attf8JdCXE0EWKyLBStMVAfo0J39ShFcwyIMvO0vm2_TRDVbcKovpY0vFrfyE2pFIChnJECmivImdKmBMIW78vEtN6qBrKskI3HzA9N1XjxGY4GOAu30iqtNRanO65nZGng0lqpJd15bAwUaqj-KD_BAZIUT9T2qCf2COF9GKvc3NQ"}
|
|
@ -0,0 +1 @@
|
|||
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMjo1NzowNC0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMC4wLWFscGhhIn0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"xZqE2HZye4qPmG688z875mHySGV_MoWOb99wChu-hInU8-CyxMesxzVCo_boG3Oae6tj6MKwdJ-Dj2cKbI3S4aX2l6t5IRFLB5z4DuIsDhmKZj9iN5LjtP8ua5_fni9dBk4e9c9TAsMq1hjXyNEen2rC1dzP_bcNYnoOs1yRWpO4JAcsslMYeqUIKKf39kzlOxOKIsJ8YhZoNeRc3HnAu4hlX2XpXwArovvMZtg1Akp6qCjVQcQQUTb_M0JeytmR8R5tdr_ZYqh-rCWbIe5tNU4u9jCP8xvlXPdSjpHgpmPsEnNd4u4gnLFxuYAq5l3UkdGDLXUsGrTx_Bi_LoFHUQ"}
|
|
@ -0,0 +1 @@
|
|||
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMzowNTo1OC0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMC4wIn0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"JG7Gk8HwJbkUheyX9eRoBDPezynCmMetYATNsW0U4ERBiagKO-DxRMN4lqHxcVFf7HXVRCWPf3A6aIYo6Vox0fHNFDWyX7g4qcD0wy8mSIgt9FsN5EBFqkgUxfC2o_5OrlUEsbaN8vU3tH4jNoTjWEcT6cNVNv7gltzkTQDQFdgl7DC-Bf12p9HJsSQQlJqdS-BhDYp-ou7dwgd3jeomureLC6kOhaU3ssmSsn69cdCt9cZgZ9U9-5knjyicGUDaCpPHWpz3_R8JgyLq3L8nzEetPBHRShwMPUwV42F_9_C2-gXR7ZVaU3ENshViL0p0T70U4VElOb7IxqAMWRmIlw"}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ",
|
||||
"protected": "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmV4cGlyeSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjAyMi0wNy0yOVQyMzo1OTowMFoiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nVGltZSI6IjIwMjItMDctMjlUMDA6MDA6MDBaIn0",
|
||||
"header": {
|
||||
"x5c": [
|
||||
"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"
|
||||
],
|
||||
"io.cncf.notary.SigningAgent": "Notation/1.0.0"
|
||||
},
|
||||
"signature": "RZtqCD4KGh5_CD8wjG69TJIzzB4Cr-cxQhKTvZJYsRVIJyl3s5D0215GhBrggomVk9-LGD2FdWd2VfuaLd4bmhW3rSV3ltmAext7DNQFg2xtMeYSeCL2U_ygN2j4bc80RDaX8w_zOTVOmuhW6i2jgwRjWXdDaJeYTbZA2syA5R38tYYewVcZJ6U057Wsflt5yPWJCdxZLuTago5CkbLASL8HHnmlUkDvKKB1Y9SNDOQ3AmGP4-XJykcX_MfPo5RGRvZE-zHUJOEKj3ryfC0UTUT7V1ISTagqOt7zOa1BEzgQ-1GQk1MbaPPZWkiOZX4RqMXMV3hVqtDuZxlpT25KzZPm1USwWwJkycv7YB69fc2aoHJAPo-39uEV9fdAz_03whnrQSpfJbmHHTXMJkWKrZ5ozU-8zlEttWyL5D85zAouSMVXWm22zMrDW-XxST9QoeV4b1_BedW1PwJDbeU6P1hhobnQh3jHmSueVl_WZ5_g8_iVepSmSBcR1e4WpoPi"
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMjo0OTowMi0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMC4xIn0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"tb2xdd03j1ATBoG1K-9QmCjNeTnK-LKLHdZS44NJ0G5MfrzSFv56w3_FDqnS1jki8FTmGVUMdPAOciTuyoP_nREMBMr9QYn-qOAHisVrvxAcqmWEL-4Uoa_VIzmPvq-_wJKw9L_oZ2m-b9dx93tl2t2z0gxQaAgtVWJP6ap47lKlri6IFeFIXDq6jpdC9sy3q_wifnxFaZ9LM3892Pp7aMLvnT_TdTPxT1AHSq6ZOvddPbStvSUVICXZLmsglFym2c8RzatxulrnGlZ1fKKS0gR7W96-L1JsqIV5KeBMXq8vFnG-rK4fsqa0FeBBkmOOV6ZKKIruvfm7Z-SJ-nJLZw"}
|
|
@ -0,0 +1 @@
|
|||
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQwMDo0NzoxOC0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMC45In0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"zdA6RgbS3-QUdrhJuhAz-4wi55PZjy0CezOyTpg0UP1zxRfigVefPne86GEhGmiC-m-QlJC6bWSYFdkF3EoBL1CpGo46zUeaGKhQXM0Db1I8VKhJE20o1T83yXm-_ZVgDEe3_LUhu_KYs-jvkfJu_DGl6DJdBp_lkEpc9Br3tYUvgkxtF2LlvSUNYuc4oILnidj2sYFO5o7IBKdDoBVlQ3Z29s2Z6NUzy48ab9mxZCq0T9-uGj8636GJ3yJ78086GI_lt-0_mXdJ592WguWb3WBogCz9NvLm-byPIC7cP4RpHRqJQRsvYp6txgsrDqy2T1I0BEsf-Fp1FSxBdWMwXA"}
|
|
@ -0,0 +1 @@
|
|||
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMjo0MDozNC0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMS4wLWFscGhhIn0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"In4X5VH7wiFuGRaop36jSxFMvgAmbnZ7Pwhl1iqhSFItCGpwCCq7Sb9fWn79fiRyxI9F6JuJSTnTtHnjmZXfShAe5KRlSUktPwRcGg6LAMG9YTvd1JayNdjAGyPvZw7PGqeKF_syNgSrw-UzLsR0YXqck639affiVlKRTMNeZla2iXb8gRa8LGGiGoizKMrwV3Ywf3QilWy4CR5NK9TUj-OmdpaBfmE3T--LDpaOt7fjzhCFMXDGq27I_7NfzhrIJ_LpS7f2R5dG6eVRIgmSOVKEkCVM0n38lJ0H1E2uwwYmhns5wzDWJeBVEem8ycFrQkEvsGHWJ1Ru9YYNXhfr9Q"}
|
|
@ -0,0 +1 @@
|
|||
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6ZmU3ZTkzMzMzOTUwNjBjMmY1ZTYzY2YzNmEzOGZiYTEwMTc2ZjE4M2I0MTYzYTU3OTRlMDgxYTQ4MGFiYmE1ZiIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo5NDJ9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbk1pblZlcnNpb24iLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMy0wMS0xOVQxMjo1NDowMS0wODowMCIsImlvLmNuY2Yubm90YXJ5LnZlcmlmaWNhdGlvblBsdWdpbiI6ImlvLmNuY2Yubm90YXJ5LnBsdWdpbi51bml0dGVzdC5tb2NrIiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luTWluVmVyc2lvbiI6IjEuMi4zIn0","header":{"x5c":["MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEbMBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMzMDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7hsGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYEs4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwKGvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMHPsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrWLYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDAe2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvjFE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/ioe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qozfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"sB7vQl3zpK1JBjKa0gwj_s0Rbboo2kb4x81MNIbUINAc2ocvfRqyxMtlJYdgmx78GledCm4j8BfXr7_sV0_WkKI6Af6n_5rYMQ0a3EOI79-uzkRqrKBJsh4BsuQuweBql-W5-ofnwAhpNUmowHUcJlh0PmpOeYPQcj0TFMCZuqwMSKi4KLj4H5ENnmIWyR4rDoNueZkenbfh-eYR47PDb8KHyGTX86m-8IY-gNlAYRm_62MRecsGjg97EQ5niFGaRxlg7jf-1RVg3jLKXYlLRs41dnsMo1QGuTW7nPEUZKpXMZIyPnqPmS91icq0v1Obcx-r4aOqTKrPTeIPCYMoSQ"}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ=",
|
||||
"protected": "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmV4cGlyeSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjEyMC0xMS0wOVQwNzowMDowMFoiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nVGltZSI6IjIwMjAtMTEtMDlUMDc6MDA6MDBaIn0",
|
||||
"header": {
|
||||
"x5c": [
|
||||
"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"
|
||||
],
|
||||
"io.cncf.notary.SigningAgent": "Notation/1.0.0"
|
||||
},
|
||||
"signature": "ZvsxyaSqDzS7mY_jKpnq2XtBcmyWmSE461BHL6q2pAx_-Rxr8Fvs2oIfZdSG2o3qugPDjzZDMhKdYdnrW1AIEkVIG_QUmeyGj28PVXxsC5NKpXwrPUMOzrXSFLHIvBNZ2q87wRYInsgCPtv5ZPv0IgA2sAW6y7NlVM2D0vJax55ITsJO5aEaEUlAdi_H7-TCD48DHuFpnJdNkVB_hZkwYfxuqIKU2C__Z2hLLHxaS2LzuzhqOnYlbqn4e225uZt9odXq3qmZ_44Vx3DYL_-ZuV0S9jEk7NW8-dO0T0MeQn6VXDyfT1rjc6IVPnLxAnELFyLn121GYulYC8V2D1_MLcv8sDHY23rHb3-R-WCLMDSfaIvReY89vQfxcfpdCRC0F3N2CcnrgsrUC6Fplm5Uy45Gn9--b7x5cdSzOzQsefCH1GpixW7YyNs1xZQ17WqdYyWD2EBrB5vqVFzkzDYnQ4H-p9G3AzM4HTrjWqHX-0cYHlpmTS4AjVxn0UV80Jn9"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ",
|
||||
"protected": "eyJTb21lS2V5IjoiU29tZVZhbHVlIiwiYWxnIjoiUFMzODQiLCJjcml0IjpbImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiLCJTb21lS2V5IiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IiwiaW8uY25jZi5ub3RhcnkudmVyaWZpY2F0aW9uUGx1Z2luIl0sImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS5wYXlsb2FkLnYxK2pzb24iLCJpby5jbmNmLm5vdGFyeS5leHBpcnkiOiIyMTIwLTExLTA5VDA3OjAwOjAwWiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMC0xMS0wOVQwNzowMDowMFoiLCJpby5jbmNmLm5vdGFyeS52ZXJpZmljYXRpb25QbHVnaW4iOiJwbHVnaW4tbmFtZSJ9",
|
||||
"header": {
|
||||
"x5c": [
|
||||
"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"
|
||||
],
|
||||
"io.cncf.notary.SigningAgent": "Notation/1.0.0"
|
||||
},
|
||||
"signature": "cyB34qtMss9N1E_2XAQ_71c6j1fOcamenm7YrYsXn562XOhFgJKUjmDYWkz9mmdLN-GqQNKA8MhAfKt2ipXxsWldrb3a-6AZ-y4jIkY5XIY_s7Sndz58DPtez0X4kAehvKiyUtDVPbqIJQ5Hwgj8tC_f0Yva6pdrSD7xwenxwiCZmxM6N_LV9d1oYSDQi9890XRrFK4M1YRlOZquJ19HrhADLVJXS-ZfqcTE_tceoU2Hq82pqd2MnazAtJiWZm0cxwt-OsGlgGrkvHoNcMYS8K6BSBvL-vVtOuSpca89QrLsTCnKnmvUlw3wrWTDf83qhPyfw-2ASrE2V57vunpxSNyoA_70fNgOuhWUZZUTi9eXxutp0GCcGTem7MzZRBJVOVdw9OgR3pClGiRxP3BE2Atn3EUXs2HgQHEiE1KZvVHFeObB6asMqfbAMMNDgZCsZi7Yah7NaYg1NH9YwrJgAtNFW0p2trxiQ6uqICD2m54yGtRmvw_O9kt5HnUaBQJX"
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"payload": "eyJ0YXJnZXRBcnRpZmFjdCI6eyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiZGlnZXN0Ijoic2hhMjU2OjYwMDQzY2Y0NWVhZWJjNGMwODY3ZmVhNDg1YTAzOWI1OThmNTJmZDA5ZmQ1YjA3YjBiMmQyZjg4ZmFkOWQ3NGUiLCJzaXplIjo1Mjh9fQ",
|
||||
"protected": "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsImlvLmNuY2Yubm90YXJ5LmV4cGlyeSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5IjoiMjEyMC0xMS0wOVQwNzowMDowMFoiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nU2NoZW1lIjoibm90YXJ5Lng1MDkiLCJpby5jbmNmLm5vdGFyeS5zaWduaW5nVGltZSI6IjIwMjAtMTEtMDlUMDc6MDA6MDBaIn0",
|
||||
"header": {
|
||||
"x5c": [
|
||||
"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"
|
||||
],
|
||||
"io.cncf.notary.SigningAgent": "Notation/1.0.0"
|
||||
},
|
||||
"signature": "ZvsxyaSqDzS7mY_jKpnq2XtBcmyWmSE461BHL6q2pAx_-Rxr8Fvs2oIfZdSG2o3qugPDjzZDMhKdYdnrW1AIEkVIG_QUmeyGj28PVXxsC5NKpXwrPUMOzrXSFLHIvBNZ2q87wRYInsgCPtv5ZPv0IgA2sAW6y7NlVM2D0vJax55ITsJO5aEaEUlAdi_H7-TCD48DHuFpnJdNkVB_hZkwYfxuqIKU2C__Z2hLLHxaS2LzuzhqOnYlbqn4e225uZt9odXq3qmZ_44Vx3DYL_-ZuV0S9jEk7NW8-dO0T0MeQn6VXDyfT1rjc6IVPnLxAnELFyLn121GYulYC8V2D1_MLcv8sDHY23rHb3-R-WCLMDSfaIvReY89vQfxcfpdCRC0F3N2CcnrgsrUC6Fplm5Uy45Gn9--b7x5cdSzOzQsefCH1GpixW7YyNs1xZQ17WqdYyWD2EBrB5vqVFzkzDYnQ4H-p9G3AzM4HTrjWqHX-0cYHlpmTS4AjVxn0UV80Jn9"
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue