Compare commits

...

76 Commits
v1.1.2 ... main

Author SHA1 Message Date
Junjie Gao 6063ebe30f
chore: update maintainer list: Junjie Gao retired (#551)
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2025-06-30 17:46:34 +08:00
dependabot[bot] f457e85917
build(deps): bump golang.org/x/mod from 0.24.0 to 0.25.0 (#550) 2025-06-20 07:15:57 +00:00
dependabot[bot] 8c17f1cfc2
build(deps): bump golang.org/x/crypto from 0.38.0 to 0.39.0 (#549) 2025-06-16 07:22:29 +00:00
dependabot[bot] 2bc67e7695
build(deps): bump oras.land/oras-go/v2 from 2.5.0 to 2.6.0 (#548) 2025-05-12 01:58:18 +00:00
dependabot[bot] 9d3ac1c22d
build(deps): bump golang.org/x/crypto from 0.37.0 to 0.38.0 (#547) 2025-05-12 01:55:26 +00:00
Jakub Jarosz e1a37eb756
chore: show openssf scorecard (#543)
Signed-off-by: Jakub Jarosz <jakub@jarosz.dev>
2025-05-09 18:01:53 +08:00
Junjie Gao 626ac1d9c0
fix: remove generate-envelope plugin support for blob signing (#546)
resolves #544

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2025-05-09 16:27:41 +08:00
Patrick Zheng de3655adc0
feat: `artifactType` support in signature manifest (#542)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2025-05-08 17:01:58 +08:00
dependabot[bot] 02cc632b8b
build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.10 to 3.4.11 (#539) 2025-04-28 08:58:14 +00:00
Patrick Zheng 287b8785c6
bump: bump up dependencies (#535)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2025-04-18 10:53:59 +08:00
Jakub Jarosz 81056bd695
fix: staticcheck complains - replace deprecated functions (#533)
This PR fixes a part of `staticcheck` complains reported in
https://github.com/notaryproject/notation-go/issues/531

The remaining issues will be addressed in a separate PRs.

Signed-off-by: Jakub Jarosz <jakub@jarosz.dev>
2025-04-16 13:31:47 +08:00
dependabot[bot] 0caf40c9ae
build(deps): bump golang.org/x/crypto from 0.36.0 to 0.37.0 (#530)
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from
0.36.0 to 0.37.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="959f8f3db0"><code>959f8f3</code></a>
go.mod: update golang.org/x dependencies</li>
<li><a
href="769bcd6997"><code>769bcd6</code></a>
ssh: use the configured rand in kex init</li>
<li><a
href="d0a798f774"><code>d0a798f</code></a>
cryptobyte: fix typo 'octects' into 'octets' for asn1.go</li>
<li><a
href="acbcbef23f"><code>acbcbef</code></a>
acme: remove unnecessary []byte conversion</li>
<li><a
href="376eb14006"><code>376eb14</code></a>
x509roots: support constrained roots</li>
<li><a
href="b369b723c8"><code>b369b72</code></a>
crypto/internal/poly1305: implement function update in assembly on
loong64</li>
<li><a
href="6b853fbea3"><code>6b853fb</code></a>
ssh/knownhosts: check more than one key</li>
<li>See full diff in <a
href="https://github.com/golang/crypto/compare/v0.36.0...v0.37.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/crypto&package-manager=go_modules&previous-version=0.36.0&new-version=0.37.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-16 09:57:16 +08:00
Jakub Jarosz dbbeca1679
refactor: update error names to align with Go convention (#529)
This PR fixes https://github.com/notaryproject/notation-go/issues/528
and adds the following changes:

- add new custom errors (naming aligned with [Go
convention](https://go.dev/doc/effective_go#errors))
- mark old custom error deprecated

---------

Signed-off-by: Jakub Jarosz <jakub@jarosz.dev>
2025-04-11 15:24:06 +08:00
dependabot[bot] 3bd0ac92b2
build(deps): bump github.com/opencontainers/image-spec from 1.1.0 to 1.1.1 (#523) 2025-03-25 09:39:58 +00:00
dependabot[bot] 02ff112615
build(deps): bump golang.org/x/crypto from 0.35.0 to 0.36.0 (#525) 2025-03-25 09:36:18 +00:00
dependabot[bot] d6e291d617
build(deps): bump golang.org/x/mod from 0.23.0 to 0.24.0 (#524) 2025-03-25 09:12:14 +00:00
dependabot[bot] fd2a749d85
build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 (#526) 2025-03-25 09:09:36 +00:00
Patrick Zheng fcc1ce32f8
fix: update error message for signing key config (#527)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2025-03-24 14:25:55 +08:00
Junjie Gao fdcf9cc476
feat: add `SignOCI` function to return both artifact and signature manifest descriptor (#522)
Feat:
- added `SignOCI` function and **deprecated** the `Sign` function to
return both artifact and signature manifest descriptor.

Fix:
- handled referrer index deletion error as a warning to avoid confusion
about the final signing status.

Resolve part of https://github.com/notaryproject/notation/issues/695

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2025-03-10 14:03:48 +08:00
dependabot[bot] 4b058c99ef
build(deps): bump golang.org/x/crypto from 0.33.0 to 0.35.0 (#520) 2025-03-07 03:30:54 +00:00
Patrick Zheng bb2ee7a8a7
fix: timestamp against signing time (#518)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2025-02-21 16:11:11 +08:00
Junjie Gao 752832c674
bump: update go v1.23 (#512)
bump:
- updated go v1.23

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2025-02-20 16:05:20 +08:00
dependabot[bot] 93b25a3e46
build(deps): bump golang.org/x/crypto from 0.32.0 to 0.33.0 (#510) 2025-02-17 02:30:16 +00:00
dependabot[bot] c2b5474983
build(deps): bump golang.org/x/mod from 0.22.0 to 0.23.0 (#509) 2025-02-14 01:08:20 +00:00
Patrick Zheng 6eb53a50d6
refactor: updated verifier constructor (#508)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2025-01-22 15:22:55 +08:00
Patrick Zheng 96b7133718
bump: bump up dependencies (#504)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2025-01-15 14:15:29 +08:00
dependabot[bot] 851cbabbc4
build(deps): bump golang.org/x/crypto from 0.31.0 to 0.32.0 (#503) 2025-01-14 00:44:47 +00:00
Patrick Zheng b40566d885
chore: clean up (#502)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2025-01-14 07:55:12 +08:00
Patrick Zheng 26ce0894a6
docs: fix docs (#498)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2025-01-07 08:36:20 +08:00
dependabot[bot] e5eef5e899
build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.8 to 3.4.10 (#501) 2024-12-31 00:28:04 +00:00
Junjie Gao 9fe530b5fd
fix: `check-line-endings` command of Makefile (#499)
Fix:
- update the command to search script file in `.` instead of `scripts/`
folder to avoid returning an error.

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
Co-authored-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-12-30 10:13:10 +08:00
Patrick Zheng 3eeef95a40
chore(verifier): improve log (#497)
This PR improves log readability of the library.

Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-12-29 00:56:17 -08:00
Junjie Gao cefd007065
fix: limit the plugin output size (#484)
Fix:
- set the plugin output limit for STDOUT and STDERR to be 10MiB

Test:
- when the plugin output size exceeds 64MiB, the output pipe breaks, and
the plugin process outputs an error to STDERR

Spec changes: https://github.com/notaryproject/specifications/pull/320
Resolves #187

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-12-13 08:19:49 +08:00
dependabot[bot] d1d64e7041
build(deps): bump golang.org/x/crypto from 0.29.0 to 0.31.0 (#491) 2024-12-13 00:15:40 +00:00
Patrick Zheng 57c5e0dadf
bump: bump up dependencies (#488)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-12-10 07:57:44 +08:00
Junjie Gao 95bac00829
perf(log): encode objects only when logged (#481)
Fix:
- replaced `.String()` with the `%v` format to avoid rendering the
string before actually logging it.

Resolves #480

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-12-02 10:03:54 +08:00
Patrick Zheng e99be1954a
fix: enable timestamping cert chain revocation check during signing (#482)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-12-02 08:30:56 +08:00
Junjie Gao 240181a5eb
fix: add warning message for non-revokable certificate (#479)
Fix:
- added warning message for non-revokable certificate

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-11-29 08:38:08 +08:00
Patrick Zheng 5a323330d0
fix: timestamping (#478)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
Co-authored-by: Pritesh Bandi <priteshbandi@gmail.com>
2024-11-15 16:09:00 +08:00
Pritesh Bandi 7b9636e239
chore: Improve error message in case of plugin timeout (#472)
When a plugin exceeds the specified timeout or deadline for content
processing, the current error message displayed is ```signal: killed```.
This PR updates the error message to a more informative message:
```[plugin_name] [command_name] command execution timeout: signal:
killed```

---------

Signed-off-by: Pritesh Bandi <priteshbandi@gmail.com>
2024-11-14 11:06:45 -08:00
dependabot[bot] 8797d86735
build(deps): bump golang.org/x/mod from 0.21.0 to 0.22.0 (#477) 2024-11-14 19:03:25 +00:00
Patrick Zheng cf70e77099
fix: crl cache log and err msg (#475)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-11-12 07:51:07 +08:00
dependabot[bot] 0191e75373
build(deps): bump golang.org/x/crypto from 0.28.0 to 0.29.0 (#476) 2024-11-11 17:44:50 +00:00
dependabot[bot] f332ed9212
build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 (#474) 2024-11-11 17:42:01 +00:00
dependabot[bot] f855f25526
build(deps): bump golang.org/x/crypto from 0.27.0 to 0.28.0 (#468) 2024-11-05 04:44:48 +00:00
Patrick Zheng c6c8cc7f66
chore: add crl cache debug logs (#473)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-11-03 20:19:01 +08:00
Patrick Zheng 1dc55d0add
fix: added tsa trust store root cert validation (#471)
This PR adds tsa trust store root cert validation while getting
certificates from trust store. This is to fail fast if cert in TSA trust
store is not a root CA certificate.

Resolves #470

---------

Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-11-01 10:40:07 -07:00
Shiwei Zhang e1b80e2f2e
Merge commit from fork
fix: OS error when setting CRL cache leads to denial of signature verification
2024-11-01 10:35:22 +08:00
Junjie Gao 7d11fa2346
fix: OS error when setting CRL cache leads to denial of signature verification
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-10-23 02:59:09 +00:00
Patrick Zheng 82014a953f
chore: update logs (#469)
This PR updates logs.
Resolves #430. Also should resolve issue https://github.com/notaryproject/notation/issues/1004.

Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-10-16 10:31:10 -07:00
dependabot[bot] 11866a54a0
build(deps): bump github.com/notaryproject/notation-core-go from 1.1.1-0.20240920045731-0786f51de737 to 1.2.0-rc.1 (#466) 2024-10-01 05:51:37 +00:00
dependabot[bot] 4c2d986035
build(deps): bump github.com/veraison/go-cose from 1.1.0 to 1.3.0 (#467) 2024-10-01 05:48:38 +00:00
AdamKorcz a86f8da6ea
test: add fuzz test (#459)
Adds a fuzz test from cncf-fuzzing:
https://github.com/cncf/cncf-fuzzing/blob/main/projects/notary/fuzz_pkix_test.go

Signed-off-by: Adam Korczynski <adam@adalogics.com>
2024-09-29 14:11:20 -07:00
Patrick Zheng 84c2ec0762
feat: crl cache (#462)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-09-26 09:57:24 +08:00
dependabot[bot] 9faa6e2747
build(deps): bump golang.org/x/crypto from 0.26.0 to 0.27.0 (#455) 2024-09-20 04:58:03 +00:00
Junjie Gao 7c20eba012
feat: crl support (#458)
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-09-19 07:53:58 +08:00
Patrick Zheng 694e3a0314
bump: update golang version to v1.22.0 (#457)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-09-10 13:37:32 +08:00
JasonTheDeveloper 4d76f9a415
fix: dir no longer panics when HOME and XDG_CONFIG_HOME are not set (#449)
This PR addresses the issue #446

In this PR I:

- I removed the `init()` function from `dir/path`
- When `userConfigDir()` returns an error, instead of `panic(err)` I
default to the current directory instead
- Split `loadUserPath()` into two new functions used to setup and return
the values for `UserConfigDir` and `UserLibexecDir`
- Added additional unit tests for the two new functions and to test the
default directory is used when `HOME` is set to `""`

---------

Signed-off-by: Jason <jagoodse@microsoft.com>
Signed-off-by: JasonTheDeveloper <jagoodse@microsoft.com>
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
Co-authored-by: Shiwei Zhang <shizh@microsoft.com>
Co-authored-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-09-04 15:05:30 +08:00
Patrick Zheng 115509e289
bump: bump up notation-core-go and signingAgent (#444)
This PR targets on main branch.

---------

Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-09-04 08:59:43 +08:00
Patrick Zheng 4403efa431
docs: create release checklist on main (#443)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-09-04 08:47:35 +08:00
Patrick Zheng 974c2916fa
bump: upgrade golang to v1.22 (#440)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-08-27 09:59:07 +08:00
dependabot[bot] 8e2131d192
build(deps): bump golang.org/x/crypto from 0.25.0 to 0.26.0 (#434) 2024-08-19 03:22:44 +00:00
dependabot[bot] aaadf0b342
build(deps): bump golang.org/x/mod from 0.19.0 to 0.20.0 (#433) 2024-08-18 17:23:22 +00:00
Patrick Zheng 3c5a659c1d
refactor!: update revocation based on notation-core-go (#429)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-08-14 08:46:21 +08:00
Junjie Gao c3b2f51140
feat: support distinguished name state(S) (#432)
Resolves #431 
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-08-07 10:07:09 +08:00
Junjie Gao 09e32d7940
fix: improve error message for plugin (#406)
Fix:
- Simplified the error message for plugin errors.
- Logged the details of the failing plugin command instead of printing
them out in the CLI output, as users may not understand what the plugin
command is, which can mislead them about the important part of the error
message.
- Logged the error code instead of printing it out in the CLI output to
simplify the error message.

Previous example:
```
notation sign xxx.azurecr.io/hello-app:v1 --plugin azure-kv --id https://xxx.vault.azure.net/certificates --plugin-config credential_type=azurecli 
Warning: Always sign the artifact using digest(@sha256:...) rather than a tag(:v1) because tags are mutable and a tag reference can point to a different artifact than the one signed.
Error: failed to execute the describe-key command for azure-kv: VALIDATION_ERROR: Invalid input passed to "--id". Please follow this format to input the ID "https://<vault-name>.vault.azure.net/certificates/<certificate-name>/[certificate-version]"
```

Current example:
```
notation sign xxx.azurecr.io/hello-app:v1 --plugin azure-kv --id https://xxx.vault.azure.net/certificates --plugin-config credential_type=azurecli 
Warning: Always sign the artifact using digest(@sha256:...) rather than a tag(:v1) because tags are mutable and a tag reference can point to a different artifact than the one signed.
Error: failed to sign with the plugin azure-kv: Invalid input passed to "--id". Please follow this format to input the ID "https://<vault-name>.vault.azure.net/certificates/<certificate-name>/[certificate-version]"
```
Resolves #404 
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
Co-authored-by: Pritesh Bandi <priteshbandi@gmail.com>
2024-07-30 09:34:01 +08:00
Patrick Zheng b3b8cbe0cc
bump: bump up notation-core-go (#426)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-07-23 08:13:10 +08:00
Patrick Zheng 8340920475
fix: fix usage of SignerInfo.AuthenticSigningTime (#424)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-07-19 12:57:53 +08:00
Patrick Zheng 8ada12a746
chore: fix logs (#417)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-07-17 12:44:22 +08:00
Patrick Zheng b52583166f
feat: Timestamp (#418)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-07-15 12:40:11 +08:00
dependabot[bot] 1c3e3783d5
build(deps): bump golang.org/x/crypto from 0.24.0 to 0.25.0 (#420) 2024-07-12 07:26:11 +00:00
dependabot[bot] c2cd70f095
build(deps): bump golang.org/x/mod from 0.18.0 to 0.19.0 (#421) 2024-07-11 15:34:44 +00:00
Junjie Gao 54b73d8a69
fix(ci): pass CODECOV_TOKEN to reusable-build.yml (#419)
Depends on https://github.com/notaryproject/notation-core-go/pull/209

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-07-09 09:05:24 +08:00
Pritesh Bandi a1825db13d
feat: Implements VerifyBlob functionality (#394)
This PR implements VerifyBlob functionality as per
https://github.com/notaryproject/specifications/blob/main/specs/trust-store-trust-policy.md
---------

Signed-off-by: Pritesh Bandi <priteshbandi@gmail.com>
2024-06-27 00:10:28 -07:00
dependabot[bot] a6b0a8c2e5
build(deps): bump golang.org/x/crypto from 0.23.0 to 0.24.0 (#414) 2024-06-12 20:05:29 +00:00
dependabot[bot] aba7ba74b2
build(deps): bump golang.org/x/mod from 0.17.0 to 0.18.0 (#413) 2024-06-10 18:23:07 +00:00
83 changed files with 5882 additions and 1641 deletions

View File

@ -16,3 +16,6 @@ coverage:
project:
default:
target: 80%
patch:
default:
target: 80%

View File

@ -26,3 +26,5 @@ on:
jobs:
build:
uses: notaryproject/notation-core-go/.github/workflows/reusable-build.yml@main
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,3 +1,3 @@
# Repo-Level Owners (in alphabetical order)
# Note: This is only for the notaryproject/notation-go repo
* @gokarnm @JeyJeyGao @niazfk @priteshbandi @rgnote @shizhMSFT @toddysm @Two-Hearts @vaninrao10 @yizha1
* @gokarnm @niazfk @priteshbandi @rgnote @shizhMSFT @toddysm @Two-Hearts @vaninrao10 @yizha1

View File

@ -9,7 +9,6 @@ Yi Zha <yizha1@microsoft.com> (@yizha1)
# Repo-Level Maintainers (in alphabetical order)
# Note: This is for the notaryproject/notation-go repo
Junjie Gao <junjiegao@microsoft.com> (@JeyJeyGao)
Milind Gokarn <gokarnm@amazon.com> (@gokarnm)
Patrick Zheng <patrickzheng@microsoft.com> (@Two-Hearts)
Rakesh Gariganti <garigant@amazon.com> (@rgnote)
@ -17,3 +16,6 @@ Rakesh Gariganti <garigant@amazon.com> (@rgnote)
# Emeritus Org Maintainers (in alphabetical order)
Justin Cormack <justin.cormack@docker.com> (@justincormack)
Steve Lasker <StevenLasker@hotmail.com> (@stevelasker)
# Emeritus Repo-Level Maintainers (in alphabetical order)
Junjie Gao <junjiegao@microsoft.com> (@JeyJeyGao)

View File

@ -29,7 +29,7 @@ clean:
.PHONY: check-line-endings
check-line-endings: ## check line endings
! find . -name "*.go" -type f -exec file "{}" ";" | grep CRLF
! find scripts -name "*.sh" -type f -exec file "{}" ";" | grep CRLF
! find . -name "*.sh" -type f -exec file "{}" ";" | grep CRLF
.PHONY: fix-line-endings
fix-line-endings: ## fix line endings

View File

@ -3,6 +3,7 @@
[![Build Status](https://github.com/notaryproject/notation-go/actions/workflows/build.yml/badge.svg?event=push&branch=main)](https://github.com/notaryproject/notation-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain)
[![Codecov](https://codecov.io/gh/notaryproject/notation-go/branch/main/graph/badge.svg)](https://codecov.io/gh/notaryproject/notation-go)
[![Go Reference](https://pkg.go.dev/badge/github.com/notaryproject/notation-go.svg)](https://pkg.go.dev/github.com/notaryproject/notation-go@main)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/notaryproject/notation-go/badge)](https://scorecard.dev/viewer/?uri=github.com/notaryproject/notation-go)
notation-go contains libraries for signing and verification of artifacts as per [Notary Project specifications](https://github.com/notaryproject/specifications). notation-go is being used by [notation](https://github.com/notaryproject/notation) CLI for signing and verifying artifacts.

36
RELEASE_CHECKLIST.md Normal file
View File

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

View File

@ -17,6 +17,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/notaryproject/notation-go/dir"
@ -33,6 +34,9 @@ func TestLoadNonExistentFile(t *testing.T) {
}
func TestLoadSymlink(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
root := t.TempDir()
dir.UserConfigDir = root
fileName := "symlink"

35
config/errors.go Normal file
View File

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

28
config/errors_test.go Normal file
View File

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

View File

@ -50,9 +50,6 @@ type KeySuite struct {
*ExternalKey
}
var errorKeyNameEmpty = errors.New("key name cannot be empty")
var errKeyNotFound = errors.New("signing key not found")
// SigningKeys reflects the signingkeys.json file.
type SigningKeys struct {
Default *string `json:"default,omitempty"`
@ -67,13 +64,12 @@ func NewSigningKeys() *SigningKeys {
// Add adds new signing key
func (s *SigningKeys) Add(name, keyPath, certPath string, markDefault bool) error {
if name == "" {
return errorKeyNameEmpty
return ErrKeyNameEmpty
}
_, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return err
}
ks := KeySuite{
Name: name,
X509KeyPair: &X509KeyPair{
@ -88,25 +84,20 @@ func (s *SigningKeys) Add(name, keyPath, certPath string, markDefault bool) erro
func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName string, pluginConfig map[string]string, markDefault bool) error {
logger := log.GetLogger(ctx)
logger.Debugf("Adding key with name %v and plugin name %v", keyName, pluginName)
if keyName == "" {
return errorKeyNameEmpty
return ErrKeyNameEmpty
}
if id == "" {
return errors.New("missing key id")
}
if pluginName == "" {
return errors.New("plugin name cannot be empty")
}
mgr := plugin.NewCLIManager(dir.PluginFS())
_, err := mgr.Get(ctx, pluginName)
if err != nil {
return err
}
ks := KeySuite{
Name: keyName,
ExternalKey: &ExternalKey{
@ -115,7 +106,6 @@ func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName str
PluginConfig: pluginConfig,
},
}
if err = s.add(ks, markDefault); err != nil {
logger.Error("Failed to add key with error: %v", err)
return err
@ -127,14 +117,12 @@ func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName str
// Get returns signing key for the given name
func (s *SigningKeys) Get(keyName string) (KeySuite, error) {
if keyName == "" {
return KeySuite{}, errorKeyNameEmpty
return KeySuite{}, ErrKeyNameEmpty
}
idx := slices.IndexIsser(s.Keys, keyName)
if idx < 0 {
return KeySuite{}, errKeyNotFound
return KeySuite{}, KeyNotFoundError{KeyName: keyName}
}
return s.Keys[idx], nil
}
@ -144,7 +132,6 @@ func (s *SigningKeys) GetDefault() (KeySuite, error) {
return KeySuite{}, errors.New("default signing key not set." +
" Please set default signing key or specify a key name")
}
return s.Get(*s.Default)
}
@ -153,12 +140,11 @@ func (s *SigningKeys) Remove(keyName ...string) ([]string, error) {
var deletedNames []string
for _, name := range keyName {
if name == "" {
return deletedNames, errorKeyNameEmpty
return deletedNames, ErrKeyNameEmpty
}
idx := slices.IndexIsser(s.Keys, name)
if idx < 0 {
return deletedNames, errors.New(name + ": not found")
return deletedNames, KeyNotFoundError{KeyName: name}
}
s.Keys = slices.Delete(s.Keys, idx)
deletedNames = append(deletedNames, name)
@ -172,13 +158,11 @@ func (s *SigningKeys) Remove(keyName ...string) ([]string, error) {
// UpdateDefault updates default signing key
func (s *SigningKeys) UpdateDefault(keyName string) error {
if keyName == "" {
return errorKeyNameEmpty
return ErrKeyNameEmpty
}
if !slices.ContainsIsser(s.Keys, keyName) {
return fmt.Errorf("key with name '%s' not found", keyName)
return KeyNotFoundError{KeyName: keyName}
}
s.Default = &keyName
return nil
}
@ -189,11 +173,9 @@ func (s *SigningKeys) Save() error {
if err != nil {
return err
}
if err := validateKeys(s); err != nil {
return err
}
return save(path, s)
}
@ -208,11 +190,9 @@ func LoadSigningKeys() (*SigningKeys, error) {
}
return nil, err
}
if err := validateKeys(&config); err != nil {
return nil, err
}
return &config, nil
}
@ -224,11 +204,9 @@ func LoadExecSaveSigningKeys(fn func(keys *SigningKeys) error) error {
if err != nil {
return err
}
if err := fn(signingKeys); err != nil {
return err
}
return signingKeys.Save()
}
@ -241,12 +219,10 @@ func (s *SigningKeys) add(key KeySuite, markDefault bool) error {
if slices.ContainsIsser(s.Keys, key.Name) {
return fmt.Errorf("signing key with name %q already exists", key.Name)
}
s.Keys = append(s.Keys, key)
if markDefault {
s.Default = &key.Name
}
return nil
}
@ -262,17 +238,14 @@ func validateKeys(config *SigningKeys) error {
}
uniqueKeyNames.Add(key.Name)
}
if config.Default != nil {
defaultKey := *config.Default
if len(defaultKey) == 0 {
return fmt.Errorf("malformed %s: default key name cannot be empty", dir.PathSigningKeys)
}
if !uniqueKeyNames.Contains(defaultKey) {
return fmt.Errorf("malformed %s: default key '%s' not found", dir.PathSigningKeys, defaultKey)
}
}
return nil
}

View File

@ -17,6 +17,7 @@ import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"os"
"path/filepath"
"reflect"
@ -310,14 +311,22 @@ func TestGet(t *testing.T) {
})
t.Run("NonExistent", func(t *testing.T) {
if _, err := sampleSigningKeysInfo.Get("nonExistent"); err == nil {
_, err := sampleSigningKeysInfo.Get("nonExistent")
if err == nil {
t.Error("expected Get() to fail for nonExistent key name")
}
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
t.Error("expected Get() to return ErrorKeyNotFound")
}
})
t.Run("InvalidName", func(t *testing.T) {
if _, err := sampleSigningKeysInfo.Get(""); err == nil {
t.Error("expected Get() to fail for invalid key name")
t.Run("EmptyName", func(t *testing.T) {
_, err := sampleSigningKeysInfo.Get("")
if err == nil {
t.Error("expected Get() to fail for empty key name")
}
if !errors.Is(err, ErrKeyNameEmpty) {
t.Error("expected Get() to return ErrorKeyNameEmpty")
}
})
}
@ -358,14 +367,22 @@ func TestUpdateDefault(t *testing.T) {
})
t.Run("NonExistent", func(t *testing.T) {
if err := sampleSigningKeysInfo.UpdateDefault("nonExistent"); err == nil {
err := sampleSigningKeysInfo.UpdateDefault("nonExistent")
if err == nil {
t.Error("expected Get() to fail for nonExistent key name")
}
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
t.Error("expected Get() to return ErrorKeyNotFound")
}
})
t.Run("InvalidName", func(t *testing.T) {
if err := sampleSigningKeysInfo.UpdateDefault(""); err == nil {
t.Error("expected Get() to fail for invalid key name")
t.Run("EmptyName", func(t *testing.T) {
err := sampleSigningKeysInfo.UpdateDefault("")
if err == nil {
t.Error("expected Get() to fail for empty key name")
}
if !errors.Is(err, ErrKeyNameEmpty) {
t.Error("expected Get() to return ErrorKeyNameEmpty")
}
})
}
@ -382,21 +399,28 @@ func TestRemove(t *testing.T) {
if _, err := testSigningKeysInfo.Get(testKeyName); err == nil {
t.Error("Delete() filed to delete key")
}
if keys[0] != testKeyName {
t.Error("Delete() deleted key name mismatch")
}
})
t.Run("NonExistent", func(t *testing.T) {
if _, err := testSigningKeysInfo.Remove(testKeyName); err == nil {
_, err := testSigningKeysInfo.Remove("nonExistent")
if err == nil {
t.Error("expected Get() to fail for nonExistent key name")
}
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
t.Error("expected Get() to return ErrorKeyNotFound")
}
})
t.Run("InvalidName", func(t *testing.T) {
if _, err := testSigningKeysInfo.Remove(""); err == nil {
t.Error("expected Get() to fail for invalid key name")
t.Run("EmptyName", func(t *testing.T) {
_, err := testSigningKeysInfo.Remove("")
if err == nil {
t.Error("expected Get() to fail for empty key name")
}
if !errors.Is(err, ErrKeyNameEmpty) {
t.Error("expected Get() to return ErrorKeyNameEmpty")
}
})
}

View File

@ -51,10 +51,17 @@ func NewSysFS(root string) SysFS {
// ConfigFS is the config SysFS
func ConfigFS() SysFS {
return NewSysFS(UserConfigDir)
return NewSysFS(userConfigDirPath())
}
// PluginFS is the plugin SysFS
func PluginFS() SysFS {
return NewSysFS(filepath.Join(UserLibexecDir, PathPlugins))
return NewSysFS(filepath.Join(userLibexecDirPath(), PathPlugins))
}
// CacheFS is the cache SysFS.
//
// To get the root of crl file cache, use `CacheFS().SysFS(PathCRLCache)`.
func CacheFS() SysFS {
return NewSysFS(userCacheDirPath())
}

View File

@ -67,7 +67,18 @@ func TestPluginFS(t *testing.T) {
if err != nil {
t.Fatalf("SysPath() failed. err = %v", err)
}
if path != filepath.Join(UserLibexecDir, PathPlugins, "plugin") {
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(UserLibexecDir, PathPlugins, "plugin"))
if path != filepath.Join(userLibexecDirPath(), PathPlugins, "plugin") {
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(userLibexecDirPath(), PathPlugins, "plugin"))
}
}
func TestCRLFileCacheFS(t *testing.T) {
cacheFS := CacheFS()
path, err := cacheFS.SysPath(PathCRLCache)
if err != nil {
t.Fatalf("SysPath() failed. err = %v", err)
}
if path != filepath.Join(UserCacheDir, PathCRLCache) {
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, UserConfigDir)
}
}

View File

@ -11,8 +11,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// package dir implements Notation directory structure.
// [directory spec]: https://github.com/notaryproject/notation/blob/main/specs/directory.md
// Package dir implements Notation directory structure.
// [directory spec]: https://notaryproject.dev/docs/user-guides/how-to/directory-structure/
//
// Example:
//
@ -31,7 +31,7 @@
// - Set custom configurations directory:
// dir.UserConfigDir = '/path/to/configurations/'
//
// Only user level directory is supported for RC.1, and system level directory
// Only user level directory is supported, and system level directory
// may be added later.
package dir
@ -44,6 +44,7 @@ import (
var (
UserConfigDir string // Absolute path of user level {NOTATION_CONFIG}
UserLibexecDir string // Absolute path of user level {NOTATION_LIBEXEC}
UserCacheDir string // Absolute path of user level {NOTATION_CACHE}
)
const (
@ -57,10 +58,15 @@ const (
PathConfigFile = "config.json"
// PathSigningKeys is the signingkeys file relative path.
PathSigningKeys = "signingkeys.json"
// PathTrustPolicy is the trust policy file relative path.
// PathTrustPolicy is the OCI trust policy file relative path.
//
// Deprecated: PathTrustPolicy exists for historical compatibility and should not be used.
// To get OCI trust policy path, use PathOCITrustPolicy.
PathTrustPolicy = "trustpolicy.json"
// PathPlugins is the plugins directory relative path.
PathPlugins = "plugins"
// PathOCITrustPolicy is the OCI trust policy file relative path.
PathOCITrustPolicy = "trustpolicy.oci.json"
// PathBlobTrustPolicy is the Blob trust policy file relative path.
PathBlobTrustPolicy = "trustpolicy.blob.json"
// LocalKeysDir is the directory name for local key relative path.
LocalKeysDir = "localkeys"
// LocalCertificateExtension defines the extension of the certificate files.
@ -71,23 +77,62 @@ const (
TrustStoreDir = "truststore"
)
var userConfigDir = os.UserConfigDir // for unit test
// The relative path to {NOTATION_LIBEXEC}
const (
// PathPlugins is the plugins directory relative path.
PathPlugins = "plugins"
)
func init() {
loadUserPath()
// The relative path to {NOTATION_CACHE}
const (
// PathCRLCache is the crl file cache directory relative path.
PathCRLCache = "crl"
)
// for unit tests
var (
userConfigDir = os.UserConfigDir
userCacheDir = os.UserCacheDir
)
// userConfigDirPath returns the user level {NOTATION_CONFIG} path.
func userConfigDirPath() string {
if UserConfigDir == "" {
userDir, err := userConfigDir()
if err != nil {
// fallback to current directory
UserConfigDir = "." + notation
return UserConfigDir
}
// set user config
UserConfigDir = filepath.Join(userDir, notation)
}
return UserConfigDir
}
// loadUserPath function defines UserConfigDir and UserLibexecDir.
func loadUserPath() {
// set user config
userDir, err := userConfigDir()
if err != nil {
panic(err)
// userLibexecDirPath returns the user level {NOTATION_LIBEXEC} path.
func userLibexecDirPath() string {
if UserLibexecDir == "" {
// set user libexec
UserLibexecDir = userConfigDirPath()
}
UserConfigDir = filepath.Join(userDir, notation)
return UserLibexecDir
}
// set user libexec
UserLibexecDir = UserConfigDir
// userCacheDirPath returns the user level {NOTATION_CACHE} path.
func userCacheDirPath() string {
if UserCacheDir == "" {
userDir, err := userCacheDir()
if err != nil {
// fallback to current directory
UserCacheDir = filepath.Join("."+notation, "cache")
return UserCacheDir
}
// set user cache
UserCacheDir = filepath.Join(userDir, notation)
}
return UserCacheDir
}
// LocalKeyPath returns the local key and local cert relative paths.

View File

@ -14,30 +14,70 @@
package dir
import (
"os"
"path/filepath"
"testing"
)
func mockGetUserConfig() (string, error) {
func mockUserPath() (string, error) {
return "/path/", nil
}
func Test_loadPath(t *testing.T) {
wantDir := filepath.FromSlash("/path/notation")
userConfigDir = mockGetUserConfig
loadUserPath()
if UserConfigDir != wantDir {
t.Fatalf(`loadPath() UserConfigDir is incorrect. got: %q, want: %q`, UserConfigDir, wantDir)
}
func setup() {
UserConfigDir = ""
UserLibexecDir = ""
UserCacheDir = ""
}
if UserLibexecDir != UserConfigDir {
t.Fatalf(`loadPath() UserLibexecDir is incorrect. got: %q, want: %q`, UserLibexecDir, wantDir)
func Test_UserConfigDirPath(t *testing.T) {
userConfigDir = mockUserPath
setup()
got := userConfigDirPath()
if got != "/path/notation" {
t.Fatalf(`UserConfigDirPath() = %q, want "/path/notation"`, got)
}
}
func Test_NoHomeVariable(t *testing.T) {
t.Setenv("HOME", "")
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("XDG_CACHE_HOME", "")
setup()
userConfigDir = os.UserConfigDir
got := userConfigDirPath()
if got != ".notation" {
t.Fatalf(`userConfigDirPath() = %q, want ".notation"`, got)
}
got = userCacheDirPath()
want := filepath.Join("."+notation, "cache")
if got != want {
t.Fatalf(`userCacheDirPath() = %q, want %q`, got, want)
}
}
func Test_UserLibexecDirPath(t *testing.T) {
userConfigDir = mockUserPath
setup()
got := userLibexecDirPath()
if got != "/path/notation" {
t.Fatalf(`UserConfigDirPath() = %q, want "/path/notation"`, got)
}
}
func Test_UserCacheDirPath(t *testing.T) {
userCacheDir = mockUserPath
setup()
got := userCacheDirPath()
if got != "/path/notation" {
t.Fatalf(`UserCacheDirPath() = %q, want "/path/notation"`, got)
}
}
func TestLocalKeyPath(t *testing.T) {
userConfigDir = mockGetUserConfig
loadUserPath()
userConfigDir = mockUserPath
setup()
_ = userConfigDirPath()
_ = userLibexecDirPath()
gotKeyPath, gotCertPath := LocalKeyPath("web")
if gotKeyPath != "localkeys/web.key" {
t.Fatalf(`LocalKeyPath() gotKeyPath = %q, want "localkeys/web.key"`, gotKeyPath)
@ -48,8 +88,10 @@ func TestLocalKeyPath(t *testing.T) {
}
func TestX509TrustStoreDir(t *testing.T) {
userConfigDir = mockGetUserConfig
loadUserPath()
userConfigDir = mockUserPath
setup()
_ = userConfigDirPath()
_ = userLibexecDirPath()
if got := X509TrustStoreDir("ca", "web"); got != "truststore/x509/ca/web" {
t.Fatalf(`X509TrustStoreDir() = %q, want "truststore/x509/ca/web"`, got)
}

View File

@ -15,11 +15,17 @@ package notation
// ErrorPushSignatureFailed is used when failed to push signature to the
// target registry.
type ErrorPushSignatureFailed struct {
//
// Deprecated: Use PushSignatureFailedError instead.
type ErrorPushSignatureFailed = PushSignatureFailedError
// PushSignatureFailedError is used when failed to push signature to the
// target registry.
type PushSignatureFailedError struct {
Msg string
}
func (e ErrorPushSignatureFailed) Error() string {
func (e PushSignatureFailedError) Error() string {
if e.Msg != "" {
return "failed to push signature to registry with error: " + e.Msg
}
@ -28,11 +34,17 @@ func (e ErrorPushSignatureFailed) Error() string {
// ErrorVerificationInconclusive is used when signature verification fails due
// to a runtime error (e.g. a network error)
type ErrorVerificationInconclusive struct {
//
// Deprecated: Use VerificationInconclusiveError instead.
type ErrorVerificationInconclusive = VerificationInconclusiveError
// VerificationInconclusiveError is used when signature verification fails due
// to a runtime error (e.g. a network error)
type VerificationInconclusiveError struct {
Msg string
}
func (e ErrorVerificationInconclusive) Error() string {
func (e VerificationInconclusiveError) Error() string {
if e.Msg != "" {
return e.Msg
}
@ -41,11 +53,17 @@ func (e ErrorVerificationInconclusive) Error() string {
// ErrorNoApplicableTrustPolicy is used when there is no trust policy that
// applies to the given artifact
type ErrorNoApplicableTrustPolicy struct {
//
// Deprecated: Use NoApplicableTrustPolicyError instead.
type ErrorNoApplicableTrustPolicy = NoApplicableTrustPolicyError
// NoApplicableTrustPolicyError is used when there is no trust policy that
// applies to the given artifact
type NoApplicableTrustPolicyError struct {
Msg string
}
func (e ErrorNoApplicableTrustPolicy) Error() string {
func (e NoApplicableTrustPolicyError) Error() string {
if e.Msg != "" {
return e.Msg
}
@ -54,11 +72,17 @@ func (e ErrorNoApplicableTrustPolicy) Error() string {
// ErrorSignatureRetrievalFailed is used when notation is unable to retrieve the
// digital signature/s for the given artifact
type ErrorSignatureRetrievalFailed struct {
//
// Deprecated: Use SignatureRetrievalFailedError instead.
type ErrorSignatureRetrievalFailed = SignatureRetrievalFailedError
// SignatureRetrievalFailedError is used when notation is unable to retrieve the
// digital signature/s for the given artifact
type SignatureRetrievalFailedError struct {
Msg string
}
func (e ErrorSignatureRetrievalFailed) Error() string {
func (e SignatureRetrievalFailedError) Error() string {
if e.Msg != "" {
return e.Msg
}
@ -67,11 +91,17 @@ func (e ErrorSignatureRetrievalFailed) Error() string {
// ErrorVerificationFailed is used when it is determined that the digital
// signature/s is not valid for the given artifact
type ErrorVerificationFailed struct {
//
// Deprecated: Use VerificationFailedError instead.
type ErrorVerificationFailed = VerificationFailedError
// VerificationFailedError is used when it is determined that the digital
// signature/s is not valid for the given artifact
type VerificationFailedError struct {
Msg string
}
func (e ErrorVerificationFailed) Error() string {
func (e VerificationFailedError) Error() string {
if e.Msg != "" {
return e.Msg
}
@ -80,11 +110,17 @@ func (e ErrorVerificationFailed) Error() string {
// ErrorUserMetadataVerificationFailed is used when the signature does not
// contain the user specified metadata
type ErrorUserMetadataVerificationFailed struct {
//
// Deprecated: Use UserMetadataVerificationFailedError instead.
type ErrorUserMetadataVerificationFailed = UserMetadataVerificationFailedError
// UserMetadataVerificationFailedError is used when the signature does not
// contain the user specified metadata
type UserMetadataVerificationFailedError struct {
Msg string
}
func (e ErrorUserMetadataVerificationFailed) Error() string {
func (e UserMetadataVerificationFailedError) Error() string {
if e.Msg != "" {
return e.Msg
}

View File

@ -91,3 +91,80 @@ func TestErrorMessages(t *testing.T) {
})
}
}
func TestCustomErrorPrintsCorrectMessage(t *testing.T) {
tests := []struct {
name string
err error
want string
}{
{
name: "PushSignatureFailedError with message",
err: PushSignatureFailedError{Msg: "test message"},
want: "failed to push signature to registry with error: test message",
},
{
name: "PushSignatureFailedError without message",
err: PushSignatureFailedError{},
want: "failed to push signature to registry",
},
{
name: "VerificationInconclusiveError with message",
err: VerificationInconclusiveError{Msg: "test message"},
want: "test message",
},
{
name: "VerificationInconclusiveError without message",
err: VerificationInconclusiveError{},
want: "signature verification was inclusive due to an unexpected error",
},
{
name: "NoApplicableTrustPolicyError with message",
err: NoApplicableTrustPolicyError{Msg: "test message"},
want: "test message",
},
{
name: "NoApplicableTrustPolicyError without message",
err: NoApplicableTrustPolicyError{},
want: "there is no applicable trust policy for the given artifact",
},
{
name: "SignatureRetrievalFailedError with message",
err: SignatureRetrievalFailedError{Msg: "test message"},
want: "test message",
},
{
name: "SignatureRetrievalFailedError without message",
err: SignatureRetrievalFailedError{},
want: "unable to retrieve the digital signature from the registry",
},
{
name: "VerificationFailedError with message",
err: VerificationFailedError{Msg: "test message"},
want: "test message",
},
{
name: "VerificationFailedError without message",
err: VerificationFailedError{},
want: "signature verification failed",
},
{
name: "UserMetadataVerificationFailedError with message",
err: UserMetadataVerificationFailedError{Msg: "test message"},
want: "test message",
},
{
name: "UserMetadataVerificationFailedError without message",
err: UserMetadataVerificationFailedError{},
want: "unable to find specified metadata in the signature",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.want {
t.Errorf("Error() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -46,7 +46,7 @@ func Example_localSign() {
// exampleSigner is a notation.Signer given key and X509 certificate chain.
// Users should replace `exampleCertTuple.PrivateKey` with their own private
// key and replace `exampleCerts` with the corresponding full certificate
// chain, following the Notary certificate requirements:
// 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 {

View File

@ -31,10 +31,10 @@ import (
// examplePolicyDocument is an example of a valid trust policy document.
// trust policy document should follow this spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-policy
var examplePolicyDocument = trustpolicy.Document{
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy
var examplePolicyDocument = trustpolicy.OCIDocument{
Version: "1.0",
TrustPolicies: []trustpolicy.TrustPolicy{
TrustPolicies: []trustpolicy.OCITrustPolicy{
{
Name: "test-statement-name",
RegistryScopes: []string{"example/software"},
@ -73,9 +73,9 @@ func Example_localVerify() {
}
// createTrustStore creates a trust store directory for demo purpose.
// Users could use the default trust store from Notary and add trusted
// certificates into it following the trust store spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-store
// Users could use the default trust store from Notary Project and
// add trusted certificates into it following the trust store spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
if err := createTrustStore(); err != nil {
panic(err) // Handle error
}
@ -172,8 +172,8 @@ func createTrustStore() error {
// generate the `exampleSignatureEnvelopePem` above.)
// Users should replace `exampleX509Certificate` with their own trusted
// certificate and add to the trust store, following the
// Notary certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
// Notary Project certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP

View File

@ -45,7 +45,7 @@ func Example_remoteSign() {
// exampleSigner is a notation.Signer given key and X509 certificate chain.
// Users should replace `exampleCertTuple.PrivateKey` with their own private
// key and replace `exampleCerts` with the corresponding full certificate
// chain, following the Notary certificate requirements:
// 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 {
@ -70,13 +70,16 @@ func Example_remoteSign() {
// remote sign core process
// upon successful signing, descriptor of the sign content is returned and
// the generated signature is pushed into remote registry.
targetDesc, err := notation.Sign(context.Background(), exampleSigner, exampleRepo, exampleSignOptions)
targetManifestDesc, sigManifestDesc, err := notation.SignOCI(context.Background(), exampleSigner, exampleRepo, exampleSignOptions)
if err != nil {
panic(err) // Handle error
}
fmt.Println("Successfully signed")
fmt.Println("targetDesc MediaType:", targetDesc.MediaType)
fmt.Println("targetDesc Digest:", targetDesc.Digest)
fmt.Println("targetDesc Size:", targetDesc.Size)
fmt.Println("targetManifestDesc.MediaType:", targetManifestDesc.MediaType)
fmt.Println("targetManifestDesc.Digest:", targetManifestDesc.Digest)
fmt.Println("targetManifestDesc.Size:", targetManifestDesc.Size)
fmt.Println("sigManifestDesc.MediaType:", sigManifestDesc.MediaType)
fmt.Println("sigManifestDesc.Digest:", sigManifestDesc.Digest)
fmt.Println("sigManifestDesc.Size:", sigManifestDesc.Size)
}

View File

@ -37,10 +37,10 @@ func Example_remoteVerify() {
// examplePolicyDocument is an example of a valid trust policy document.
// trust policy document should follow this spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-policy
examplePolicyDocument := trustpolicy.Document{
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy
examplePolicyDocument := trustpolicy.OCIDocument{
Version: "1.0",
TrustPolicies: []trustpolicy.TrustPolicy{
TrustPolicies: []trustpolicy.OCITrustPolicy{
{
Name: "test-statement-name",
RegistryScopes: []string{"*"},
@ -52,9 +52,9 @@ func Example_remoteVerify() {
}
// generateTrustStore generates a trust store directory for demo purpose.
// Users could use the default trust store from Notary and add trusted
// certificates into it following the trust store spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-store
// Users should configure their own trust store and add trusted certificates
// into it following the trust store spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
if err := generateTrustStore(); err != nil {
panic(err) // Handle error
}
@ -101,8 +101,8 @@ func generateTrustStore() error {
// an example of a valid X509 self-signed certificate for demo purpose ONLY.
// Users should replace `exampleX509Certificate` with their own trusted
// certificate and add to the trust store, following the
// Notary certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
// Notary Project certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP

88
example_signBlob_test.go Normal file
View File

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

View File

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

154
example_verifyBlob_test.go Normal file
View File

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

View File

@ -0,0 +1,192 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notation_test
import (
"context"
"fmt"
"os"
_ "github.com/notaryproject/notation-core-go/signature/cose"
_ "github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/verifier"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation-go/verifier/truststore"
"oras.land/oras-go/v2/registry/remote"
)
// Example_verifyWithTimestamp demonstrates how to use notation.Verify to verify
// signature of an artifact including RFC 3161 compliant timestamp countersignature
func Example_verifyWithTimestamp() {
// exampleArtifactReference is an example of the target artifact reference
exampleArtifactReference := "localhost:5000/software@sha256:60043cf45eaebc4c0867fea485a039b598f52fd09fd5b07b0b2d2f88fad9d74e"
// examplePolicyDocument is an example of a valid trust policy document with
// timestamping configurations.
// trust policy document should follow this spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy
examplePolicyDocument := trustpolicy.OCIDocument{
Version: "1.0",
TrustPolicies: []trustpolicy.OCITrustPolicy{
{
Name: "test-statement-name",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
// verify timestamp countersignature only if the signing
// certificate chain has expired.
// Default: trustpolicy.OptionAlways
VerifyTimestamp: trustpolicy.OptionAfterCertExpiry,
},
// `tsa` trust store type MUST be configured to enable
// timestamp verification
TrustStores: []string{"ca:valid-trust-store", "tsa:valid-tsa"},
// TrustedIdentities only contains trusted identities of `ca`
// and `signingAuthority`
TrustedIdentities: []string{"*"},
},
},
}
// generateTrustStoreWithTimestamp generates a trust store directory for demo purpose.
// Users should configure their own trust store and add trusted certificates
// into it following the trust store spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
if err := generateTrustStoreWithTimestamp(); err != nil {
panic(err) // Handle error
}
// exampleVerifier is an example of notation.Verifier given
// trust policy document and X509 trust store.
exampleVerifier, err := verifier.New(&examplePolicyDocument, truststore.NewX509TrustStore(dir.ConfigFS()), nil)
if err != nil {
panic(err) // Handle error
}
// exampleRepo is an example of registry.Repository.
remoteRepo, err := remote.NewRepository(exampleArtifactReference)
if err != nil {
panic(err) // Handle error
}
exampleRepo := registry.NewRepository(remoteRepo)
// exampleVerifyOptions is an example of notation.VerifyOptions.
exampleVerifyOptions := notation.VerifyOptions{
ArtifactReference: exampleArtifactReference,
MaxSignatureAttempts: 50,
}
// remote verify core process
// upon successful verification, the target manifest descriptor
// and signature verification outcome are returned.
targetDesc, _, err := notation.Verify(context.Background(), exampleVerifier, exampleRepo, exampleVerifyOptions)
if err != nil {
panic(err) // Handle error
}
fmt.Println("Successfully verified")
fmt.Println("targetDesc MediaType:", targetDesc.MediaType)
fmt.Println("targetDesc Digest:", targetDesc.Digest)
fmt.Println("targetDesc Size:", targetDesc.Size)
}
func generateTrustStoreWithTimestamp() error {
// changing the path of the trust store for demo purpose.
// Users could keep the default value, i.e. os.UserConfigDir.
dir.UserConfigDir = "tmp"
// an example of a valid X509 self-signed certificate for demo purpose ONLY.
// Users should replace `exampleX509Certificate` with their own trusted
// certificate and add to the trust store, following the
// Notary Project certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
MA0GA1UEAxMGYWxwaW5lMCAXDTAwMDgyOTEzNTAwMFoYDzIxMjMwODI5MTM1MDAw
WjBOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUx
DzANBgNVBAoTBk5vdGFyeTEPMA0GA1UEAxMGYWxwaW5lMIIBIjANBgkqhkiG9w0B
AQEFAAOCAQ8AMIIBCgKCAQEAocg3qEsyNDDLfB8OHD4dhi+M1NPK1Asy5NX84c+g
vacZuoPLTwmpOfm6nPt7GPPB9G7S6xxhFNbRxTYfYUjK+kaCj38XjBRf5lGewbSJ
KVkxQ82/axU70ceSW3JpazrageN9JUTZ/Jfi4MfnipITwcmMoiij8eGrHskjyVyZ
bJd0WMMKRDWVhLPUiPMVWt/4d7YtZItzacaQKtXmXgsTCTWpIols3gftNYjrQoMs
UelUdD8vOAWN9J28/SyC+uSh/K1KfyUlbqufn4di8DEBxntP5wnXYbJL1jtjsUgE
xAVjQxT1zI59X36m3t3YKqCQh1cud02L5onObY6zj57N6QIDAQABoycwJTAOBgNV
HQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQAD
ggEBAC8AjBLy7EsRpi6oguCdFSb6nRGjvF17N+b6mDb3sARnB8T1pxvzTT26ya+A
yWR+jjodEwbMIS+13lV+9qT2LwqlbOUNY519Pa2GRRY72JjeowWI3iKkKaMzfZUB
7lRTGXdEuZApLbTO/3JVcR9ffu00N1UaAP9YGElSt4JDJYA9M+d/Qto+HiIsE0Kj
+jdnwIYovPPOlryKOLfFb/r1GEq7n63xFZz83iyWNaZdsJ5N3YHxdOpkbBbCalOE
BDJTjQKqeAYBLoANNU0OBslmqHCSBTEnhbqJHN6QKyF09ScOl5LwM1QsTl0UY5si
GLAfj/jSf9OH9VLTPHOS8/N0Ka4=
-----END CERTIFICATE-----`
// Adding the certificate into the trust store.
if err := os.MkdirAll("tmp/truststore/x509/ca/valid-trust-store", 0700); err != nil {
return err
}
if err := os.WriteFile("tmp/truststore/x509/ca/valid-trust-store/NotationExample.pem", []byte(exampleX509Certificate), 0600); err != nil {
return err
}
// an example of a valid TSA root certificate for demo purpose ONLY.
// Users should replace `exampleTSARootCertificate` with their own trusted
// TSA root certificate and add to the trust store, following the
// Notary Project certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleTSARootCertificate := `-----BEGIN CERTIFICATE-----
MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg
RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV
UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu
Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG
SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y
ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If
xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV
ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO
DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ
jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/
CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi
EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM
fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY
uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK
chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t
9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB
hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD
ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2
SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd
+SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc
fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa
sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N
cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N
0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie
4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI
r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1
/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm
gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+
-----END CERTIFICATE-----`
// Adding the tsa root certificate into the trust store.
if err := os.MkdirAll("tmp/truststore/x509/tsa/valid-tsa", 0700); err != nil {
return err
}
return os.WriteFile("tmp/truststore/x509/tsa/valid-tsa/NotationTSAExample.pem", []byte(exampleTSARootCertificate), 0600)
}

25
go.mod
View File

@ -1,25 +1,26 @@
module github.com/notaryproject/notation-go
go 1.21
go 1.23.0
require (
github.com/go-ldap/ldap/v3 v3.4.8
github.com/notaryproject/notation-core-go v1.0.3
github.com/go-ldap/ldap/v3 v3.4.11
github.com/notaryproject/notation-core-go v1.3.0
github.com/notaryproject/notation-plugin-framework-go v1.0.0
github.com/notaryproject/tspclient-go v1.0.0
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.0
github.com/veraison/go-cose v1.1.0
golang.org/x/crypto v0.23.0
golang.org/x/mod v0.17.0
oras.land/oras-go/v2 v2.5.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.6.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // 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.6.0 // indirect
golang.org/x/sync v0.14.0 // indirect
)

108
go.sum
View File

@ -2,22 +2,18 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA=
github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/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/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
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=
@ -32,85 +28,33 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6
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.0.3 h1:FCgvULSypEFrrNgvDRdHbKAGAgbXK43n/jKD9q2WECA=
github.com/notaryproject/notation-core-go v1.0.3/go.mod h1:eDo5/LTUp23mB7w0CckJLnl+p93oGdyiKDzzggpqTH4=
github.com/notaryproject/notation-core-go v1.3.0 h1:mWJaw1QBpBxpjLSiKOjzbZvB+xh2Abzk14FHWQ+9Kfs=
github.com/notaryproject/notation-core-go v1.3.0/go.mod h1:hzvEOit5lXfNATGNBT8UQRx2J6Fiw/dq/78TQL8aE64=
github.com/notaryproject/notation-plugin-framework-go v1.0.0 h1:6Qzr7DGXoCgXEQN+1gTZWuJAZvxh3p8Lryjn5FaLzi4=
github.com/notaryproject/notation-plugin-framework-go v1.0.0/go.mod h1:RqWSrTOtEASCrGOEffq0n8pSg2KOgKYiWqFWczRSics=
github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4=
github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
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.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o=
github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4=
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=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
oras.land/oras-go/v2 v2.5.0/go.mod h1:z4eisnLP530vwIOUOJeBIj0aGI0L1C3d53atvCBqZHg=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=

View File

@ -15,6 +15,7 @@ package file
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
@ -23,6 +24,11 @@ import (
"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")
@ -110,3 +116,38 @@ func CopyDirToDir(src, dst string) error {
func TrimFileExtension(fileName string) string {
return strings.TrimSuffix(fileName, filepath.Ext(fileName))
}
// WriteFile writes content to a temporary file and moves it to path.
// If path already exists and is a file, WriteFile overwrites it.
//
// Parameters:
// - tempDir is the directory to create the temporary file. It should be
// in the same mount point as path. If tempDir is empty, the default
// directory for temporary files is used.
// - path is the destination file path.
// - content is the content to write.
func WriteFile(tempDir, path string, content []byte) (writeErr error) {
tempFile, err := os.CreateTemp(tempDir, tempFileNamePrefix)
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer func() {
// remove the temp file in case of error
if writeErr != nil {
tempFile.Close()
os.Remove(tempFile.Name())
}
}()
if _, err := tempFile.Write(content); err != nil {
return fmt.Errorf("failed to write content to temp file: %w", err)
}
// close before moving
if err := tempFile.Close(); err != nil {
return fmt.Errorf("failed to close temp file: %w", err)
}
// rename is atomic on UNIX-like platforms
return os.Rename(tempFile.Name(), path)
}

View File

@ -18,6 +18,7 @@ import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
@ -26,7 +27,10 @@ func TestCopyToDir(t *testing.T) {
tempDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
t.Fatal(err)
}
if err := WriteFile(tempDir, filename, data); err != nil {
t.Fatal(err)
}
@ -45,7 +49,10 @@ func TestCopyToDir(t *testing.T) {
destDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
t.Fatal(err)
}
if err := WriteFile(tempDir, filename, data); err != nil {
t.Fatal(err)
}
@ -77,7 +84,10 @@ func TestCopyToDir(t *testing.T) {
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
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
@ -100,7 +110,10 @@ func TestCopyToDir(t *testing.T) {
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
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
@ -123,7 +136,10 @@ func TestCopyToDir(t *testing.T) {
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
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
@ -140,7 +156,10 @@ func TestCopyToDir(t *testing.T) {
tempDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := writeFile(filename, data); err != nil {
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
t.Fatal(err)
}
if err := WriteFile(tempDir, filename, data); err != nil {
t.Fatal(err)
}
@ -161,6 +180,29 @@ func TestFileNameWithoutExtension(t *testing.T) {
}
}
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 {
@ -170,10 +212,3 @@ func validFileContent(t *testing.T, filename string, content []byte) {
t.Fatal("file content is not correct")
}
}
func writeFile(path string, data []byte) error {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}

View File

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

View File

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

View File

@ -15,6 +15,7 @@ package ocilayout
import (
"os"
"runtime"
"testing"
)
@ -26,7 +27,10 @@ func TestCopy(t *testing.T) {
}
})
t.Run("invalid target path", func(t *testing.T) {
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 {

View File

@ -0,0 +1,24 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pkix
import (
"testing"
)
func FuzzParseDistinguishedName(f *testing.F) {
f.Fuzz(func(t *testing.T, name string) {
_, _ = ParseDistinguishedName(name)
})
}

View File

@ -26,11 +26,10 @@ func ParseDistinguishedName(name string) (map[string]string, error) {
return nil, fmt.Errorf("unsupported distinguished name (DN) %q: notation does not support x509.subject identities containing \"=#\"", name)
}
mandatoryFields := []string{"C", "ST", "O"}
attrKeyValue := make(map[string]string)
dn, err := ldapv3.ParseDN(name)
if err != nil {
return nil, fmt.Errorf("parsing distinguished name (DN) %q failed with err: %v. A valid DN must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name, err)
return nil, fmt.Errorf("parsing distinguished name (DN) %q failed with err: %v. A valid DN must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard", name, err)
}
for _, rdn := range dn.RDNs {
@ -39,6 +38,10 @@ func ParseDistinguishedName(name string) (map[string]string, error) {
return nil, fmt.Errorf("distinguished name (DN) %q has multi-valued RDN attributes, remove multi-valued RDN attributes as they are not supported", name)
}
for _, attribute := range rdn.Attributes {
// stateOrProvince name 'S' is an alias for 'ST'
if attribute.Type == "S" {
attribute.Type = "ST"
}
if attrKeyValue[attribute.Type] == "" {
attrKeyValue[attribute.Type] = attribute.Value
} else {
@ -48,11 +51,13 @@ func ParseDistinguishedName(name string) (map[string]string, error) {
}
// Verify mandatory fields are present
mandatoryFields := []string{"C", "ST", "O"}
for _, field := range mandatoryFields {
if attrKeyValue[field] == "" {
return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum", name, field)
return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum", name, field)
}
}
// No errors
return attrKeyValue, nil
}

143
internal/pkix/pkix_test.go Normal file
View File

@ -0,0 +1,143 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pkix
import "testing"
func TestParseDistinguishedName(t *testing.T) {
// Test cases
tests := []struct {
name string
input string
wantErr bool
}{
{
name: "valid DN",
input: "C=US,ST=California,O=Notary Project",
wantErr: false,
},
{
name: "valid DN with State alias",
input: "C=US,S=California,O=Notary Project",
wantErr: false,
},
{
name: "invalid DN",
input: "C=US,ST=California",
wantErr: true,
},
{
name: "invalid DN without State",
input: "C=US,O=Notary Project",
wantErr: true,
},
{
name: "invalid DN without State",
input: "invalid",
wantErr: true,
},
{
name: "duplicate RDN attribute",
input: "C=US,ST=California,O=Notary Project,S=California",
wantErr: true,
},
{
name: "unsupported DN =#",
input: "C=US,ST=California,O=Notary Project=#",
wantErr: true,
},
{
name: "multi-valued RDN attributes",
input: "OU=Sales+CN=J. Smith,DC=example,DC=net",
wantErr: true,
},
}
// Run tests
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseDistinguishedName(tt.input)
if tt.wantErr != (err != nil) {
t.Errorf("ParseDistinguishedName() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestIsSubsetDN(t *testing.T) {
// Test cases
tests := []struct {
name string
dn1 map[string]string
dn2 map[string]string
want bool
}{
{
name: "subset DN",
dn1: map[string]string{
"C": "US",
"ST": "California",
"O": "Notary Project",
},
dn2: map[string]string{
"C": "US",
"ST": "California",
"O": "Notary Project",
"L": "Los Angeles",
},
want: true,
},
{
name: "not subset DN",
dn1: map[string]string{
"C": "US",
"ST": "California",
"O": "Notary Project",
},
dn2: map[string]string{
"C": "US",
"ST": "California",
"O": "Notary Project 2",
"L": "Los Angeles",
"CN": "Notary",
},
want: false,
},
{
name: "not subset DN 2",
dn1: map[string]string{
"C": "US",
"ST": "California",
"O": "Notary Project",
"CN": "Notary",
},
dn2: map[string]string{
"C": "US",
"ST": "California",
"O": "Notary Project",
"L": "Los Angeles",
},
want: false,
},
}
// Run tests
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsSubsetDN(tt.dn1, tt.dn2); got != tt.want {
t.Errorf("IsSubsetDN() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -12,12 +12,13 @@
// limitations under the License.
// Package notation provides signer and verifier for notation Sign
// and Verification.
// and Verification. It supports both OCI artifact and arbitrary blob.
package notation
import (
"context"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/json"
"errors"
@ -30,6 +31,7 @@ import (
orasRegistry "oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-core-go/signature/cose"
"github.com/notaryproject/notation-core-go/signature/jws"
@ -37,6 +39,7 @@ import (
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/tspclient-go"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
@ -45,7 +48,7 @@ var errDoneVerification = errors.New("done verification")
var reservedAnnotationPrefixes = [...]string{"io.cncf.notary"}
// SignerSignOptions contains parameters for Signer.Sign.
// SignerSignOptions contains parameters for [Signer] and [BlobSigner].
type SignerSignOptions struct {
// SignatureMediaType is the envelope type of the signature.
// Currently, both `application/jose+json` and `application/cose` are
@ -61,6 +64,17 @@ type SignerSignOptions struct {
// SigningAgent sets the signing agent name
SigningAgent string
// Timestamper denotes the timestamper for RFC 3161 timestamping
Timestamper tspclient.Timestamper
// TSARootCAs is the cert pool holding caller's TSA trust anchor
TSARootCAs *x509.CertPool
// TSARevocationValidator is used for validating revocation status of
// timestamping certificate chain with context during signing.
// When present, only used when timestamping is performed.
TSARevocationValidator revocation.Validator
}
// Signer is a generic interface for signing an OCI artifact.
@ -72,25 +86,35 @@ type Signer interface {
Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error)
}
// SignBlobOptions contains parameters for notation.SignBlob.
// SignBlobOptions contains parameters for [notation.SignBlob].
type SignBlobOptions struct {
SignerSignOptions
// ContentMediaType is the media-type of the blob being signed.
ContentMediaType string
// UserMetadata contains key-value pairs that are added to the signature
// payload
UserMetadata map[string]string
}
// BlobDescriptorGenerator creates descriptor using the digest Algorithm.
// Below is the example of minimal descriptor, it must contain mediatype,
// digest and size of the artifact.
//
// {
// "mediaType": "application/octet-stream",
// "digest": "sha256:2f3a23b6373afb134ddcd864be8e037e34a662d090d33ee849471ff73c873345",
// "size": 1024
// }
type BlobDescriptorGenerator func(digest.Algorithm) (ocispec.Descriptor, error)
// BlobSigner is a generic interface for signing arbitrary data.
// The interface allows signing with local or remote keys,
// and packing in various signature formats.
type BlobSigner interface {
// SignBlob signs the descriptor returned by genDesc ,
// and returns the signature and SignerInfo
// SignBlob signs the descriptor returned by genDesc, and returns the
// signature and SignerInfo.
SignBlob(ctx context.Context, genDesc BlobDescriptorGenerator, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error)
}
@ -101,7 +125,7 @@ type signerAnnotation interface {
PluginAnnotations() map[string]string
}
// SignOptions contains parameters for notation.Sign.
// SignOptions contains parameters for [notation.Sign].
type SignOptions struct {
SignerSignOptions
@ -116,13 +140,30 @@ type SignOptions struct {
// Sign signs the OCI artifact and push the signature to the Repository.
// The descriptor of the sign content is returned upon successful signing.
//
// Deprecated: use [SignOCI] instead.
func Sign(ctx context.Context, signer Signer, repo registry.Repository, signOpts SignOptions) (ocispec.Descriptor, error) {
artifactMenifestDesc, _, err := SignOCI(ctx, signer, repo, signOpts)
return artifactMenifestDesc, err
}
// SignOCI signs the OCI artifact and push the signature to the Repository.
//
// Both artifact and signature manifest descriptors are returned upon successful
// signing.
//
// Note: If the error type is [remote.ReferrersError] and
// referrerError.IsReferrersIndexDelete() returns true, the signature is
// successfully pushed to the repository, but the referrers index deletion
// failed. In this case, the artifact and signature manifest descriptors are
// returned with the error.
func SignOCI(ctx context.Context, signer Signer, repo registry.Repository, signOpts SignOptions) (artifactManifestDesc, sigManifestDesc ocispec.Descriptor, err error) {
// sanity check
if err := validateSignArguments(signer, signOpts.SignerSignOptions); err != nil {
return ocispec.Descriptor{}, err
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
}
if repo == nil {
return ocispec.Descriptor{}, errors.New("repo cannot be nil")
return ocispec.Descriptor{}, ocispec.Descriptor{}, errors.New("repo cannot be nil")
}
logger := log.GetLogger(ctx)
@ -131,71 +172,72 @@ func Sign(ctx context.Context, signer Signer, repo registry.Repository, signOpts
// artifactRef is a valid full reference
artifactRef = ref.Reference
}
targetDesc, err := repo.Resolve(ctx, artifactRef)
artifactManifestDesc, err = repo.Resolve(ctx, artifactRef)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to resolve reference: %w", err)
return ocispec.Descriptor{}, ocispec.Descriptor{}, fmt.Errorf("failed to resolve reference: %w", err)
}
// artifactRef is a tag or a digest, if it's a digest it has to match
// the resolved digest
if artifactRef != targetDesc.Digest.String() {
if artifactRef != artifactManifestDesc.Digest.String() {
if _, err := digest.Parse(artifactRef); err == nil {
// artifactRef is a digest, but does not match the resolved digest
return ocispec.Descriptor{}, fmt.Errorf("user input digest %s does not match the resolved digest %s", artifactRef, targetDesc.Digest.String())
return ocispec.Descriptor{}, ocispec.Descriptor{}, fmt.Errorf("user input digest %s does not match the resolved digest %s", artifactRef, artifactManifestDesc.Digest.String())
}
// artifactRef is a tag
logger.Warnf("Always sign the artifact using digest(`@sha256:...`) rather than a tag(`:%s`) because tags are mutable and a tag reference can point to a different artifact than the one signed", artifactRef)
logger.Infof("Resolved artifact tag `%s` to digest `%s` before signing", artifactRef, targetDesc.Digest.String())
logger.Infof("Resolved artifact tag `%s` to digest `%v` before signing", artifactRef, artifactManifestDesc.Digest)
}
descToSign, err := addUserMetadataToDescriptor(ctx, targetDesc, signOpts.UserMetadata)
descToSign, err := addUserMetadataToDescriptor(ctx, artifactManifestDesc, signOpts.UserMetadata)
if err != nil {
return ocispec.Descriptor{}, err
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
}
sig, signerInfo, err := signer.Sign(ctx, descToSign, signOpts.SignerSignOptions)
if err != nil {
return ocispec.Descriptor{}, err
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
}
var pluginAnnotations map[string]string
if signerAnts, ok := signer.(signerAnnotation); ok {
pluginAnnotations = signerAnts.PluginAnnotations()
}
logger.Debug("Generating annotation")
annotations, err := generateAnnotations(signerInfo, pluginAnnotations)
if err != nil {
return ocispec.Descriptor{}, err
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
}
logger.Debugf("Generated annotations: %+v", annotations)
logger.Debugf("Pushing signature of artifact descriptor: %+v, signature media type: %v", targetDesc, signOpts.SignatureMediaType)
_, _, err = repo.PushSignature(ctx, signOpts.SignatureMediaType, sig, targetDesc, annotations)
logger.Debugf("Pushing signature of artifact descriptor: %+v, signature media type: %v", artifactManifestDesc, signOpts.SignatureMediaType)
_, sigManifestDesc, err = repo.PushSignature(ctx, signOpts.SignatureMediaType, sig, artifactManifestDesc, annotations)
if err != nil {
var referrerError *remote.ReferrersError
// do not log an error for failing to delete referral index
if !errors.As(err, &referrerError) || !referrerError.IsReferrersIndexDelete() {
logger.Error("Failed to push the signature")
if errors.As(err, &referrerError) && referrerError.IsReferrersIndexDelete() {
// return the descriptors for referrersIndexDelete error as
// the signature is successfully pushed to the repository
return artifactManifestDesc, sigManifestDesc, err
}
return ocispec.Descriptor{}, ErrorPushSignatureFailed{Msg: err.Error()}
logger.Error("Failed to push the signature")
return ocispec.Descriptor{}, ocispec.Descriptor{}, ErrorPushSignatureFailed{Msg: err.Error()}
}
return targetDesc, nil
return artifactManifestDesc, sigManifestDesc, nil
}
// SignBlob signs the arbitrary data and returns the signature
// SignBlob signs the arbitrary data from blobReader and returns
// the signature and SignerInfo.
func SignBlob(ctx context.Context, signer BlobSigner, blobReader io.Reader, signBlobOpts SignBlobOptions) ([]byte, *signature.SignerInfo, error) {
// sanity checks
if err := validateSignArguments(signer, signBlobOpts.SignerSignOptions); err != nil {
return nil, nil, err
}
if blobReader == nil {
return nil, nil, errors.New("blobReader cannot be nil")
}
if signBlobOpts.ContentMediaType == "" {
return nil, nil, errors.New("content media-type cannot be empty")
}
if _, _, err := mime.ParseMediaType(signBlobOpts.ContentMediaType); err != nil {
return nil, nil, fmt.Errorf("invalid content media-type '%s': %v", signBlobOpts.ContentMediaType, err)
if err := validateContentMediaType(signBlobOpts.ContentMediaType); err != nil {
return nil, nil, err
}
getDescFunc := getDescriptorFunc(ctx, blobReader, signBlobOpts.ContentMediaType, signBlobOpts.UserMetadata)
@ -215,37 +257,29 @@ func validateSignArguments(signer any, signOpts SignerSignOptions) error {
if signOpts.SignatureMediaType == "" {
return errors.New("signature media-type cannot be empty")
}
if !(signOpts.SignatureMediaType == jws.MediaTypeEnvelope || signOpts.SignatureMediaType == cose.MediaTypeEnvelope) {
return fmt.Errorf("invalid signature media-type '%s'", signOpts.SignatureMediaType)
if err := validateSigMediaType(signOpts.SignatureMediaType); err != nil {
return err
}
return nil
}
func addUserMetadataToDescriptor(ctx context.Context, desc ocispec.Descriptor, userMetadata map[string]string) (ocispec.Descriptor, error) {
logger := log.GetLogger(ctx)
if desc.Annotations == nil && len(userMetadata) > 0 {
desc.Annotations = map[string]string{}
}
for k, v := range userMetadata {
logger.Debugf("Adding metadata %v=%v to annotations", k, v)
for _, reservedPrefix := range reservedAnnotationPrefixes {
if strings.HasPrefix(k, reservedPrefix) {
return desc, fmt.Errorf("error adding user metadata: metadata key %v has reserved prefix %v", k, reservedPrefix)
}
}
if _, ok := desc.Annotations[k]; ok {
return desc, fmt.Errorf("error adding user metadata: metadata key %v is already present in the target artifact", k)
}
desc.Annotations[k] = v
}
return desc, nil
}
@ -287,6 +321,7 @@ type VerificationOutcome struct {
Error error
}
// UserMetadata returns the user metadata from the signature envelope.
func (outcome *VerificationOutcome) UserMetadata() (map[string]string, error) {
if outcome.EnvelopeContent == nil {
return nil, errors.New("unable to find envelope content for verification outcome")
@ -297,22 +332,21 @@ func (outcome *VerificationOutcome) UserMetadata() (map[string]string, error) {
if err != nil {
return nil, errors.New("failed to unmarshal the payload content in the signature blob to envelope.Payload")
}
if payload.TargetArtifact.Annotations == nil {
return map[string]string{}, nil
}
return payload.TargetArtifact.Annotations, nil
}
// VerifierVerifyOptions contains parameters for Verifier.Verify.
// VerifierVerifyOptions contains parameters for [Verifier.Verify] used for
// verifying OCI artifact.
type VerifierVerifyOptions struct {
// ArtifactReference is the reference of the artifact that is being
// verified against to. It must be a full reference.
ArtifactReference string
// SignatureMediaType is the envelope type of the signature.
// Currently both `application/jose+json` and `application/cose` are
// Currently only `application/jose+json` and `application/cose` are
// supported.
SignatureMediaType string
@ -324,22 +358,49 @@ type VerifierVerifyOptions struct {
UserMetadata map[string]string
}
// Verifier is a generic interface for verifying an artifact.
// Verifier is a generic interface for verifying an OCI artifact.
type Verifier interface {
// Verify verifies the signature blob `signature` against the target OCI
// artifact with manifest descriptor `desc`, and returns the outcome upon
// Verify verifies the `signature` associated with the target OCI artifact
// with manifest descriptor `desc`, and returns the outcome upon
// successful verification.
// If nil signature is present and the verification level is not 'skip',
// an error will be returned.
Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifierVerifyOptions) (*VerificationOutcome, error)
}
// BlobVerifierVerifyOptions contains parameters for [BlobVerifier.Verify].
type BlobVerifierVerifyOptions struct {
// SignatureMediaType is the envelope type of the signature.
// Currently only `application/jose+json` and `application/cose` are
// supported.
SignatureMediaType string
// PluginConfig is a map of plugin configs.
PluginConfig map[string]string
// UserMetadata contains key-value pairs that must be present in the
// signature.
UserMetadata map[string]string
// TrustPolicyName is the name of trust policy picked by caller.
// If empty, the global trust policy will be applied.
TrustPolicyName string
}
// BlobVerifier is a generic interface for verifying a blob.
type BlobVerifier interface {
// VerifyBlob verifies the `signature` against the target blob using the
// descriptor returned by descGenFunc parameter and
// returns the outcome upon successful verification.
VerifyBlob(ctx context.Context, descGenFunc BlobDescriptorGenerator, signature []byte, opts BlobVerifierVerifyOptions) (*VerificationOutcome, error)
}
type verifySkipper interface {
// SkipVerify validates whether the verification level is skip.
SkipVerify(ctx context.Context, opts VerifierVerifyOptions) (bool, *trustpolicy.VerificationLevel, error)
}
// VerifyOptions contains parameters for notation.Verify.
// VerifyOptions contains parameters for [notation.Verify].
type VerifyOptions struct {
// ArtifactReference is the reference of the artifact that is being
// verified against to.
@ -358,8 +419,51 @@ type VerifyOptions struct {
UserMetadata map[string]string
}
// VerifyBlobOptions contains parameters for [notation.VerifyBlob].
type VerifyBlobOptions struct {
BlobVerifierVerifyOptions
// ContentMediaType is the media-type type of the content being verified.
ContentMediaType string
}
// VerifyBlob performs signature verification for a blob using notation supported
// verification types (like integrity, authenticity, etc.) and returns the
// successful signature verification outcome. The blob is read using blobReader,
// and upon successful verification, it returns the descriptor of the blob.
// For more details on signature verification, see
// https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification
func VerifyBlob(ctx context.Context, blobVerifier BlobVerifier, blobReader io.Reader, signature []byte, verifyBlobOpts VerifyBlobOptions) (ocispec.Descriptor, *VerificationOutcome, error) {
if blobVerifier == nil {
return ocispec.Descriptor{}, nil, errors.New("blobVerifier cannot be nil")
}
if blobReader == nil {
return ocispec.Descriptor{}, nil, errors.New("blobReader cannot be nil")
}
if len(signature) == 0 {
return ocispec.Descriptor{}, nil, errors.New("signature cannot be nil or empty")
}
if err := validateContentMediaType(verifyBlobOpts.ContentMediaType); err != nil {
return ocispec.Descriptor{}, nil, err
}
if err := validateSigMediaType(verifyBlobOpts.SignatureMediaType); err != nil {
return ocispec.Descriptor{}, nil, err
}
getDescFunc := getDescriptorFunc(ctx, blobReader, verifyBlobOpts.ContentMediaType, verifyBlobOpts.UserMetadata)
vo, err := blobVerifier.VerifyBlob(ctx, getDescFunc, signature, verifyBlobOpts.BlobVerifierVerifyOptions)
if err != nil {
return ocispec.Descriptor{}, nil, err
}
var desc ocispec.Descriptor
if err = json.Unmarshal(vo.EnvelopeContent.Payload.Content, &desc); err != nil {
return ocispec.Descriptor{}, nil, err
}
return desc, vo, nil
}
// Verify performs signature verification on each of the notation supported
// verification types (like integrity, authenticity, etc.) and return the
// verification types (like integrity, authenticity, etc.) and returns the
// successful signature verification outcome.
// For more details on signature verification, see
// https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#signature-verification
@ -383,7 +487,6 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
PluginConfig: verifyOpts.PluginConfig,
UserMetadata: verifyOpts.UserMetadata,
}
if skipChecker, ok := verifier.(verifySkipper); ok {
logger.Info("Checking whether signature verification should be skipped or not")
skip, verificationLevel, err := skipChecker.SkipVerify(ctx, opts)
@ -391,10 +494,10 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
return ocispec.Descriptor{}, nil, err
}
if skip {
logger.Infoln("Verification skipped for", verifyOpts.ArtifactReference)
logger.Infoln("Signature verification skipped for", verifyOpts.ArtifactReference)
return ocispec.Descriptor{}, []*VerificationOutcome{{VerificationLevel: verificationLevel}}, nil
}
logger.Info("Check over. Trust policy is not configured to skip signature verification")
logger.Info("Check over. The signature verification level is not set to 'skip' in the trust policy.")
}
// get artifact descriptor
@ -412,7 +515,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
}
if ref.ValidateReferenceAsDigest() != nil {
// artifactRef is not a digest reference
logger.Infof("Resolved artifact tag `%s` to digest `%s` before verification", ref.Reference, artifactDescriptor.Digest.String())
logger.Infof("Resolved artifact tag `%s` to digest `%v` before verification", ref.Reference, artifactDescriptor.Digest)
logger.Warn("The resolved digest may not point to the same signed artifact, since tags are mutable")
} else if ref.Reference != artifactDescriptor.Digest.String() {
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("user input digest %s does not match the resolved digest %s", ref.Reference, artifactDescriptor.Digest.String())}
@ -457,6 +560,7 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
}
// at this point, the signature is verified successfully
verificationSucceeded = true
// on success, verificationOutcomes only contains the
// succeeded outcome
verificationOutcomes = []*VerificationOutcome{outcome}
@ -465,14 +569,11 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
// early break on success
return errDoneVerification
}
if numOfSignatureProcessed >= verifyOpts.MaxSignatureAttempts {
return errExceededMaxVerificationLimit
}
return nil
})
if err != nil && !errors.Is(err, errDoneVerification) {
if errors.Is(err, errExceededMaxVerificationLimit) {
return ocispec.Descriptor{}, verificationOutcomes, err
@ -536,3 +637,19 @@ func getDescriptorFunc(ctx context.Context, reader io.Reader, contentMediaType s
return addUserMetadataToDescriptor(ctx, targetDesc, userMetadata)
}
}
func validateContentMediaType(contentMediaType string) error {
if contentMediaType != "" {
if _, _, err := mime.ParseMediaType(contentMediaType); err != nil {
return fmt.Errorf("invalid content media-type %q: %v", contentMediaType, err)
}
}
return nil
}
func validateSigMediaType(sigMediaType string) error {
if !(sigMediaType == jws.MediaTypeEnvelope || sigMediaType == cose.MediaTypeEnvelope) {
return fmt.Errorf("invalid signature media-type %q", sigMediaType)
}
return nil
}

View File

@ -26,6 +26,8 @@ import (
"testing"
"time"
"oras.land/oras-go/v2/registry/remote"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-core-go/signature/cose"
"github.com/notaryproject/notation-core-go/signature/jws"
@ -37,7 +39,6 @@ import (
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/registry/remote"
)
var expectedMetadata = map[string]string{"foo": "bar", "bar": "foo"}
@ -117,7 +118,7 @@ func TestSignBlobError(t *testing.T) {
}{
{"negativeExpiry", &dummySigner{}, -1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration cannot be a negative value"},
{"milliSecExpiry", &dummySigner{}, 1 * time.Millisecond, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration supports minimum granularity of seconds"},
{"invalidContentMediaType", &dummySigner{}, 1 * time.Second, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type 'video/mp4/zoping': mime: unexpected content after media subtype"},
{"invalidContentMediaType", &dummySigner{}, 1 * time.Second, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type \"video/mp4/zoping\": mime: unexpected content after media subtype"},
{"emptyContentMediaType", &dummySigner{}, 1 * time.Second, reader, "", jws.MediaTypeEnvelope, "content media-type cannot be empty"},
{"invalidSignatureMediaType", &dummySigner{}, 1 * time.Second, reader, "", "", "content media-type cannot be empty"},
{"nilReader", &dummySigner{}, 1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"},
@ -304,9 +305,9 @@ func TestSignOptsUnknownMediaType(t *testing.T) {
}
func TestRegistryResolveError(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
errorMessage := "network error"
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
@ -322,9 +323,9 @@ func TestRegistryResolveError(t *testing.T) {
}
func TestVerifyEmptyReference(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
errorMessage := "reference is missing digest or tag"
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
@ -338,9 +339,9 @@ func TestVerifyEmptyReference(t *testing.T) {
}
func TestVerifyTagReferenceFailed(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
errorMessage := "invalid reference: invalid repository \"UPPERCASE/test\""
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
@ -354,10 +355,10 @@ func TestVerifyTagReferenceFailed(t *testing.T) {
}
func TestVerifyDigestNotMatchResolve(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
repo.MissMatchDigest = true
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
errorMessage := fmt.Sprintf("user input digest %s does not match the resolved digest %s", mock.SampleDigest, mock.ZeroDigest)
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
@ -381,7 +382,7 @@ func TestSignDigestNotMatchResolve(t *testing.T) {
}
errorMessage := fmt.Sprintf("user input digest %s does not match the resolved digest %s", mock.SampleDigest, mock.ZeroDigest)
expectedErr := fmt.Errorf(errorMessage)
expectedErr := errors.New(errorMessage)
_, err := Sign(context.Background(), &dummySigner{}, repo, signOpts)
if err == nil || err.Error() != errorMessage {
@ -390,9 +391,9 @@ func TestSignDigestNotMatchResolve(t *testing.T) {
}
func TestSkippedSignatureVerification(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelSkip}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelSkip, false}
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, outcomes, err := Verify(context.Background(), &verifier, repo, opts)
@ -403,9 +404,9 @@ func TestSkippedSignatureVerification(t *testing.T) {
}
func TestRegistryNoSignatureManifests(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
errorMessage := fmt.Sprintf("no signature is associated with %q, make sure the artifact was signed successfully", mock.SampleArtifactUri)
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
@ -420,9 +421,9 @@ func TestRegistryNoSignatureManifests(t *testing.T) {
}
func TestRegistryFetchSignatureBlobError(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
errorMessage := fmt.Sprintf("unable to retrieve digital signature with digest %q associated with %q from the Repository, error : network error", mock.SampleDigest, mock.SampleArtifactUri)
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
@ -437,23 +438,37 @@ func TestRegistryFetchSignatureBlobError(t *testing.T) {
}
func TestVerifyValid(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
// mock the repository
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), &verifier, repo, opts)
if err != nil {
t.Fatalf("SignaureMediaTypeMismatch expected: %v got: %v", nil, err)
t.Fatalf("expected nil error, but got: %v", err)
}
}
func TestVerifySkip(t *testing.T) {
repo := mock.NewRepository()
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, true}
// mock the repository
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), &verifier, repo, opts)
if err != nil {
t.Fatalf("expected nil error, but got: %v", err)
}
}
func TestMaxSignatureAttemptsMissing(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
expectedErr := ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("verifyOptions.MaxSignatureAttempts expects a positive number, got %d", 0)}
// mock the repository
@ -466,10 +481,11 @@ func TestMaxSignatureAttemptsMissing(t *testing.T) {
}
func TestExceededMaxSignatureAttempts(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
repo.ExceededNumOfSignatures = true
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict, false}
expectedErr := ErrorVerificationFailed{Msg: fmt.Sprintf("signature evaluation stopped. The configured limit of %d signatures to verify per artifact exceeded", 1)}
// mock the repository
@ -485,7 +501,7 @@ func TestVerifyFailed(t *testing.T) {
t.Run("verification error", func(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict}
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict, false}
expectedErr := ErrorVerificationFailed{}
// mock the repository
@ -512,7 +528,7 @@ func TestVerifyFailed(t *testing.T) {
t.Run("repo is nil", func(t *testing.T) {
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
expectedErr := errors.New("repo cannot be nil")
// mock the repository
@ -525,16 +541,73 @@ func TestVerifyFailed(t *testing.T) {
})
}
func dummyPolicyDocument() (policyDoc trustpolicy.Document) {
policyDoc = trustpolicy.Document{
func TestVerifyBlobError(t *testing.T) {
reader := strings.NewReader("some content")
sig := []byte("signature")
testCases := []struct {
name string
verifier BlobVerifier
sig []byte
rdr io.Reader
ctMType string
sigMType string
errMsg string
}{
{"nilVerifier", nil, sig, reader, "video/mp4", jws.MediaTypeEnvelope, "blobVerifier cannot be nil"},
{"verifierError", &dummyVerifier{FailVerify: true}, sig, reader, "video/mp4", jws.MediaTypeEnvelope, "failed verify"},
{"nilSignature", &dummyVerifier{}, nil, reader, "video/mp4", jws.MediaTypeEnvelope, "signature cannot be nil or empty"},
{"emptySignature", &dummyVerifier{}, []byte{}, reader, "video/mp4", jws.MediaTypeEnvelope, "signature cannot be nil or empty"},
{"nilReader", &dummyVerifier{}, sig, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"},
{"invalidContentType", &dummyVerifier{}, sig, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type \"video/mp4/zoping\": mime: unexpected content after media subtype"},
{"invalidSigType", &dummyVerifier{}, sig, reader, "video/mp4", "hola!", "invalid signature media-type \"hola!\""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := VerifyBlobOptions{
BlobVerifierVerifyOptions: BlobVerifierVerifyOptions{
SignatureMediaType: tc.sigMType,
UserMetadata: nil,
TrustPolicyName: "",
},
ContentMediaType: tc.ctMType,
}
_, _, err := VerifyBlob(context.Background(), tc.verifier, tc.rdr, tc.sig, opts)
if err == nil {
t.Fatalf("expected error but didnt found")
}
if err.Error() != tc.errMsg {
t.Fatalf("expected err message to be '%s' but found '%s'", tc.errMsg, err.Error())
}
})
}
}
func TestVerifyBlobValid(t *testing.T) {
opts := VerifyBlobOptions{
BlobVerifierVerifyOptions: BlobVerifierVerifyOptions{
SignatureMediaType: jws.MediaTypeEnvelope,
UserMetadata: nil,
TrustPolicyName: "",
},
}
_, _, err := VerifyBlob(context.Background(), &dummyVerifier{}, strings.NewReader("some content"), []byte("signature"), opts)
if err != nil {
t.Fatalf("expected nil error, but got: %v", err)
}
}
func dummyPolicyDocument() (policyDoc trustpolicy.OCIDocument) {
policyDoc = trustpolicy.OCIDocument{
Version: "1.0",
TrustPolicies: []trustpolicy.TrustPolicy{dummyPolicyStatement()},
TrustPolicies: []trustpolicy.OCITrustPolicy{dummyPolicyStatement()},
}
return
}
func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
policyStatement = trustpolicy.TrustPolicy{
func dummyPolicyStatement() (policyStatement trustpolicy.OCITrustPolicy) {
policyStatement = trustpolicy.OCITrustPolicy{
Name: "test-statement-name",
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
@ -548,7 +621,7 @@ type dummySigner struct {
fail bool
}
func (s *dummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
func (s *dummySigner) Sign(_ context.Context, _ ocispec.Descriptor, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
return []byte("ABC"), &signature.SignerInfo{
SignedAttributes: signature.SignedAttributes{
SigningTime: time.Now(),
@ -575,7 +648,7 @@ func (s *dummySigner) SignBlob(_ context.Context, descGenFunc BlobDescriptorGene
type verifyMetadataSigner struct{}
func (s *verifyMetadataSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
func (s *verifyMetadataSigner) Sign(_ context.Context, desc ocispec.Descriptor, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
for k, v := range expectedMetadata {
if desc.Annotations[k] != v {
return nil, nil, errors.New("expected metadata not present in descriptor")
@ -589,13 +662,14 @@ func (s *verifyMetadataSigner) Sign(ctx context.Context, desc ocispec.Descriptor
}
type dummyVerifier struct {
TrustPolicyDoc *trustpolicy.Document
TrustPolicyDoc *trustpolicy.OCIDocument
PluginManager plugin.Manager
FailVerify bool
VerificationLevel trustpolicy.VerificationLevel
SkipVerification bool
}
func (v *dummyVerifier) Verify(ctx context.Context, desc ocispec.Descriptor, signature []byte, opts VerifierVerifyOptions) (*VerificationOutcome, error) {
func (v *dummyVerifier) Verify(_ context.Context, _ ocispec.Descriptor, _ []byte, _ VerifierVerifyOptions) (*VerificationOutcome, error) {
outcome := &VerificationOutcome{
VerificationResults: []*ValidationResult{},
VerificationLevel: &v.VerificationLevel,
@ -606,6 +680,29 @@ func (v *dummyVerifier) Verify(ctx context.Context, desc ocispec.Descriptor, sig
return outcome, nil
}
func (v *dummyVerifier) SkipVerify(_ context.Context, _ VerifierVerifyOptions) (bool, *trustpolicy.VerificationLevel, error) {
if v.SkipVerification {
return true, nil, nil
}
return false, nil, nil
}
func (v *dummyVerifier) VerifyBlob(_ context.Context, _ BlobDescriptorGenerator, _ []byte, _ BlobVerifierVerifyOptions) (*VerificationOutcome, error) {
if v.FailVerify {
return nil, errors.New("failed verify")
}
return &VerificationOutcome{
VerificationResults: []*ValidationResult{},
VerificationLevel: &v.VerificationLevel,
EnvelopeContent: &signature.EnvelopeContent{
Payload: signature.Payload{
Content: []byte("{}"),
},
},
}, nil
}
var (
reference = "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
artifactReference = "local/oci-layout@sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
@ -614,7 +711,7 @@ var (
type ociDummySigner struct{}
func (s *ociDummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
func (s *ociDummySigner) Sign(_ context.Context, _ ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
sigBlob, err := os.ReadFile(signaturePath)
if err != nil {
return nil, nil, err
@ -666,7 +763,7 @@ func TestLocalContent(t *testing.T) {
MaxSignatureAttempts: math.MaxInt64,
}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
// verify signatures inside the OCI layout folder
_, _, err = Verify(context.Background(), &verifier, repo, verifyOpts)
if err != nil {

View File

@ -35,7 +35,7 @@ type Manager interface {
List(ctx context.Context) ([]string, error)
}
// CLIManager implements Manager
// CLIManager implements [Manager]
type CLIManager struct {
pluginFS dir.SysFS
}

View File

@ -27,35 +27,43 @@ import (
"path/filepath"
"strings"
"github.com/notaryproject/notation-go/internal/io"
"github.com/notaryproject/notation-go/internal/slices"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/plugin/proto"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
)
// maxPluginOutputSize is the maximum size of the plugin output.
const maxPluginOutputSize = 64 * 1024 * 1024 // 64 MiB
var executor commander = &execCommander{} // for unit test
// GenericPlugin is the base requirement to be a plugin.
//
// Deprecated: GenericPlugin exists for historical compatibility and should not be used.
// To access GenericPlugin, use the notation-plugin-framework-go's plugin.GenericPlugin type.
type GenericPlugin = plugin.GenericPlugin
// SignPlugin defines the required methods to be a SignPlugin.
//
// Deprecated: SignPlugin exists for historical compatibility and should not be used.
// To access SignPlugin, use the notation-plugin-framework-go's plugin.SignPlugin type.
type SignPlugin = plugin.SignPlugin
// VerifyPlugin defines the required method to be a VerifyPlugin.
//
// Deprecated: VerifyPlugin exists for historical compatibility and should not be used.
// To access VerifyPlugin, use the notation-plugin-framework-go's plugin.VerifyPlugin type.
type VerifyPlugin = plugin.VerifyPlugin
// Plugin defines required methods to be a Plugin.
//
// Deprecated: Plugin exists for historical compatibility and should not be used.
// To access Plugin, use the notation-plugin-framework-go's plugin.Plugin type.
type Plugin = plugin.Plugin
// CLIPlugin implements Plugin interface to CLI plugins.
// CLIPlugin implements [Plugin] interface to CLI plugins.
type CLIPlugin struct {
name string
path string
@ -173,20 +181,21 @@ func run(ctx context.Context, pluginName string, pluginPath string, req plugin.R
if len(stderr) == 0 {
// if stderr is empty, it is possible that the plugin is not
// running properly.
logger.Errorf("failed to execute the %s command for plugin %s: %s", req.Command(), pluginName, err)
return &PluginExecutableFileError{
Msg: fmt.Sprintf("failed to execute the %s command for plugin %s", req.Command(), pluginName),
InnerError: err,
}
} else {
var re proto.RequestError
jsonErr := json.Unmarshal(stderr, &re)
if jsonErr != nil {
logger.Errorf("failed to execute the %s command for plugin %s: %s", req.Command(), pluginName, strings.TrimSuffix(string(stderr), "\n"))
return &PluginMalformedError{
Msg: fmt.Sprintf("failed to execute the %s command for plugin %s: %s", req.Command(), pluginName, strings.TrimSuffix(string(stderr), "\n")),
InnerError: jsonErr,
}
}
return fmt.Errorf("failed to execute the %s command for plugin %s: %w", req.Command(), pluginName, re)
logger.Errorf("failed to execute the %s command for plugin %s: %s: %w", req.Command(), pluginName, re.Code, re)
return re
}
}
@ -217,10 +226,15 @@ func (c execCommander) Output(ctx context.Context, name string, command plugin.C
var stdout, stderr bytes.Buffer
cmd := exec.CommandContext(ctx, name, string(command))
cmd.Stdin = bytes.NewReader(req)
cmd.Stderr = &stderr
cmd.Stdout = &stdout
// The limit writer will be handled by the caller in run() by comparing the
// bytes written with the expected length of the bytes.
cmd.Stderr = io.LimitWriter(&stderr, maxPluginOutputSize)
cmd.Stdout = io.LimitWriter(&stdout, maxPluginOutputSize)
err := cmd.Run()
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, stderr.Bytes(), fmt.Errorf("'%s %s' command execution timeout: %w", name, string(command), err)
}
return nil, stderr.Bytes(), err
}
return stdout.Bytes(), nil, nil

View File

@ -19,9 +19,11 @@ import (
"errors"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
"time"
"github.com/notaryproject/notation-go/plugin/proto"
)
@ -30,7 +32,7 @@ func TestGetMetadata(t *testing.T) {
t.Run("plugin error is in invalid json format", func(t *testing.T) {
exitErr := errors.New("unknown error")
stderr := []byte("sad")
expectedErrMsg := "failed to execute the get-plugin-metadata command for plugin test-plugin: sad"
expectedErrMsg := "invalid character 's' looking for beginning of value"
plugin := CLIPlugin{name: "test-plugin"}
executor = testCommander{stdout: nil, stderr: stderr, err: exitErr}
_, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
@ -55,7 +57,7 @@ func TestGetMetadata(t *testing.T) {
t.Run("plugin cause system error", func(t *testing.T) {
exitErr := errors.New("system error")
stderr := []byte("")
expectedErrMsg := "failed to execute the get-plugin-metadata command for plugin test-plugin"
expectedErrMsg := "system error"
plugin := CLIPlugin{name: "test-plugin"}
executor = testCommander{stdout: nil, stderr: stderr, err: exitErr}
_, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
@ -181,7 +183,7 @@ func TestValidateMetadata(t *testing.T) {
}
}
func TestNewCLIPlugin_PathError(t *testing.T) {
func TestNewCLIPlugin_Error(t *testing.T) {
ctx := context.Background()
t.Run("plugin directory exists without executable.", func(t *testing.T) {
p, err := NewCLIPlugin(ctx, "emptyplugin", "./testdata/plugins/emptyplugin/notation-emptyplugin")
@ -203,6 +205,25 @@ func TestNewCLIPlugin_PathError(t *testing.T) {
t.Errorf("NewCLIPlugin() plugin = %v, want nil", p)
}
})
t.Run("plugin timeout error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
expectedErrMsg := "'sleep 2' command execution timeout: signal: killed"
ctxWithTimout, cancel := context.WithTimeout(ctx, 10 * time.Millisecond)
defer cancel()
var twoSeconds proto.Command
twoSeconds = "2"
_, _, err := execCommander{}.Output(ctxWithTimout, "sleep", twoSeconds, nil);
if err == nil {
t.Errorf("execCommander{}.Output() expected error = %v, got nil", expectedErrMsg)
}
if err.Error() != expectedErrMsg {
t.Errorf("execCommander{}.Output() error = %v, want %v", err, expectedErrMsg)
}
})
}
func TestNewCLIPlugin_ValidError(t *testing.T) {

View File

@ -72,7 +72,7 @@ type RequestError struct {
}
func (e RequestError) Error() string {
return fmt.Sprintf("%s: %v", e.Code, e.Err)
return fmt.Sprintf("%v", e.Err)
}
func (e RequestError) Unwrap() error {

View File

@ -22,7 +22,7 @@ import (
func TestRequestError_Error(t *testing.T) {
err := RequestError{Code: ErrorCodeAccessDenied, Err: errors.New("an error")}
want := string(ErrorCodeAccessDenied) + ": an error"
want := "an error"
if got := err.Error(); got != want {
t.Errorf("RequestError.Error() = %v, want %v", got, want)
}

View File

@ -14,5 +14,5 @@
package registry
// ArtifactTypeNotation specifies the artifact type for a notation object.
// spec: https://github.com/notaryproject/notaryproject/blob/efc828223710f99ab9639d2d0f72d59036a8e80c/specs/signature-specification.md#storage
// spec: https://github.com/notaryproject/specifications/blob/v1.1.0/specs/signature-specification.md#signature
const ArtifactTypeNotation = "application/vnd.cncf.notary.signature"

View File

@ -14,19 +14,17 @@
package registry
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/registry/internal/artifactspec"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/oci"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/registry"
)
@ -35,30 +33,17 @@ const (
maxManifestSizeLimit = 4 * 1024 * 1024 // 4 MiB
)
var (
// notationEmptyConfigDesc is the descriptor of an empty notation manifest
// config
// reference: https://github.com/notaryproject/specifications/blob/v1.0.0/specs/signature-specification.md#storage
notationEmptyConfigDesc = ocispec.Descriptor{
MediaType: ArtifactTypeNotation,
Digest: ocispec.DescriptorEmptyJSON.Digest,
Size: ocispec.DescriptorEmptyJSON.Size,
}
// notationEmptyConfigData is the data of an empty notation manifest config
notationEmptyConfigData = ocispec.DescriptorEmptyJSON.Data
)
// RepositoryOptions provides user options when creating a Repository
// RepositoryOptions provides user options when creating a [Repository]
// it is kept for future extensibility
type RepositoryOptions struct{}
// repositoryClient implements Repository
// repositoryClient implements [Repository]
type repositoryClient struct {
oras.GraphTarget
RepositoryOptions
}
// NewRepository returns a new Repository.
// NewRepository returns a new [Repository].
// Known implementations of oras.GraphTarget:
// - [remote.Repository](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote#Repository)
// - [oci.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/oci#Store)
@ -68,7 +53,7 @@ func NewRepository(target oras.GraphTarget) Repository {
}
}
// NewRepositoryWithOptions returns a new Repository with user specified
// NewRepositoryWithOptions returns a new [Repository] with user specified
// options.
func NewRepositoryWithOptions(target oras.GraphTarget, opts RepositoryOptions) Repository {
return &repositoryClient{
@ -77,7 +62,7 @@ func NewRepositoryWithOptions(target oras.GraphTarget, opts RepositoryOptions) R
}
}
// NewOCIRepository returns a new Repository with oci.Store as
// NewOCIRepository returns a new [Repository] with oci.Store as
// its oras.GraphTarget. `path` denotes directory path to the target OCI layout.
func NewOCIRepository(path string, opts RepositoryOptions) (Repository, error) {
fileInfo, err := os.Stat(path)
@ -108,7 +93,6 @@ func (c *repositoryClient) ListSignatures(ctx context.Context, desc ocispec.Desc
if repo, ok := c.GraphTarget.(registry.ReferrerLister); ok {
return repo.Referrers(ctx, desc, ArtifactTypeNotation, fn)
}
signatureManifests, err := signatureReferrers(ctx, c.GraphTarget, desc)
if err != nil {
return fmt.Errorf("failed to get referrers during ListSignatures due to %w", err)
@ -126,7 +110,6 @@ func (c *repositoryClient) FetchSignatureBlob(ctx context.Context, desc ocispec.
if sigBlobDesc.Size > maxBlobSizeLimit {
return nil, ocispec.Descriptor{}, fmt.Errorf("signature blob too large: %d bytes", sigBlobDesc.Size)
}
var fetcher content.Fetcher = c.GraphTarget
if repo, ok := c.GraphTarget.(registry.Repository); ok {
fetcher = repo.Blobs()
@ -179,6 +162,7 @@ func (c *repositoryClient) getSignatureBlobDesc(ctx context.Context, sigManifest
// get the signature blob descriptor from signature manifest
var signatureBlobs []ocispec.Descriptor
// OCI image manifest
if sigManifestDesc.MediaType == ocispec.MediaTypeImageManifest {
var sigManifest ocispec.Manifest
@ -193,55 +177,27 @@ func (c *repositoryClient) getSignatureBlobDesc(ctx context.Context, sigManifest
}
signatureBlobs = sigManifest.Blobs
}
if len(signatureBlobs) != 1 {
return ocispec.Descriptor{}, fmt.Errorf("signature manifest requries exactly one signature envelope blob, got %d", len(signatureBlobs))
}
return signatureBlobs[0], nil
}
// uploadSignatureManifest uploads the signature manifest to the registry
func (c *repositoryClient) uploadSignatureManifest(ctx context.Context, subject, blobDesc ocispec.Descriptor, annotations map[string]string) (ocispec.Descriptor, error) {
configDesc, err := pushNotationManifestConfig(ctx, c.GraphTarget)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("failed to push notation manifest config: %w", err)
}
opts := oras.PackManifestOptions{
Subject: &subject,
ManifestAnnotations: annotations,
Layers: []ocispec.Descriptor{blobDesc},
ConfigDescriptor: &configDesc,
}
return oras.PackManifest(ctx, c.GraphTarget, oras.PackManifestVersion1_1, "", opts)
}
// pushNotationManifestConfig pushes an empty notation manifest config, if it
// doesn't exist.
//
// if the config exists, it returns the descriptor of the config without error.
func pushNotationManifestConfig(ctx context.Context, pusher content.Storage) (ocispec.Descriptor, error) {
// check if the config exists
exists, err := pusher.Exists(ctx, notationEmptyConfigDesc)
if err != nil {
return ocispec.Descriptor{}, fmt.Errorf("unable to verify existence: %s: %s. Details: %w", notationEmptyConfigDesc.Digest.String(), notationEmptyConfigDesc.MediaType, err)
}
if exists {
return notationEmptyConfigDesc, nil
}
// return nil if the config pushed successfully or it already exists
if err := pusher.Push(ctx, notationEmptyConfigDesc, bytes.NewReader(notationEmptyConfigData)); err != nil && !errors.Is(err, errdef.ErrAlreadyExists) {
return ocispec.Descriptor{}, fmt.Errorf("unable to push: %s: %s. Details: %w", notationEmptyConfigDesc.Digest.String(), notationEmptyConfigDesc.MediaType, err)
}
return notationEmptyConfigDesc, nil
return oras.PackManifest(ctx, c.GraphTarget, oras.PackManifestVersion1_1, ArtifactTypeNotation, opts)
}
// signatureReferrers returns referrer nodes of desc in target filtered by
// the "application/vnd.cncf.notary.signature" artifact type
func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
logger := log.GetLogger(ctx)
var results []ocispec.Descriptor
predecessors, err := target.Predecessors(ctx, desc)
if err != nil {
@ -257,6 +213,7 @@ func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage
if err != nil {
return nil, err
}
var artifact artifactspec.Artifact
if err := json.Unmarshal(fetched, &artifact); err != nil {
return nil, err
@ -264,6 +221,10 @@ func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage
if artifact.Subject == nil || !content.Equal(*artifact.Subject, desc) {
continue
}
if artifact.ArtifactType != ArtifactTypeNotation {
// not a valid Notary Project signature
continue
}
node.ArtifactType = artifact.ArtifactType
node.Annotations = artifact.Annotations
case ocispec.MediaTypeImageManifest:
@ -274,6 +235,7 @@ func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage
if err != nil {
return nil, err
}
var image ocispec.Manifest
if err := json.Unmarshal(fetched, &image); err != nil {
return nil, err
@ -281,15 +243,39 @@ func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage
if image.Subject == nil || !content.Equal(*image.Subject, desc) {
continue
}
node.ArtifactType = image.Config.MediaType
// check if image is a valid Notary Project signature
switch image.ArtifactType {
case ArtifactTypeNotation:
// 1. artifactType is "application/vnd.cncf.notary.signature",
// and config.mediaType is "application/vnd.oci.empty.v1+json"
if image.Config.MediaType == ocispec.MediaTypeEmptyJSON {
node.ArtifactType = image.ArtifactType
} else {
// not a valid Notary Project signature
logger.Warnf("not a valid Notary Project signature with artifactType %q, but config.mediaType is %q", image.ArtifactType, image.Config.MediaType)
continue
}
case "":
// 2. artifacteType does not exist,
// and config.mediaType is "application/vnd.cncf.notary.signature"
if image.Config.MediaType == ArtifactTypeNotation {
node.ArtifactType = image.Config.MediaType
} else {
// not a valid Notary Project signature
continue
}
default:
// not a valid Notary Project signature
continue
}
node.Annotations = image.Annotations
default:
continue
}
// only keep nodes of "application/vnd.cncf.notary.signature"
if node.ArtifactType == ArtifactTypeNotation {
results = append(results, node)
}
// add the node to results
results = append(results, node)
}
return results, nil
}

View File

@ -16,6 +16,7 @@ package registry
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
@ -23,6 +24,7 @@ import (
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
@ -139,7 +141,7 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
Body: io.NopCloser(bytes.NewReader([]byte{})),
}, nil
case "/v2/test/manifests/" + invalidDigest:
return &http.Response{}, fmt.Errorf(errMsg)
return &http.Response{}, errors.New(errMsg)
case "v2/test/manifest/" + validDigest2:
return &http.Response{
StatusCode: http.StatusOK,
@ -163,7 +165,7 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
},
}, nil
default:
return &http.Response{}, fmt.Errorf(msg)
return &http.Response{}, errors.New(msg)
}
case "/v2/test/referrers/":
return &http.Response{
@ -219,7 +221,7 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
}
return resp, nil
}
return &http.Response{}, fmt.Errorf(errMsg)
return &http.Response{}, errors.New(errMsg)
}
}
@ -479,8 +481,8 @@ var (
}
expectedSignatureManifestDesc = ocispec.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: "sha256:baeaea44f55c94499b7e082bd3c98ad5ec40fdf23ef89cdf4e5db6b83e4f18f5",
Size: 728,
Digest: "sha256:64300ad03f1dcd18136787363f3069c9598623221cbe76e3233d35266b7973d6",
Size: 793,
}
expectedSignatureBlobDesc = ocispec.Descriptor{
MediaType: joseTag,
@ -607,6 +609,9 @@ func TestNewOCIRepositoryFailed(t *testing.T) {
})
t.Run("no permission to create new path", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
// create a directory in the temp dir
dirPath := filepath.Join(t.TempDir(), "dir")
err := os.Mkdir(dirPath, 0000)
@ -806,6 +811,31 @@ func TestSignatureReferrers(t *testing.T) {
}
})
t.Run("artifact manifest with invalid artifactType", func(t *testing.T) {
sigManifest := `{"artifactType":"invalid", "subject":{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:835c3386406350fbddf5ee376b358bd20c6c423d6becbec166f83c533e4df5d6",
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Size: 198,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
}
})
t.Run("no valid image manifest", func(t *testing.T) {
store := &testStorage{
store: &memory.Store{},
@ -821,7 +851,6 @@ func TestSignatureReferrers(t *testing.T) {
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
@ -829,4 +858,172 @@ func TestSignatureReferrers(t *testing.T) {
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
}
})
t.Run("image manifest with invalid mediaType", func(t *testing.T) {
sigManifest := `{}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "invalid",
Size: 2,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatal("expected length of descriptors to be 0")
}
})
t.Run("image manifest with valid artifactType and config.MediaType", func(t *testing.T) {
sigManifest := `{"artifactType":"application/vnd.cncf.notary.signature","config":{"mediaType":"application/vnd.oci.empty.v1+json"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:ad3ab7874c72d7bf5db0e55ce839b37ee71320bf7c18ac1a512600963f03c54d",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 283,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 1 {
t.Fatal("expected length of descriptors to be 1")
}
if !content.Equal(sigManifestDesc, descriptors[0]) {
t.Fatalf("expected %v, got: %v", sigManifestDesc, descriptors[0])
}
})
t.Run("image manifest with valid artifactType but invalid config.MediaType", func(t *testing.T) {
sigManifest := `{"artifactType":"application/vnd.cncf.notary.signature","config":{"mediaType":"invalid"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:becfe1975b40352d0c7bd1337707a4c471fdcfa1ac380f2875fe8076a3bc3581",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 257,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatal("expected length of descriptors to be 0")
}
})
t.Run("image manifest with no artifactType and valid config.MediaType", func(t *testing.T) {
sigManifest := `{"config":{"mediaType":"application/vnd.cncf.notary.signature"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:0e0be61f687ba634dd772f6d3048101f78f22fabda64cc9600671cee41ab2d47",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 232,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 1 {
t.Fatal("expected length of descriptors to be 1")
}
if !content.Equal(sigManifestDesc, descriptors[0]) {
t.Fatalf("expected %v, got: %v", sigManifestDesc, descriptors[0])
}
})
t.Run("image manifest with no artifactType and invalid config.MediaType", func(t *testing.T) {
sigManifest := `{"config":{"mediaType":"invalid"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:1580e4f590269bd40a33e902888429c9bbb250902f5a7eb50f04fbb8bd4dbab3",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 202,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatal("expected length of descriptors to be 0")
}
})
t.Run("image manifest with invalid artifactType", func(t *testing.T) {
sigManifest := `{"artifactType":"invalid","config":{"mediaType":"application/vnd.oci.empty.v1+json"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:d8c225cb4eca3e15fa2a44c9d302044e8c8683399939e26f417edb82f8b69cc3",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 253,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatal("expected length of descriptors to be 0")
}
})
}
func TestUploadSignatureManifest(t *testing.T) {
ref, err := registry.ParseReference(validReference)
if err != nil {
t.Fatalf("failed to parse reference")
}
client := newRepositoryClientWithImageManifest(mockRemoteClient{}, ref, false)
manifest, err := client.uploadSignatureManifest(context.Background(),
ocispec.Descriptor{}, ocispec.Descriptor{}, nil)
if err != nil {
t.Fatalf("failed to upload signature manifest: %v", err)
}
if manifest.ArtifactType != ArtifactTypeNotation {
t.Fatalf("expected artifact type: %s, got: %s", ArtifactTypeNotation, manifest.ArtifactType)
}
}

View File

@ -35,7 +35,8 @@ import (
)
// PluginSigner signs artifacts and generates signatures.
// It implements notation.Signer
//
// It implements [notation.Signer] and [notation.BlobSigner].
type PluginSigner struct {
plugin plugin.SignPlugin
keyID string
@ -49,16 +50,17 @@ var algorithms = map[crypto.Hash]digest.Algorithm{
crypto.SHA512: digest.SHA512,
}
// NewFromPlugin creates a notation.Signer that signs artifacts and generates
// NewFromPlugin creates a [PluginSigner] that signs artifacts and generates
// signatures by delegating the one or more operations to the named plugin,
// as defined in https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md#signing-interfaces.
// Deprecated: NewFromPlugin function exists for historical compatibility and should not be used.
// To create PluginSigner, use NewPluginSigner() function.
//
// Deprecated: NewFromPlugin function exists for historical compatibility and
// should not be used. To create [PluginSigner], use NewPluginSigner() function.
func NewFromPlugin(plugin plugin.SignPlugin, keyID string, pluginConfig map[string]string) (notation.Signer, error) {
return NewPluginSigner(plugin, keyID, pluginConfig)
}
// NewPluginSigner creates a notation.Signer that signs artifacts and generates
// NewPluginSigner creates a [PluginSigner] that signs artifacts and generates
// signatures by delegating the one or more operations to the named plugin,
// as defined in https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md#signing-interfaces.
func NewPluginSigner(plugin plugin.SignPlugin, keyID string, pluginConfig map[string]string) (*PluginSigner, error) {
@ -68,7 +70,6 @@ func NewPluginSigner(plugin plugin.SignPlugin, keyID string, pluginConfig map[st
if keyID == "" {
return nil, errors.New("keyID not specified")
}
return &PluginSigner{
plugin: plugin,
keyID: keyID,
@ -82,40 +83,52 @@ func (s *PluginSigner) PluginAnnotations() map[string]string {
}
// Sign signs the artifact described by its descriptor and returns the
// marshalled envelope.
// signature and SignerInfo.
func (s *PluginSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
logger := log.GetLogger(ctx)
mergedConfig := s.mergeConfig(opts.PluginConfig)
logger.Debug("Invoking plugin's get-plugin-metadata command")
metadata, err := s.plugin.GetMetadata(ctx, &plugin.GetMetadataRequest{PluginConfig: mergedConfig})
if err != nil {
return nil, nil, err
}
logger.Debugf("Using plugin %v with capabilities %v to sign oci artifact %v in signature media type %v", metadata.Name, metadata.Capabilities, desc.Digest, opts.SignatureMediaType)
if metadata.HasCapability(plugin.CapabilitySignatureGenerator) {
ks, err := s.getKeySpec(ctx, mergedConfig)
if err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
}
return s.generateSignature(ctx, desc, opts, ks, metadata, mergedConfig)
sig, signerInfo, err := s.generateSignature(ctx, desc, opts, ks, metadata, mergedConfig)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
}
return sig, signerInfo, nil
} else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) {
return s.generateSignatureEnvelope(ctx, desc, opts)
sig, signerInfo, err := s.generateSignatureEnvelope(ctx, desc, opts)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
}
return sig, signerInfo, nil
}
return nil, nil, fmt.Errorf("plugin does not have signing capabilities")
}
// SignBlob signs the arbitrary data and returns the marshalled envelope.
// SignBlob signs the descriptor returned by genDesc, and returns the
// signature and SignerInfo.
func (s *PluginSigner) SignBlob(ctx context.Context, descGenFunc notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
logger := log.GetLogger(ctx)
mergedConfig := s.mergeConfig(opts.PluginConfig)
logger.Debug("Invoking plugin's get-plugin-metadata command")
metadata, err := s.plugin.GetMetadata(ctx, &plugin.GetMetadataRequest{PluginConfig: mergedConfig})
if err != nil {
return nil, nil, err
}
// only support blob signing with the signature generator capability because
// the envelope generator capability is designed for OCI signing.
// A new capability may be added in the future for blob signing.
if !metadata.HasCapability(plugin.CapabilitySignatureGenerator) {
return nil, nil, fmt.Errorf("the plugin %q lacks the signature generator capability required for blob signing", metadata.Name)
}
logger.Debug("Invoking plugin's describe-key command")
ks, err := s.getKeySpec(ctx, mergedConfig)
@ -128,14 +141,8 @@ func (s *PluginSigner) SignBlob(ctx context.Context, descGenFunc notation.BlobDe
if err != nil {
return nil, nil, err
}
logger.Debugf("Using plugin %v with capabilities %v to sign blob using descriptor %+v", metadata.Name, metadata.Capabilities, desc)
if metadata.HasCapability(plugin.CapabilitySignatureGenerator) {
return s.generateSignature(ctx, desc, opts, ks, metadata, mergedConfig)
} else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) {
return s.generateSignatureEnvelope(ctx, desc, opts)
}
return nil, nil, fmt.Errorf("plugin does not have signing capabilities")
return s.generateSignature(ctx, desc, opts, ks, metadata, mergedConfig)
}
func (s *PluginSigner) getKeySpec(ctx context.Context, config map[string]string) (signature.KeySpec, error) {
@ -145,11 +152,9 @@ func (s *PluginSigner) getKeySpec(ctx context.Context, config map[string]string)
if err != nil {
return signature.KeySpec{}, err
}
if s.keyID != descKeyResp.KeyID {
return signature.KeySpec{}, fmt.Errorf("keyID in describeKey response %q does not match request %q", descKeyResp.KeyID, s.keyID)
}
return proto.DecodeKeySpec(descKeyResp.KeySpec)
}
@ -165,7 +170,6 @@ func (s *PluginSigner) generateSignature(ctx context.Context, desc ocispec.Descr
keySpec: ks,
},
}
opts.SigningAgent = fmt.Sprintf("%s %s/%s", signingAgent, metadata.Name, metadata.Version)
return genericSigner.Sign(ctx, desc, opts)
}
@ -178,6 +182,7 @@ func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
if err != nil {
return nil, nil, fmt.Errorf("envelope payload can't be marshalled: %w", err)
}
// Execute plugin sign command.
req := &plugin.GenerateEnvelopeRequest{
ContractVersion: plugin.ContractVersion,
@ -200,13 +205,11 @@ func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
resp.SignatureEnvelopeType, req.SignatureEnvelopeType,
)
}
logger.Debug("Verifying signature envelope generated by the plugin")
sigEnv, err := signature.ParseEnvelope(opts.SignatureMediaType, resp.SignatureEnvelope)
if err != nil {
return nil, nil, err
}
envContent, err := sigEnv.Verify()
if err != nil {
return nil, nil, fmt.Errorf("generated signature failed verification: %w", err)
@ -214,31 +217,29 @@ func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
if err := envelope.ValidatePayloadContentType(&envContent.Payload); err != nil {
return nil, nil, err
}
content := envContent.Payload.Content
var signedPayload envelope.Payload
if err = json.Unmarshal(content, &signedPayload); err != nil {
return nil, nil, fmt.Errorf("signed envelope payload can't be unmarshalled: %w", err)
}
if !isPayloadDescriptorValid(desc, signedPayload.TargetArtifact) {
return nil, nil, fmt.Errorf("during signing descriptor subject has changed from %+v to %+v", desc, signedPayload.TargetArtifact)
}
if unknownAttributes := areUnknownAttributesAdded(content); len(unknownAttributes) != 0 {
return nil, nil, fmt.Errorf("during signing, following unknown attributes were added to subject descriptor: %+q", unknownAttributes)
}
s.manifestAnnotations = resp.Annotations
return resp.SignatureEnvelope, &envContent.SignerInfo, nil
}
func (s *PluginSigner) mergeConfig(config map[string]string) map[string]string {
c := make(map[string]string, len(s.pluginConfig)+len(config))
// First clone s.PluginConfig.
for k, v := range s.pluginConfig {
c[k] = v
}
// Then set or override entries from config.
for k, v := range config {
c[k] = v
@ -256,7 +257,6 @@ func (s *PluginSigner) describeKey(ctx context.Context, config map[string]string
if err != nil {
return nil, err
}
return resp, nil
}
@ -266,6 +266,7 @@ func isDescriptorSubset(original, newDesc ocispec.Descriptor) bool {
if !content.Equal(original, newDesc) {
return false
}
// Plugins may append additional annotations but not replace/override
// existing.
for k, v := range original.Annotations {
@ -283,6 +284,7 @@ func isPayloadDescriptorValid(originalDesc, newDesc ocispec.Descriptor) bool {
func areUnknownAttributesAdded(content []byte) []string {
var targetArtifactMap map[string]interface{}
// Ignoring error because we already successfully unmarshalled before this
// point
_ = json.Unmarshal(content, &targetArtifactMap)
@ -339,12 +341,10 @@ func (s *pluginPrimitiveSigner) Sign(payload []byte) ([]byte, []*x509.Certificat
if err != nil {
return nil, nil, err
}
keySpecHash, err := proto.HashAlgorithmFromKeySpec(s.keySpec)
if err != nil {
return nil, nil, err
}
req := &plugin.GenerateSignatureRequest{
ContractVersion: plugin.ContractVersion,
KeyID: s.keyID,
@ -353,7 +353,6 @@ func (s *pluginPrimitiveSigner) Sign(payload []byte) ([]byte, []*x509.Certificat
Payload: payload,
PluginConfig: s.pluginConfig,
}
resp, err := s.plugin.GenerateSignature(s.ctx, req)
if err != nil {
return nil, nil, err

View File

@ -216,6 +216,17 @@ func (p *mockPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEn
return &proto.GenerateEnvelopeResponse{}, nil
}
func TestPluginSignerImpl(t *testing.T) {
p := &PluginSigner{}
if _, ok := interface{}(p).(notation.Signer); !ok {
t.Fatal("PluginSigner does not implement notation.Signer")
}
if _, ok := interface{}(p).(notation.BlobSigner); !ok {
t.Fatal("PluginSigner does not implement notation.BlobSigner")
}
}
func TestNewFromPluginFailed(t *testing.T) {
tests := map[string]struct {
pl plugin.SignPlugin
@ -352,6 +363,21 @@ func TestPluginSigner_SignBlob_Valid(t *testing.T) {
}
}
func TestPluginSigner_SignBlob_Invalid(t *testing.T) {
t.Run("blob signing with generate envelope plugin should fail", func(t *testing.T) {
plugin := &mockPlugin{}
plugin.wantEnvelope = true
pluginSigner := PluginSigner{
plugin: plugin,
}
_, _, err := pluginSigner.SignBlob(context.Background(), getDescriptorFunc(false), validSignOpts)
expectedErrMsg := "the plugin \"testPlugin\" lacks the signature generator capability required for blob signing"
if err == nil || !strings.Contains(err.Error(), expectedErrMsg) {
t.Fatalf("expected error %q, got %v", expectedErrMsg, err)
}
})
}
func TestPluginSigner_SignEnvelope_RunFailed(t *testing.T) {
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {

View File

@ -12,8 +12,8 @@
// limitations under the License.
// Package signer provides notation signing functionality. It implements the
// notation.Signer interface by providing builtinSigner for local signing and
// PluginSigner for remote signing.
// [notation.Signer] and [notation.BlobSigner] interfaces by providing
// builtinSigner for local signing and [PluginSigner] for remote signing.
package signer
import (
@ -34,21 +34,24 @@ import (
)
// signingAgent is the unprotected header field used by signature.
const signingAgent = "Notation/1.0.0"
const signingAgent = "notation-go/1.3.0+unreleased"
// GenericSigner implements notation.Signer and embeds signature.Signer
// GenericSigner implements [notation.Signer] and [notation.BlobSigner].
// It embeds signature.Signer.
type GenericSigner struct {
signer signature.Signer
}
// New returns a builtinSigner given key and cert chain
// Deprecated: New function exists for historical compatibility and should not be used.
// To create GenericSigner, use NewGenericSigner() function.
// New returns a [notation.Signer] given key and cert chain.
//
// Deprecated: New function exists for historical compatibility and
// should not be used. To create [GenericSigner],
// use NewGenericSigner() function.
func New(key crypto.PrivateKey, certChain []*x509.Certificate) (notation.Signer, error) {
return NewGenericSigner(key, certChain)
}
// NewGenericSigner returns a builtinSigner given key and cert chain
// NewGenericSigner returns a builtinSigner given key and cert chain.
func NewGenericSigner(key crypto.PrivateKey, certChain []*x509.Certificate) (*GenericSigner, error) {
localSigner, err := signature.NewLocalSigner(certChain, key)
if err != nil {
@ -59,12 +62,13 @@ func NewGenericSigner(key crypto.PrivateKey, certChain []*x509.Certificate) (*Ge
}, nil
}
// NewFromFiles returns a builtinSigner given key and certChain paths.
// NewFromFiles returns a [notation.Signer] given key and certChain paths.
func NewFromFiles(keyPath, certChainPath string) (notation.Signer, error) {
return NewGenericSignerFromFiles(keyPath, certChainPath)
}
// NewGenericSignerFromFiles returns a builtinSigner given key and certChain paths.
// NewGenericSignerFromFiles returns a builtinSigner given key and certChain
// paths.
func NewGenericSignerFromFiles(keyPath, certChainPath string) (*GenericSigner, error) {
if keyPath == "" {
return nil, errors.New("key path not specified")
@ -96,7 +100,7 @@ func NewGenericSignerFromFiles(keyPath, certChainPath string) (*GenericSigner, e
}
// Sign signs the artifact described by its descriptor and returns the
// marshalled envelope.
// signature and SignerInfo.
func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
logger := log.GetLogger(ctx)
logger.Debugf("Generic signing for %v in signature media type %v", desc.Digest, opts.SignatureMediaType)
@ -106,22 +110,30 @@ func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
if err != nil {
return nil, nil, fmt.Errorf("envelope payload can't be marshalled: %w", err)
}
var signingAgentId string
if opts.SigningAgent != "" {
signingAgentId = opts.SigningAgent
} else {
signingAgentId = signingAgent
}
if opts.Timestamper != nil && opts.TSARootCAs == nil {
return nil, nil, errors.New("timestamping: got Timestamper but nil TSARootCAs")
}
if opts.TSARootCAs != nil && opts.Timestamper == nil {
return nil, nil, errors.New("timestamping: got TSARootCAs but nil Timestamper")
}
signReq := &signature.SignRequest{
Payload: signature.Payload{
ContentType: envelope.MediaTypePayloadV1,
Content: payloadBytes,
},
Signer: s.signer,
SigningTime: time.Now(),
SigningScheme: signature.SigningSchemeX509,
SigningAgent: signingAgentId,
Signer: s.signer,
SigningTime: time.Now(),
SigningScheme: signature.SigningSchemeX509,
SigningAgent: signingAgentId,
Timestamper: opts.Timestamper,
TSARootCAs: opts.TSARootCAs,
TSARevocationValidator: opts.TSARevocationValidator,
}
// Add expiry only if ExpiryDuration is not zero
@ -135,18 +147,25 @@ func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
logger.Debugf(" Expiry: %v", signReq.Expiry)
logger.Debugf(" SigningScheme: %v", signReq.SigningScheme)
logger.Debugf(" SigningAgent: %v", signReq.SigningAgent)
if signReq.Timestamper != nil {
logger.Debug("Enabled timestamping")
if signReq.TSARevocationValidator != nil {
logger.Debug("Enabled timestamping certificate chain revocation check")
}
}
// Add ctx to the SignRequest
signReq = signReq.WithContext(ctx)
// perform signing
sigEnv, err := signature.NewEnvelope(opts.SignatureMediaType)
if err != nil {
return nil, nil, err
}
sig, err := sigEnv.Sign(signReq)
if err != nil {
return nil, nil, err
}
envContent, err := sigEnv.Verify()
if err != nil {
return nil, nil, fmt.Errorf("generated signature failed verification: %v", err)
@ -154,34 +173,29 @@ func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
if err := envelope.ValidatePayloadContentType(&envContent.Payload); err != nil {
return nil, nil, err
}
// TODO: re-enable timestamping https://github.com/notaryproject/notation-go/issues/78
return sig, &envContent.SignerInfo, nil
}
// SignBlob signs the descriptor returned by blobGen and returns the marshalled envelope
func (s *GenericSigner) SignBlob(ctx context.Context, descGenFunc notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
// SignBlob signs the descriptor returned by genDesc, and returns the
// signature and SignerInfo.
func (s *GenericSigner) SignBlob(ctx context.Context, genDesc notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
logger := log.GetLogger(ctx)
logger.Debugf("Generic blob signing for signature media type %v", opts.SignatureMediaType)
logger.Debugf("Generic blob signing for signature media type %s", opts.SignatureMediaType)
ks, err := s.signer.KeySpec()
if err != nil {
return nil, nil, err
}
desc, err := getDescriptor(ks, descGenFunc)
desc, err := getDescriptor(ks, genDesc)
if err != nil {
return nil, nil, err
}
return s.Sign(ctx, desc, opts)
}
func getDescriptor(ks signature.KeySpec, descGenFunc notation.BlobDescriptorGenerator) (ocispec.Descriptor, error) {
func getDescriptor(ks signature.KeySpec, genDesc notation.BlobDescriptorGenerator) (ocispec.Descriptor, error) {
digestAlg, ok := algorithms[ks.SignatureAlgorithm().Hash()]
if !ok {
return ocispec.Descriptor{}, fmt.Errorf("unknown hashing algo %v", ks.SignatureAlgorithm().Hash())
}
return descGenFunc(digestAlg)
return genDesc(digestAlg)
}

View File

@ -30,17 +30,23 @@ import (
"testing"
"time"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/notation-core-go/revocation/purpose"
"github.com/notaryproject/notation-core-go/signature"
_ "github.com/notaryproject/notation-core-go/signature/cose"
_ "github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-core-go/testhelper"
nx509 "github.com/notaryproject/notation-core-go/x509"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/internal/envelope"
"github.com/notaryproject/notation-go/plugin/proto"
"github.com/notaryproject/tspclient-go"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
const rfc3161URL = "http://timestamp.digicert.com"
type keyCertPair struct {
keySpecName string
key crypto.PrivateKey
@ -49,7 +55,8 @@ type keyCertPair struct {
var keyCertPairCollections []*keyCertPair
// setUpKeyCertPairCollections setups all combinations of private key and certificates.
// setUpKeyCertPairCollections setups all combinations of private key and
// certificates.
func setUpKeyCertPairCollections() []*keyCertPair {
// rsa
var keyCertPairs []*keyCertPair
@ -117,7 +124,7 @@ func generateKeyBytes(key crypto.PrivateKey) (keyBytes []byte, err error) {
return keyBytes, nil
}
func prepareTestKeyCertFile(keyCert *keyCertPair, envelopeType, dir string) (string, string, error) {
func prepareTestKeyCertFile(keyCert *keyCertPair, dir string) (string, string, error) {
keyPath, certPath := filepath.Join(dir, keyCert.keySpecName+".key"), filepath.Join(dir, keyCert.keySpecName+".cert")
keyBytes, err := generateKeyBytes(keyCert.key)
if err != nil {
@ -138,7 +145,7 @@ func prepareTestKeyCertFile(keyCert *keyCertPair, envelopeType, dir string) (str
}
func testSignerFromFile(t *testing.T, keyCert *keyCertPair, envelopeType, dir string) {
keyPath, certPath, err := prepareTestKeyCertFile(keyCert, envelopeType, dir)
keyPath, certPath, err := prepareTestKeyCertFile(keyCert, dir)
if err != nil {
t.Fatalf("prepareTestKeyCertFile() failed: %v", err)
}
@ -156,6 +163,17 @@ func testSignerFromFile(t *testing.T, keyCert *keyCertPair, envelopeType, dir st
basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1], nil)
}
func TestGenericSignerImpl(t *testing.T) {
g := &GenericSigner{}
if _, ok := interface{}(g).(notation.Signer); !ok {
t.Fatal("GenericSigner does not implement notation.Signer")
}
if _, ok := interface{}(g).(notation.BlobSigner); !ok {
t.Fatal("GenericSigner does not implement notation.BlobSigner")
}
}
func TestNewFromFiles(t *testing.T) {
// sign with key
dir := t.TempDir()
@ -208,12 +226,74 @@ func TestSignWithCertChain(t *testing.T) {
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
for _, keyCert := range keyCertPairCollections {
t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs)
validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs, false)
})
}
}
}
func TestSignWithTimestamping(t *testing.T) {
// sign with key
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
for _, keyCert := range keyCertPairCollections {
t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs, true)
})
}
}
// timestamping without timestamper
envelopeType := signature.RegisteredEnvelopeTypes()[0]
keyCert := keyCertPairCollections[0]
s, err := New(keyCert.key, keyCert.certs)
if err != nil {
t.Fatalf("NewSigner() error = %v", err)
}
ctx := context.Background()
desc, sOpts := generateSigningContent()
sOpts.SignatureMediaType = envelopeType
sOpts.TSARootCAs = x509.NewCertPool()
_, _, err = s.Sign(ctx, desc, sOpts)
expectedErrMsg := "timestamping: got TSARootCAs but nil Timestamper"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
// timestamping without TSARootCAs
desc, sOpts = generateSigningContent()
sOpts.SignatureMediaType = envelopeType
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
if err != nil {
t.Fatal(err)
}
_, _, err = s.Sign(ctx, desc, sOpts)
expectedErrMsg = "timestamping: got Timestamper but nil TSARootCAs"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
// timestamping with unknown authority
desc, sOpts = generateSigningContent()
sOpts.SignatureMediaType = envelopeType
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
if err != nil {
t.Fatal(err)
}
sOpts.TSARootCAs = x509.NewCertPool()
tsaRevocationValidator, err := revocation.NewWithOptions(revocation.Options{
CertChainPurpose: purpose.Timestamping,
})
if err != nil {
t.Fatal(err)
}
sOpts.TSARevocationValidator = tsaRevocationValidator
_, _, err = s.Sign(ctx, desc, sOpts)
expectedErrMsg = "timestamp: failed to verify signed token: cms verification failure: x509: certificate signed by unknown authority"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
}
func TestSignBlobWithCertChain(t *testing.T) {
// sign with key
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
@ -269,7 +349,7 @@ func signRSA(digest []byte, hash crypto.Hash, pk *rsa.PrivateKey) ([]byte, error
return rsa.SignPSS(rand.Reader, pk, hash, digest, &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash})
}
func signECDSA(digest []byte, hash crypto.Hash, pk *ecdsa.PrivateKey) ([]byte, error) {
func signECDSA(digest []byte, pk *ecdsa.PrivateKey) ([]byte, error) {
r, s, err := ecdsa.Sign(rand.Reader, pk, digest)
if err != nil {
return nil, err
@ -289,7 +369,7 @@ func localSign(payload []byte, hash crypto.Hash, pk crypto.PrivateKey) ([]byte,
case *rsa.PrivateKey:
return signRSA(digest, hash, key)
case *ecdsa.PrivateKey:
return signECDSA(digest, hash, key)
return signECDSA(digest, key)
default:
return nil, errors.New("signing private key not supported")
}
@ -354,7 +434,7 @@ func verifySigningAgent(t *testing.T, signingAgentId string, metadata *proto.Get
}
}
func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.PrivateKey, certs []*x509.Certificate) {
func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.PrivateKey, certs []*x509.Certificate, timestamp bool) {
s, err := New(key, certs)
if err != nil {
t.Fatalf("NewSigner() error = %v", err)
@ -363,6 +443,19 @@ func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.Private
ctx := context.Background()
desc, sOpts := generateSigningContent()
sOpts.SignatureMediaType = envelopeType
if timestamp {
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
if err != nil {
t.Fatal(err)
}
rootCerts, err := nx509.ReadCertificateFile("./testdata/DigiCertTSARootSHA384.cer")
if err != nil {
t.Fatal(err)
}
rootCAs := x509.NewCertPool()
rootCAs.AddCert(rootCerts[0])
sOpts.TSARootCAs = rootCAs
}
sig, _, err := s.Sign(ctx, desc, sOpts)
if err != nil {
t.Fatalf("Sign() error = %v", err)

Binary file not shown.

171
verifier/crl/crl.go Normal file
View File

@ -0,0 +1,171 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package crl provides functionalities for crl revocation check.
package crl
import (
"context"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"time"
corecrl "github.com/notaryproject/notation-core-go/revocation/crl"
"github.com/notaryproject/notation-go/internal/file"
"github.com/notaryproject/notation-go/log"
)
// FileCache implements corecrl.Cache.
//
// Key: url of the CRL.
//
// Value: corecrl.Bundle.
//
// This cache builds on top of the UNIX file system to leverage the file system's
// atomic operations. The `rename` and `remove` operations will unlink the old
// file but keep the inode and file descriptor for existing processes to access
// the file. The old inode will be dereferenced when all processes close the old
// file descriptor. Additionally, the operations are proven to be atomic on
// UNIX-like platforms, so there is no need to handle file locking.
//
// NOTE: For Windows, the `open`, `rename` and `remove` operations need file
// locking to ensure atomicity. The current implementation does not handle
// file locking, so the concurrent write from multiple processes may be failed.
// Please do not use this cache in a multi-process environment on Windows.
type FileCache struct {
// root is the root directory of the cache
root string
}
// fileCacheContent is the actual content saved in a FileCache
type fileCacheContent struct {
// BaseCRL is the ASN.1 encoded base CRL
BaseCRL []byte `json:"baseCRL"`
// DeltaCRL is the ASN.1 encoded delta CRL
DeltaCRL []byte `json:"deltaCRL,omitempty"`
}
// NewFileCache creates a FileCache with root as the root directory
//
// An example for root is `dir.CacheFS().SysPath(dir.PathCRLCache)`
func NewFileCache(root string) (*FileCache, error) {
if err := os.MkdirAll(root, 0700); err != nil {
return nil, fmt.Errorf("failed to create crl file cache: %w", err)
}
return &FileCache{
root: root,
}, nil
}
// Get retrieves CRL bundle from c given url as key. If the key does not exist
// or the content has expired, corecrl.ErrCacheMiss is returned.
func (c *FileCache) Get(ctx context.Context, url string) (*corecrl.Bundle, error) {
logger := log.GetLogger(ctx)
logger.Debugf("Retrieving crl bundle from file cache with key %q ...", url)
// get content from file cache
contentBytes, err := os.ReadFile(filepath.Join(c.root, c.fileName(url)))
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
logger.Debugf("CRL file cache miss. Key %q does not exist", url)
return nil, corecrl.ErrCacheMiss
}
return nil, fmt.Errorf("failed to get crl bundle from file cache with key %q: %w", url, err)
}
// decode content to crl Bundle
var content fileCacheContent
if err := json.Unmarshal(contentBytes, &content); err != nil {
return nil, fmt.Errorf("failed to decode file retrieved from file cache: %w", err)
}
var bundle corecrl.Bundle
bundle.BaseCRL, err = x509.ParseRevocationList(content.BaseCRL)
if err != nil {
return nil, fmt.Errorf("failed to parse base CRL of file retrieved from file cache: %w", err)
}
if content.DeltaCRL != nil {
bundle.DeltaCRL, err = x509.ParseRevocationList(content.DeltaCRL)
if err != nil {
return nil, fmt.Errorf("failed to parse delta CRL of file retrieved from file cache: %w", err)
}
}
// check expiry
if err := checkExpiry(ctx, bundle.BaseCRL.NextUpdate); err != nil {
return nil, fmt.Errorf("check BaseCRL expiry failed: %w", err)
}
if bundle.DeltaCRL != nil {
if err := checkExpiry(ctx, bundle.DeltaCRL.NextUpdate); err != nil {
return nil, fmt.Errorf("check DeltaCRL expiry failed: %w", err)
}
}
return &bundle, nil
}
// Set stores the CRL bundle in c with url as key.
func (c *FileCache) Set(ctx context.Context, url string, bundle *corecrl.Bundle) error {
logger := log.GetLogger(ctx)
logger.Debugf("Storing crl bundle to file cache with key %q ...", url)
if bundle == nil {
return errors.New("failed to store crl bundle in file cache: bundle cannot be nil")
}
if bundle.BaseCRL == nil {
return errors.New("failed to store crl bundle in file cache: bundle BaseCRL cannot be nil")
}
// actual content to be saved in the cache
content := fileCacheContent{
BaseCRL: bundle.BaseCRL.Raw,
}
if bundle.DeltaCRL != nil {
content.DeltaCRL = bundle.DeltaCRL.Raw
}
contentBytes, err := json.Marshal(content)
if err != nil {
return fmt.Errorf("failed to store crl bundle in file cache: %w", err)
}
if err := file.WriteFile(c.root, filepath.Join(c.root, c.fileName(url)), contentBytes); err != nil {
return fmt.Errorf("failed to store crl bundle in file cache: %w", err)
}
return nil
}
// fileName returns the filename of the content stored in c
func (c *FileCache) fileName(url string) string {
hash := sha256.Sum256([]byte(url))
return hex.EncodeToString(hash[:])
}
// checkExpiry returns nil when nextUpdate is bounded before current time
func checkExpiry(ctx context.Context, nextUpdate time.Time) error {
logger := log.GetLogger(ctx)
if nextUpdate.IsZero() {
return errors.New("crl bundle retrieved from file cache does not contain valid NextUpdate")
}
if time.Now().After(nextUpdate) {
logger.Debugf("CRL bundle retrieved from file cache has expired at %s", nextUpdate)
return corecrl.ErrCacheMiss
}
return nil
}

395
verifier/crl/crl_test.go Normal file
View File

@ -0,0 +1,395 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crl
import (
"context"
"crypto/rand"
"crypto/x509"
"encoding/json"
"errors"
"math/big"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"time"
corecrl "github.com/notaryproject/notation-core-go/revocation/crl"
"github.com/notaryproject/notation-core-go/testhelper"
)
func TestCache(t *testing.T) {
t.Run("file cache implement Cache interface", func(t *testing.T) {
root := t.TempDir()
var coreCache corecrl.Cache
var err error
coreCache, err = NewFileCache(root)
if err != nil {
t.Fatal(err)
}
if _, ok := coreCache.(*FileCache); !ok {
t.Fatal("FileCache does not implement coreCache")
}
})
}
func TestFileCache(t *testing.T) {
now := time.Now()
certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true)
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
Number: big.NewInt(1),
NextUpdate: now.Add(time.Hour),
}, certChain[1].Cert, certChain[1].PrivateKey)
if err != nil {
t.Fatal(err)
}
baseCRL, err := x509.ParseRevocationList(crlBytes)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
root := t.TempDir()
cache, err := NewFileCache(root)
t.Run("NewFileCache", func(t *testing.T) {
if err != nil {
t.Fatalf("expected no error, but got %v", err)
}
if cache.root != root {
t.Fatalf("expected root %v, but got %v", root, cache.root)
}
})
key := "http://example.com"
t.Run("comformance", func(t *testing.T) {
bundle := &corecrl.Bundle{BaseCRL: baseCRL}
if err := cache.Set(ctx, key, bundle); err != nil {
t.Fatal(err)
}
retrievedBundle, err := cache.Get(ctx, key)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(retrievedBundle.BaseCRL, bundle.BaseCRL) {
t.Fatalf("expected BaseCRL %+v, but got %+v", bundle.BaseCRL, retrievedBundle.BaseCRL)
}
if bundle.DeltaCRL != nil {
t.Fatalf("expected DeltaCRL to be nil, but got %+v", retrievedBundle.DeltaCRL)
}
})
t.Run("comformance with delta crl", func(t *testing.T) {
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
Number: big.NewInt(2),
NextUpdate: now.Add(time.Hour),
}, certChain[1].Cert, certChain[1].PrivateKey)
if err != nil {
t.Fatal(err)
}
deltaCRL, err := x509.ParseRevocationList(crlBytes)
if err != nil {
t.Fatal(err)
}
bundle := &corecrl.Bundle{BaseCRL: baseCRL, DeltaCRL: deltaCRL}
if err := cache.Set(ctx, key, bundle); err != nil {
t.Fatal(err)
}
retrievedBundle, err := cache.Get(ctx, key)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(retrievedBundle.BaseCRL, bundle.BaseCRL) {
t.Fatalf("expected BaseCRL %+v, but got %+v", bundle.BaseCRL, retrievedBundle.BaseCRL)
}
if !reflect.DeepEqual(retrievedBundle.DeltaCRL, bundle.DeltaCRL) {
t.Fatalf("expected DeltaCRL %+v, but got %+v", bundle.DeltaCRL, retrievedBundle.DeltaCRL)
}
})
}
func TestNewFileCacheFailed(t *testing.T) {
tempDir := t.TempDir()
t.Run("without permission to create cache directory", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
if err := os.Chmod(tempDir, 0); err != nil {
t.Fatal(err)
}
root := filepath.Join(tempDir, "test")
_, err := NewFileCache(root)
if !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("expected permission denied error, but got %v", err)
}
// restore permission
if err := os.Chmod(tempDir, 0755); err != nil {
t.Fatalf("failed to change permission: %v", err)
}
})
}
func TestGetFailed(t *testing.T) {
tempDir := t.TempDir()
cache, err := NewFileCache(tempDir)
if err != nil {
t.Fatal(err)
}
t.Run("key does not exist", func(t *testing.T) {
_, err := cache.Get(context.Background(), "nonExistKey")
if !errors.Is(err, corecrl.ErrCacheMiss) {
t.Fatalf("expected ErrCacheMiss, but got %v", err)
}
})
invalidFile := filepath.Join(tempDir, cache.fileName("invalid"))
if err := os.WriteFile(invalidFile, []byte("invalid"), 0644); err != nil {
t.Fatalf("failed to write file: %v", err)
}
t.Run("no permission to read file", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
if err := os.Chmod(invalidFile, 0); err != nil {
t.Fatal(err)
}
_, err := cache.Get(context.Background(), "invalid")
if err == nil || !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("expected permission denied error, but got %v", err)
}
// restore permission
if err := os.Chmod(invalidFile, 0755); err != nil {
t.Fatal(err)
}
})
t.Run("invalid content", func(t *testing.T) {
_, err := cache.Get(context.Background(), "invalid")
expectedErrMsg := "failed to decode file retrieved from file cache: invalid character 'i' looking for beginning of value"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
}
})
now := time.Now()
certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true)
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
Number: big.NewInt(1),
}, certChain[1].Cert, certChain[1].PrivateKey)
if err != nil {
t.Fatalf("failed to create base CRL: %v", err)
}
baseCRL, err := x509.ParseRevocationList(crlBytes)
if err != nil {
t.Fatalf("failed to parse base CRL: %v", err)
}
t.Run("empty RawBaseCRL of content", func(t *testing.T) {
content := fileCacheContent{
BaseCRL: []byte{},
}
b, err := json.Marshal(content)
if err != nil {
t.Fatal(err)
}
invalidBundleFile := filepath.Join(tempDir, cache.fileName("invalidBundle"))
if err := os.WriteFile(invalidBundleFile, b, 0644); err != nil {
t.Fatalf("failed to write file: %v", err)
}
_, err = cache.Get(context.Background(), "invalidBundle")
expectedErrMsg := "failed to parse base CRL of file retrieved from file cache: x509: malformed crl"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
}
})
t.Run("invalid RawBaseCRL of content", func(t *testing.T) {
content := fileCacheContent{
BaseCRL: []byte("invalid"),
}
b, err := json.Marshal(content)
if err != nil {
t.Fatal(err)
}
invalidBundleFile := filepath.Join(tempDir, cache.fileName("invalidBundle"))
if err := os.WriteFile(invalidBundleFile, b, 0644); err != nil {
t.Fatalf("failed to write file: %v", err)
}
_, err = cache.Get(context.Background(), "invalidBundle")
expectedErrMsg := "failed to parse base CRL of file retrieved from file cache: x509: malformed crl"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
}
})
t.Run("invalid RawDeltaCRL of content", func(t *testing.T) {
content := fileCacheContent{
BaseCRL: baseCRL.Raw,
DeltaCRL: []byte("invalid"),
}
b, err := json.Marshal(content)
if err != nil {
t.Fatal(err)
}
invalidBundleFile := filepath.Join(tempDir, cache.fileName("invalidBundle"))
if err := os.WriteFile(invalidBundleFile, b, 0644); err != nil {
t.Fatalf("failed to write file: %v", err)
}
_, err = cache.Get(context.Background(), "invalidBundle")
expectedErrMsg := "failed to parse delta CRL of file retrieved from file cache: x509: malformed crl"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
}
})
t.Run("bundle with invalid NextUpdate", func(t *testing.T) {
ctx := context.Background()
expiredBundle := &corecrl.Bundle{BaseCRL: baseCRL}
if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil {
t.Fatal(err)
}
_, err = cache.Get(ctx, "expiredKey")
expectedErrMsg := "check BaseCRL expiry failed: crl bundle retrieved from file cache does not contain valid NextUpdate"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
}
})
crlBytes, err = x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
Number: big.NewInt(1),
NextUpdate: now.Add(-time.Hour),
}, certChain[1].Cert, certChain[1].PrivateKey)
if err != nil {
t.Fatalf("failed to create base CRL: %v", err)
}
expiredBaseCRL, err := x509.ParseRevocationList(crlBytes)
if err != nil {
t.Fatalf("failed to parse base CRL: %v", err)
}
t.Run("base crl in cache has expired", func(t *testing.T) {
ctx := context.Background()
expiredBundle := &corecrl.Bundle{BaseCRL: expiredBaseCRL}
if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil {
t.Fatal(err)
}
_, err = cache.Get(ctx, "expiredKey")
if !errors.Is(err, corecrl.ErrCacheMiss) {
t.Fatalf("expected ErrCacheMiss, but got %v", err)
}
})
t.Run("delta crl in cache has expired", func(t *testing.T) {
ctx := context.Background()
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
Number: big.NewInt(1),
NextUpdate: now.Add(time.Hour),
}, certChain[1].Cert, certChain[1].PrivateKey)
if err != nil {
t.Fatalf("failed to create base CRL: %v", err)
}
baseCRL, err := x509.ParseRevocationList(crlBytes)
if err != nil {
t.Fatalf("failed to parse base CRL: %v", err)
}
crlBytes, err = x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
Number: big.NewInt(1),
NextUpdate: now.Add(-time.Hour),
}, certChain[1].Cert, certChain[1].PrivateKey)
if err != nil {
t.Fatalf("failed to create base CRL: %v", err)
}
expiredDeltaCRL, err := x509.ParseRevocationList(crlBytes)
if err != nil {
t.Fatalf("failed to parse base CRL: %v", err)
}
expiredBundle := &corecrl.Bundle{BaseCRL: baseCRL, DeltaCRL: expiredDeltaCRL}
if err := cache.Set(ctx, "expiredKey", expiredBundle); err != nil {
t.Fatal(err)
}
_, err = cache.Get(ctx, "expiredKey")
if !errors.Is(err, corecrl.ErrCacheMiss) {
t.Fatalf("expected ErrCacheMiss, but got %v", err)
}
})
}
func TestSetFailed(t *testing.T) {
tempDir := t.TempDir()
cache, err := NewFileCache(tempDir)
if err != nil {
t.Fatal(err)
}
now := time.Now()
certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true)
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
Number: big.NewInt(1),
NextUpdate: now.Add(time.Hour),
}, certChain[1].Cert, certChain[1].PrivateKey)
if err != nil {
t.Fatal(err)
}
baseCRL, err := x509.ParseRevocationList(crlBytes)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
key := "testKey"
t.Run("nil bundle", func(t *testing.T) {
err := cache.Set(ctx, key, nil)
expectedErrMsg := "failed to store crl bundle in file cache: bundle cannot be nil"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
}
})
t.Run("nil bundle BaseCRL", func(t *testing.T) {
bundle := &corecrl.Bundle{}
err := cache.Set(ctx, key, bundle)
expectedErrMsg := "failed to store crl bundle in file cache: bundle BaseCRL cannot be nil"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %v", expectedErrMsg, err)
}
})
t.Run("failed to write into cache due to permission denied", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
if err := os.Chmod(tempDir, 0); err != nil {
t.Fatal(err)
}
bundle := &corecrl.Bundle{BaseCRL: baseCRL}
err := cache.Set(ctx, key, bundle)
if err == nil || !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("expected permission denied error, but got %v", err)
}
// restore permission
if err := os.Chmod(tempDir, 0755); err != nil {
t.Fatalf("failed to change permission: %v", err)
}
})
}

View File

@ -47,7 +47,7 @@ var VerificationPluginHeaders = []string{
var errExtendedAttributeNotExist = errors.New("extended attribute not exist")
func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, policy *trustpolicy.TrustPolicy, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) {
func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, policyName string, trustStores []string, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) {
var typeToLoad truststore.Type
switch scheme {
case signature.SigningSchemeX509:
@ -57,37 +57,13 @@ func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, po
default:
return nil, truststore.TrustStoreError{Msg: fmt.Sprintf("error while loading the trust store, unrecognized signing scheme %q", scheme)}
}
processedStoreSet := set.New[string]()
var certificates []*x509.Certificate
for _, trustStore := range policy.TrustStores {
if processedStoreSet.Contains(trustStore) {
// we loaded this trust store already
continue
}
storeType, name, found := strings.Cut(trustStore, ":")
if !found {
return nil, truststore.TrustStoreError{Msg: fmt.Sprintf("error while loading the trust store, trust policy statement %q is missing separator in trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", policy.Name, trustStore)}
}
if typeToLoad != truststore.Type(storeType) {
continue
}
certs, err := x509TrustStore.GetCertificates(ctx, typeToLoad, name)
if err != nil {
return nil, err
}
certificates = append(certificates, certs...)
processedStoreSet.Add(trustStore)
}
return certificates, nil
return loadX509TrustStoresWithType(ctx, typeToLoad, policyName, trustStores, x509TrustStore)
}
// isCriticalFailure checks whether a VerificationResult fails the entire
// signature verification workflow.
// isCriticalFailure checks whether a [notation.ValidationResult] fails the
// entire signature verification workflow.
// signature verification workflow is considered failed if there is a
// VerificationResult with "Enforced" as the action but the result was
// ValidationResult with "Enforced" as the action but the result was
// unsuccessful.
func isCriticalFailure(result *notation.ValidationResult) bool {
return result.Action == trustpolicy.ActionEnforce && result.Error != nil
@ -154,3 +130,56 @@ func getVerificationPluginMinVersion(signerInfo *signature.SignerInfo) (string,
}
return version, nil
}
func loadX509TSATrustStores(ctx context.Context, scheme signature.SigningScheme, policyName string, trustStores []string, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) {
var typeToLoad truststore.Type
switch scheme {
case signature.SigningSchemeX509:
typeToLoad = truststore.TypeTSA
default:
return nil, truststore.TrustStoreError{Msg: fmt.Sprintf("error while loading the TSA trust store, signing scheme must be notary.x509, but got %s", scheme)}
}
return loadX509TrustStoresWithType(ctx, typeToLoad, policyName, trustStores, x509TrustStore)
}
func loadX509TrustStoresWithType(ctx context.Context, trustStoreType truststore.Type, policyName string, trustStores []string, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) {
processedStoreSet := set.New[string]()
var certificates []*x509.Certificate
for _, trustStore := range trustStores {
if processedStoreSet.Contains(trustStore) {
// we loaded this trust store already
continue
}
storeType, name, found := strings.Cut(trustStore, ":")
if !found {
return nil, truststore.TrustStoreError{Msg: fmt.Sprintf("error while loading the trust store, trust policy statement %q is missing separator in trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", policyName, trustStore)}
}
if trustStoreType != truststore.Type(storeType) {
continue
}
certs, err := x509TrustStore.GetCertificates(ctx, trustStoreType, name)
if err != nil {
return nil, err
}
certificates = append(certificates, certs...)
processedStoreSet.Add(trustStore)
}
return certificates, nil
}
// isTSATrustStoreInPolicy checks if tsa trust store is configured in
// trust policy
func isTSATrustStoreInPolicy(policyName string, trustStores []string) (bool, error) {
for _, trustStore := range trustStores {
storeType, _, found := strings.Cut(trustStore, ":")
if !found {
return false, truststore.TrustStoreError{Msg: fmt.Sprintf("invalid trust policy statement: %q is missing separator in trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", policyName, trustStore)}
}
if truststore.Type(storeType) == truststore.TypeTSA {
return true, nil
}
}
return false, nil
}

View File

@ -28,25 +28,6 @@ import (
"github.com/notaryproject/notation-go/verifier/truststore"
)
func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
policyStatement = trustpolicy.TrustPolicy{
Name: "test-statement-name",
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
}
return
}
func dummyPolicyDocument() (policyDoc trustpolicy.Document) {
policyDoc = trustpolicy.Document{
Version: "1.0",
TrustPolicies: []trustpolicy.TrustPolicy{dummyPolicyStatement()},
}
return
}
func TestGetArtifactDigestFromUri(t *testing.T) {
tests := []struct {
@ -79,21 +60,18 @@ func TestLoadX509TrustStore(t *testing.T) {
// load "ca" and "signingAuthority" trust store
caStore := "ca:valid-trust-store"
signingAuthorityStore := "signingAuthority:valid-trust-store"
dummyPolicy := dummyPolicyStatement()
dummyPolicy := dummyOCIPolicyDocument().TrustPolicies[0]
dummyPolicy.TrustStores = []string{caStore, signingAuthorityStore}
dir.UserConfigDir = "testdata"
x509truststore := truststore.NewX509TrustStore(dir.ConfigFS())
caCerts, err := loadX509TrustStores(context.Background(), signature.SigningSchemeX509, &dummyPolicy, x509truststore)
_, err := loadX509TrustStores(context.Background(), signature.SigningSchemeX509, dummyPolicy.Name, dummyPolicy.TrustStores, x509truststore)
if err != nil {
t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err)
}
saCerts, err := loadX509TrustStores(context.Background(), signature.SigningSchemeX509SigningAuthority, &dummyPolicy, x509truststore)
_, err = loadX509TrustStores(context.Background(), signature.SigningSchemeX509SigningAuthority, dummyPolicy.Name, dummyPolicy.TrustStores, x509truststore)
if err != nil {
t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err)
}
if len(caCerts) != 4 || len(saCerts) != 3 {
t.Fatalf("ca store should have 4 certs and signingAuthority store should have 3 certs")
}
}
func TestIsCriticalFailure(t *testing.T) {
@ -117,6 +95,34 @@ func TestIsCriticalFailure(t *testing.T) {
}
}
func TestLoadX509TSATrustStores(t *testing.T) {
policyDoc := trustpolicy.Document{
Version: "1.0",
TrustPolicies: []trustpolicy.TrustPolicy{
{
Name: "testTSA",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
TrustStores: []string{"tsa:test-timestamp"},
TrustedIdentities: []string{"*"},
},
},
}
dir.UserConfigDir = "testdata"
x509truststore := truststore.NewX509TrustStore(dir.ConfigFS())
policyStatement := policyDoc.TrustPolicies[0]
_, err := loadX509TSATrustStores(context.Background(), signature.SigningSchemeX509, policyStatement.Name, policyStatement.TrustStores, x509truststore)
if err != nil {
t.Fatalf("TestLoadX509TrustStore should not throw error for a valid trust store. Error: %v", err)
}
_, err = loadX509TSATrustStores(context.Background(), signature.SigningSchemeX509SigningAuthority, policyStatement.Name, policyStatement.TrustStores, x509truststore)
expectedErrMsg := "error while loading the TSA trust store, signing scheme must be notary.x509, but got notary.x509.signingAuthority"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
}
func getArtifactDigestFromReference(artifactReference string) (string, error) {
invalidUriErr := fmt.Errorf("artifact URI %q could not be parsed, make sure it is the fully qualified OCI artifact URI without the scheme/protocol. e.g domain.com:80/my/repository@sha256:digest", artifactReference)
i := strings.LastIndex(artifactReference, "@")
@ -131,3 +137,32 @@ func getArtifactDigestFromReference(artifactReference string) (string, error) {
return artifactReference[i+1:], nil
}
func dummyOCIPolicyDocument() (policyDoc trustpolicy.OCIDocument) {
return trustpolicy.OCIDocument{
Version: "1.0",
TrustPolicies: []trustpolicy.OCITrustPolicy{
{
Name: "test-statement-name",
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
},
},
}
}
func dummyBlobPolicyDocument() (policyDoc trustpolicy.BlobDocument) {
return trustpolicy.BlobDocument{
Version: "1.0",
TrustPolicies: []trustpolicy.BlobTrustPolicy{
{
Name: "blob-test-statement-name",
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
},
},
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"payload":"eyJ0YXJnZXRBcnRpZmFjdCI6eyJkaWdlc3QiOiJzaGEyNTY6YzA2NjllZjM0Y2RjMTQzMzJjMGYxYWIwYzJjMDFhY2I5MWQ5NjAxNGIxNzJmMWE3NmYzYTM5ZTYzZDFmMGJkYSIsIm1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuZGlzdHJpYnV0aW9uLm1hbmlmZXN0LnYyK2pzb24iLCJzaXplIjo1Mjh9fQ","protected":"eyJhbGciOiJQUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSJdLCJjdHkiOiJhcHBsaWNhdGlvbi92bmQuY25jZi5ub3RhcnkucGF5bG9hZC52MStqc29uIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDI0LTA2LTE4VDE3OjA4OjM1KzA4OjAwIn0","header":{"x5c":["MIIDPjCCAiagAwIBAgIBeTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzELMAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEPMA0GA1UEAxMGYWxwaW5lMB4XDTIzMDUwOTA0NTUxMloXDTMzMDUxMDA0NTUxMlowTjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxDzANBgNVBAMTBmFscGluZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5hpq1229GGLjMK6i9KZhuUO+SV7rUFnWIDiIPO5yWxYDkl+bGroeAvJYu6MVCMQ6FMRXD9jhnG6R+sAHwY7gVgcJ1OXak87PkLp/Ii1Cr7XkkySZeD+Br1vSQzfxs3pFG+iBCeVVkeZdsg+xqwnAlqAILXwIbTGRyJP1Xiu9nwOeuX1YmxPl2m29Pt1EtfVCL9COsVKt5LgOVyWP/9ISWevOBqSCU9bk35HFo9VTeUf6+ffhSMjv0Y9uwkFFOKXpcV8Sa3ArqyBmgQlUfGg1iwYlqiDE0fTYxiB3gLgETAlmTm50J+WB9LoDrnrQpbXFLoegm+JV+uSD8J8H7DL2sCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAt0Nvna1c4pPn8kzoN5VvmFmeIgdO/BJpmdhdg0WIQ9aeN/xPXXaVjPp1Mk7edXHAvBwQr0Gyzqyy7g/h0gdnAFG7f6blrRNzbrRBCq6cNqX8iwgK/9+2OYKxk1QWj8Gx0cvu1DN1aXjPPGgQ2j3tHjJvJv32J/zuZa8gU40RPPSLaBlc5ZjpFmyi29sKlTeeZ+F/Ssic51qXXw2CsYGGWK5yQ3xSCxbw6bb2G/s/YI7/KlWg9BktBJHzRu04ZNR77W7/dyJ3Lj17PlW1XKmMOFHsQivagXeRCbmYZ43fX4ugFRFKL7KE0EgmGOWpJ0xv+6ig93sqHzQ/0uv1YgFov"],"io.cncf.notary.signingAgent":"Notation/1.0.0"},"signature":"ToCyclYJtk-Gtb13j1sWW7FQ7iZA9Vq6u_x6nJD3pRkBXhtatvSBsaZ_mqFHKrJWEY3UOBzi2SYobCQYww0cVwbzeDetPhjBhmH-bW-N_pbjGntgB2K1owvJnlycUoOfC2RQ1eDa4mC7Dj1mKzA5Tb-qnNbrT75pvQKZjTY1RZaN6p_xKBJA-AAiQrgHEvlf4m8ZbvqtZ0x4_uiGwfWoNCqPtrZK71mEpPSjfOT3mN5FkZqY0L3jSKRtFRLd1rb0UA2RB-E0CshsNb-hJgTX4SIzUlgcVT10SJnKw0yy_QqrxhMlejOUiV8HHKgbsZqQg1kwFjP5QwzWr5HB6vbRzg"}

View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDPjCCAiagAwIBAgIBeTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP
MA0GA1UEAxMGYWxwaW5lMB4XDTIzMDUwOTA0NTUxMloXDTMzMDUxMDA0NTUxMlow
TjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8w
DQYDVQQKEwZOb3RhcnkxDzANBgNVBAMTBmFscGluZTCCASIwDQYJKoZIhvcNAQEB
BQADggEPADCCAQoCggEBAK5hpq1229GGLjMK6i9KZhuUO+SV7rUFnWIDiIPO5yWx
YDkl+bGroeAvJYu6MVCMQ6FMRXD9jhnG6R+sAHwY7gVgcJ1OXak87PkLp/Ii1Cr7
XkkySZeD+Br1vSQzfxs3pFG+iBCeVVkeZdsg+xqwnAlqAILXwIbTGRyJP1Xiu9nw
OeuX1YmxPl2m29Pt1EtfVCL9COsVKt5LgOVyWP/9ISWevOBqSCU9bk35HFo9VTeU
f6+ffhSMjv0Y9uwkFFOKXpcV8Sa3ArqyBmgQlUfGg1iwYlqiDE0fTYxiB3gLgETA
lmTm50J+WB9LoDrnrQpbXFLoegm+JV+uSD8J8H7DL2sCAwEAAaMnMCUwDgYDVR0P
AQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IB
AQAt0Nvna1c4pPn8kzoN5VvmFmeIgdO/BJpmdhdg0WIQ9aeN/xPXXaVjPp1Mk7ed
XHAvBwQr0Gyzqyy7g/h0gdnAFG7f6blrRNzbrRBCq6cNqX8iwgK/9+2OYKxk1QWj
8Gx0cvu1DN1aXjPPGgQ2j3tHjJvJv32J/zuZa8gU40RPPSLaBlc5ZjpFmyi29sKl
TeeZ+F/Ssic51qXXw2CsYGGWK5yQ3xSCxbw6bb2G/s/YI7/KlWg9BktBJHzRu04Z
NR77W7/dyJ3Lj17PlW1XKmMOFHsQivagXeRCbmYZ43fX4ugFRFKL7KE0EgmGOWpJ
0xv+6ig93sqHzQ/0uv1YgFov
-----END CERTIFICATE-----

View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDRTCCAi2gAwIBAgICAKYwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UEBhMCVVMx
CzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3Rhcnkx
EDAOBgNVBAMTB3Rlc3RUU0EwIhgPMjA5OTA5MTgxMTU0MzRaGA8yMTAwMDkxODEx
NTQzNFowTzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQHEwdTZWF0
dGxlMQ8wDQYDVQQKEwZOb3RhcnkxEDAOBgNVBAMTB3Rlc3RUU0EwggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDI7xKl3GyBZregnHgxUw7rb3yO5jSo31Pa
+EhxghQ0/rRKc/1DtfMQURjDYDdjqRmEXq8rVyEAuaBXSKqBMq9bazP7Ot8N/B0O
gRCgXwizn//Ha5XfpHqV9lUud4oztdxapejfT6UQSIVqtgWEbZkr4N74G5NV13Ll
ITtWmHpTLo2LfE7jAXTaoCjo/U/eVFFc6X7jyXwaAVyNC2Pi45d/GOaFx/MGHnK6
zbN8PeIh5KqInp0UNcHZLBbduxWQhdISULR/x6pVocqExv6zLmRbn5I65wrYL/8g
pQPTeZv4S2COpB+25Xy8oyaM6tPa96Pi1NIXtChWO8+muXj1Z4VfAgMBAAGjJzAl
MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzANBgkqhkiG9w0B
AQsFAAOCAQEAXFaaITvi3skq+czzmbyebtrAa8I9iEbjmWSPjoaUir2NYOLWsyQ7
+gkBlMcw5+anP+BC98VBgNVjuQ5oXwdu57xouW7jk/dI5uuKLOFxFdCG7FwW3ycD
6GGgj+/2LthxNOxc7CnnMjUuSw2FKJKesiuHQJpdPjgw9cKs+fZF5tr6ZhX4yAUF
qouZJ7Hc5JSj3zyEpIbFapVpSAK8O1/mct4KDtt1SmyYn34o55ggyLurrlZ9ctQW
HT8xyjc6+b4lEKbilA+xjTt+/BLIs/v/8CVIUzz6OzTCwBraj3kayM7CdGKSysoc
nJZ/yUcHVw1hLs1+JIMj75i0T6s+GtuT4A==
-----END CERTIFICATE-----

View File

@ -0,0 +1,20 @@
-----BEGIN CERTIFICATE-----
MIIDVjCCAj6gAwIBAgIBUTANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJVUzEL
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEb
MBkGA1UEAxMSd2FiYml0LW5ldHdvcmtzLmlvMB4XDTIzMDExOTA4MTkwN1oXDTMz
MDExOTA4MTkwN1owWjELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAldBMRAwDgYDVQQH
EwdTZWF0dGxlMQ8wDQYDVQQKEwZOb3RhcnkxGzAZBgNVBAMTEndhYmJpdC1uZXR3
b3Jrcy5pbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANHhlP+SiY7h
sGlf2mADOzJW/J9siqMkiQvSOx0OSM2yxetfVQL/abi4iqCXM6wkSxviBeNwIoYE
s4thMA8NGEbnKoXktyh9vmiLB1FW7HHr4QLwjgLzgWJKIQTy1JmDBecXZh56d0f3
w3Yj1IDTvkIScXCNI+5v/08GUQKhyBwv7Fq9MYpo2lfXSI7V33BKKddXIxPGVWwK
GvPE0sg2VV7WM84ZZLdDKz2mq0PtPTHrSwg3hlK/mjn+blg3gsYQ4h9/7Z6nNaF9
X0SdyESl841ZWrtMhAOFpIzLbz9ete8NRd3bYCRBIr5gscHWTf6lyUgy4xzsSwMH
PsGLM4A+Z00CAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsG
AQUFBwMDMA0GCSqGSIb3DQEBCwUAA4IBAQAbN0Eru56uTQSC28ZTf8D7VyCkYrrW
LYiJMYdOKBzzKV9mKaM0OGF2uyWwDaPxp9KTdLXmBp9EFq5SXXArFA+nRS7KinDA
e2O7A/9Std2XjKi927rkA2cj239d5lRsjWXqJXf9vAMV9a2FjUM/in2Eevlq7bvj
FE3l26VXCKtOs9ErmfxrL+6ETRKSVYOOG/rSHFv/SB2MlqDg5QsXC9lZjzL5/X/i
oe2qZKhp6X5DPpad1q1Q4ItKdTN+2EXyMyoHn1BJKNba7CUUvXf03EJebT/Im+qo
zfEksJeZJUSlSujANUPoCpsEYGWWQx5G+ViG05Sqs+6ppKrut+P+DVPo
-----END CERTIFICATE-----

425
verifier/timestamp_test.go Normal file
View File

@ -0,0 +1,425 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package verifier
import (
"context"
"crypto/x509"
"net/http"
"os"
"testing"
"time"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/notation-core-go/revocation/purpose"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-core-go/signature/cose"
"github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation-go/verifier/truststore"
)
func TestAuthenticTimestamp(t *testing.T) {
dir.UserConfigDir = "testdata"
trustStore := truststore.NewX509TrustStore(dir.ConfigFS())
dummyTrustPolicy := &trustpolicy.TrustPolicy{
Name: "test-timestamp",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
VerifyTimestamp: trustpolicy.OptionAlways,
},
TrustStores: []string{"ca:valid-trust-store", "tsa:test-timestamp"},
TrustedIdentities: []string{"*"},
}
revocationTimestampingValidator, err := revocation.NewWithOptions(revocation.Options{
OCSPHTTPClient: &http.Client{Timeout: 2 * time.Second},
CertChainPurpose: purpose.Timestamping,
})
if err != nil {
t.Fatalf("failed to get revocation timestamp client: %v", err)
}
// valid JWS signature envelope with timestamp countersignature
jwsEnvContent, err := parseEnvContent("testdata/timestamp/sigEnv/jwsWithTimestamp.sig", jws.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
// valid COSE signature envelope with timestamp countersignature
coseEnvContent, err := parseEnvContent("testdata/timestamp/sigEnv/coseWithTimestamp.sig", cose.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
t.Run("verify Authentic Timestamp with jws format", func(t *testing.T) {
outcome := &notation.VerificationOutcome{
EnvelopeContent: jwsEnvContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
if err := authenticTimestampResult.Error; err != nil {
t.Fatalf("expected nil error, but got %s", err)
}
})
t.Run("verify Authentic Timestamp with cose format", func(t *testing.T) {
outcome := &notation.VerificationOutcome{
EnvelopeContent: coseEnvContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
if err := authenticTimestampResult.Error; err != nil {
t.Fatalf("expected nil error, but got %s", err)
}
})
t.Run("verify Authentic Timestamp jws with expired codeSigning cert", func(t *testing.T) {
jwsEnvContent, err := parseEnvContent("testdata/timestamp/sigEnv/jwsExpiredWithTimestamp.sig", jws.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: jwsEnvContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
if err := authenticTimestampResult.Error; err != nil {
t.Fatalf("expected nil error, but got %s", err)
}
})
t.Run("verify Authentic Timestamp cose with expired codeSigning cert", func(t *testing.T) {
coseEnvContent, err := parseEnvContent("testdata/timestamp/sigEnv/coseExpiredWithTimestamp.sig", cose.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: coseEnvContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
if err := authenticTimestampResult.Error; err != nil {
t.Fatalf("expected nil error, but got %s", err)
}
})
t.Run("verify Authentic Timestamp with afterCertExpiry set", func(t *testing.T) {
dummyTrustPolicy := &trustpolicy.TrustPolicy{
Name: "test-timestamp",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
VerifyTimestamp: trustpolicy.OptionAfterCertExpiry,
},
TrustStores: []string{"ca:valid-trust-store", "tsa:test-timestamp"},
TrustedIdentities: []string{"*"},
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: coseEnvContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
if err := authenticTimestampResult.Error; err != nil {
t.Fatalf("expected nil error, but got %s", err)
}
})
t.Run("verify Authentic Timestamp failed due to invalid trust policy", func(t *testing.T) {
dummyTrustPolicy := &trustpolicy.TrustPolicy{
Name: "test-timestamp",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
VerifyTimestamp: trustpolicy.OptionAlways,
},
TrustStores: []string{"ca:valid-trust-store", "tsa"},
TrustedIdentities: []string{"*"},
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: jwsEnvContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "failed to check tsa trust store configuration in turst policy with error: invalid trust policy statement: \"test-timestamp\" is missing separator in trust store value \"tsa\". The required format is <TrustStoreType>:<TrustStoreName>"
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to missing tsa in trust policy and expired codeSigning cert", func(t *testing.T) {
dummyTrustPolicy := &trustpolicy.TrustPolicy{
Name: "test-timestamp",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
VerifyTimestamp: trustpolicy.OptionAlways,
},
TrustStores: []string{"ca:valid-trust-store"},
TrustedIdentities: []string{"*"},
}
coseEnvContent, err := parseEnvContent("testdata/timestamp/sigEnv/coseExpiredWithTimestamp.sig", cose.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: coseEnvContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "verification time is after certificate \"CN=testTSA,O=Notary,L=Seattle,ST=WA,C=US\" validity period, it was expired at \"Tue, 18 Jun 2024 07:30:31 +0000\""
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to missing timestamp countersignature", func(t *testing.T) {
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/withoutTimestamp.sig", jws.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: envContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "no timestamp countersignature was found in the signature envelope"
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to invalid timestamp countersignature content type", func(t *testing.T) {
signedToken, err := os.ReadFile("testdata/timestamp/countersignature/TimeStampTokenWithInvalideContentType.p7s")
if err != nil {
t.Fatalf("failed to get signedToken: %v", err)
}
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/withoutTimestamp.sig", jws.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
envContent.SignerInfo.UnsignedAttributes.TimestampSignature = signedToken
outcome := &notation.VerificationOutcome{
EnvelopeContent: envContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "failed to parse timestamp countersignature with error: unexpected content type: 1.2.840.113549.1.7.1. Expected to be id-ct-TSTInfo (1.2.840.113549.1.9.16.1.4)"
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to invalid TSTInfo", func(t *testing.T) {
signedToken, err := os.ReadFile("testdata/timestamp/countersignature/TimeStampTokenWithInvalidTSTInfo.p7s")
if err != nil {
t.Fatalf("failed to get signedToken: %v", err)
}
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/withoutTimestamp.sig", jws.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
envContent.SignerInfo.UnsignedAttributes.TimestampSignature = signedToken
outcome := &notation.VerificationOutcome{
EnvelopeContent: envContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "failed to get the timestamp TSTInfo with error: cannot unmarshal TSTInfo from timestamp token: asn1: structure error: tags don't match (23 vs {class:0 tag:16 length:3 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:24 set:false omitEmpty:false} Time @89"
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to failed to validate TSTInfo", func(t *testing.T) {
signedToken, err := os.ReadFile("testdata/timestamp/countersignature/TimeStampToken.p7s")
if err != nil {
t.Fatalf("failed to get signedToken: %v", err)
}
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/withoutTimestamp.sig", jws.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
envContent.SignerInfo.UnsignedAttributes.TimestampSignature = signedToken
envContent.SignerInfo.Signature = []byte("mismatch")
outcome := &notation.VerificationOutcome{
EnvelopeContent: envContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "failed to get timestamp from timestamp countersignature with error: invalid TSTInfo: mismatched message"
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to failed to verify timestamp countersignature", func(t *testing.T) {
signedToken, err := os.ReadFile("testdata/timestamp/countersignature/TimeStampTokenWithoutCertificate.p7s")
if err != nil {
t.Fatalf("failed to get signedToken: %v", err)
}
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/withoutTimestamp.sig", jws.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
envContent.SignerInfo.UnsignedAttributes.TimestampSignature = signedToken
envContent.SignerInfo.Signature = []byte("notation")
outcome := &notation.VerificationOutcome{
EnvelopeContent: envContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "failed to verify the timestamp countersignature with error: failed to verify signed token: signing certificate not found in the timestamp token"
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to trust store does not exist", func(t *testing.T) {
dummyTrustPolicy := &trustpolicy.TrustPolicy{
Name: "test-timestamp",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
VerifyTimestamp: trustpolicy.OptionAlways,
},
TrustStores: []string{"ca:valid-trust-store", "tsa:does-not-exist"},
TrustedIdentities: []string{"*"},
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: coseEnvContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "failed to load tsa trust store with error: the trust store \"does-not-exist\" of type \"tsa\" does not exist"
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to empty trust store", func(t *testing.T) {
dummyTrustPolicy := &trustpolicy.TrustPolicy{
Name: "test-timestamp",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
VerifyTimestamp: trustpolicy.OptionAlways,
},
TrustStores: []string{"ca:valid-trust-store", "tsa:test-empty"},
TrustedIdentities: []string{"*"},
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: coseEnvContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, dummyTrustStore{}, revocationTimestampingValidator, outcome)
expectedErrMsg := "no trusted TSA certificate found in trust store"
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to tsa not trust", func(t *testing.T) {
dummyTrustPolicy := &trustpolicy.TrustPolicy{
Name: "test-timestamp",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
VerifyTimestamp: trustpolicy.OptionAlways,
},
TrustStores: []string{"ca:valid-trust-store", "tsa:test-mismatch"},
TrustedIdentities: []string{"*"},
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: coseEnvContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "failed to verify the timestamp countersignature with error: failed to verify signed token: cms verification failure: x509: certificate signed by unknown authority"
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to timestamp before signing cert not before", func(t *testing.T) {
dummyTrustPolicy := &trustpolicy.TrustPolicy{
Name: "test-timestamp",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
VerifyTimestamp: trustpolicy.OptionAlways,
},
TrustStores: []string{"ca:valid-trust-store", "tsa:test-timestamp"},
TrustedIdentities: []string{"*"},
}
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/timestampBeforeNotBefore.sig", jws.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: envContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "timestamp can be before certificate \"CN=testTSA,O=Notary,L=Seattle,ST=WA,C=US\" validity period, it will be valid from \"Fri, 18 Sep 2099 11:54:34 +0000\""
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
t.Run("verify Authentic Timestamp failed due to timestamp after signing cert not after", func(t *testing.T) {
dummyTrustPolicy := &trustpolicy.TrustPolicy{
Name: "test-timestamp",
RegistryScopes: []string{"*"},
SignatureVerification: trustpolicy.SignatureVerification{
VerificationLevel: trustpolicy.LevelStrict.Name,
VerifyTimestamp: trustpolicy.OptionAlways,
},
TrustStores: []string{"ca:valid-trust-store", "tsa:test-timestamp"},
TrustedIdentities: []string{"*"},
}
envContent, err := parseEnvContent("testdata/timestamp/sigEnv/timestampAfterNotAfter.sig", cose.MediaTypeEnvelope)
if err != nil {
t.Fatalf("failed to get signature envelope content: %v", err)
}
outcome := &notation.VerificationOutcome{
EnvelopeContent: envContent,
VerificationLevel: trustpolicy.LevelStrict,
}
authenticTimestampResult := verifyAuthenticTimestamp(context.Background(), dummyTrustPolicy.Name, dummyTrustPolicy.TrustStores, dummyTrustPolicy.SignatureVerification, trustStore, revocationTimestampingValidator, outcome)
expectedErrMsg := "timestamp can be after certificate \"CN=testTSA,O=Notary,L=Seattle,ST=WA,C=US\" validity period, it was expired at \"Tue, 18 Sep 2001 11:54:34 +0000\""
if err := authenticTimestampResult.Error; err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
})
}
func parseEnvContent(filepath, format string) (*signature.EnvelopeContent, error) {
sigEnvBytes, err := os.ReadFile(filepath)
if err != nil {
return nil, err
}
sigEnv, err := signature.ParseEnvelope(format, sigEnvBytes)
if err != nil {
return nil, err
}
return sigEnv.Content()
}
type dummyTrustStore struct{}
func (ts dummyTrustStore) GetCertificates(ctx context.Context, storeType truststore.Type, namedStore string) ([]*x509.Certificate, error) {
return nil, nil
}

View File

@ -0,0 +1,148 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package trustpolicy
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/notaryproject/notation-go/dir"
set "github.com/notaryproject/notation-go/internal/container"
"github.com/notaryproject/notation-go/internal/slices"
)
// BlobDocument represents a trustpolicy.blob.json document for arbitrary blobs
type BlobDocument struct {
// Version of the policy document
Version string `json:"version"`
// TrustPolicies include each policy statement
TrustPolicies []BlobTrustPolicy `json:"trustPolicies"`
}
// BlobTrustPolicy represents a policy statement in the blob trust policy
// document
type BlobTrustPolicy struct {
// Name of the policy statement
Name string `json:"name"`
// SignatureVerification setting for this policy statement
SignatureVerification SignatureVerification `json:"signatureVerification"`
// TrustStores this policy statement uses
TrustStores []string `json:"trustStores"`
// TrustedIdentities this policy statement pins
TrustedIdentities []string `json:"trustedIdentities"`
// GlobalPolicy defines if policy statement is global or not
GlobalPolicy bool `json:"globalPolicy,omitempty"`
}
var supportedBlobPolicyVersions = []string{"1.0"}
// LoadBlobDocument loads a blob trust policy document from a local file system
func LoadBlobDocument() (*BlobDocument, error) {
var doc BlobDocument
err := getDocument(dir.PathBlobTrustPolicy, &doc)
return &doc, err
}
// Validate validates a blob trust policy document according to its version's
// rule set.
// If any rule is violated, returns an error.
func (policyDoc *BlobDocument) Validate() error {
// sanity check
if policyDoc == nil {
return errors.New("blob trust policy document cannot be nil")
}
// Validate Version
if policyDoc.Version == "" {
return errors.New("blob trust policy document has empty version, version must be specified")
}
if !slices.Contains(supportedBlobPolicyVersions, policyDoc.Version) {
return fmt.Errorf("blob trust policy document uses unsupported version %q", policyDoc.Version)
}
// Validate the policy according to 1.0 rules
if len(policyDoc.TrustPolicies) == 0 {
return errors.New("blob trust policy document can not have zero trust policy statements")
}
policyNames := set.New[string]()
var foundGlobalPolicy bool
for _, statement := range policyDoc.TrustPolicies {
// Verify unique policy statement names across the policy document
if policyNames.Contains(statement.Name) {
return fmt.Errorf("multiple blob trust policy statements use the same name %q, statement names must be unique", statement.Name)
}
if err := validatePolicyCore(statement.Name, statement.SignatureVerification, statement.TrustStores, statement.TrustedIdentities); err != nil {
return fmt.Errorf("blob trust policy: %w", err)
}
if statement.GlobalPolicy {
if foundGlobalPolicy {
return errors.New("multiple blob trust policy statements have globalPolicy set to true. Only one trust policy statement can be marked as global policy")
}
// verificationLevel is skip
if reflect.DeepEqual(statement.SignatureVerification.VerificationLevel, LevelSkip) {
return errors.New("global blob trust policy statement cannot have verification level set to skip")
}
foundGlobalPolicy = true
}
policyNames.Add(statement.Name)
}
return nil
}
// GetApplicableTrustPolicy returns a pointer to the deep copied [BlobTrustPolicy]
// for given policy name.
// see https://github.com/notaryproject/specifications/tree/9c81dc773508dedc5a81c02c8d805de04f65050b/specs/trust-store-trust-policy.md#blob-trust-policy
func (policyDoc *BlobDocument) GetApplicableTrustPolicy(policyName string) (*BlobTrustPolicy, error) {
if strings.TrimSpace(policyName) == "" {
return nil, errors.New("policy name cannot be empty")
}
for _, policyStatement := range policyDoc.TrustPolicies {
// exact match
if policyStatement.Name == policyName {
return (&policyStatement).clone(), nil
}
}
return nil, fmt.Errorf("no applicable blob trust policy with name %q", policyName)
}
// GetGlobalTrustPolicy returns a pointer to the deep copied [BlobTrustPolicy]
// that is marked as global policy.
// see https://github.com/notaryproject/specifications/tree/9c81dc773508dedc5a81c02c8d805de04f65050b/specs/trust-store-trust-policy.md#blob-trust-policy
func (policyDoc *BlobDocument) GetGlobalTrustPolicy() (*BlobTrustPolicy, error) {
for _, policyStatement := range policyDoc.TrustPolicies {
if policyStatement.GlobalPolicy {
return (&policyStatement).clone(), nil
}
}
return nil, fmt.Errorf("no global blob trust policy")
}
// clone returns a pointer to the deep copied [BlobTrustPolicy]
func (t *BlobTrustPolicy) clone() *BlobTrustPolicy {
return &BlobTrustPolicy{
Name: t.Name,
SignatureVerification: t.SignatureVerification,
TrustedIdentities: append([]string(nil), t.TrustedIdentities...),
TrustStores: append([]string(nil), t.TrustStores...),
GlobalPolicy: t.GlobalPolicy,
}
}

View File

@ -0,0 +1,168 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package trustpolicy
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"testing"
"github.com/notaryproject/notation-go/dir"
)
func TestLoadBlobDocument(t *testing.T) {
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
path := filepath.Join(tempRoot, "trustpolicy.blob.json")
policyJson, _ := json.Marshal(dummyBlobPolicyDocument())
if err := os.WriteFile(path, policyJson, 0600); err != nil {
t.Fatalf("TestLoadBlobDocument write policy file failed. Error: %v", err)
}
t.Cleanup(func() { os.RemoveAll(tempRoot) })
if _, err := LoadBlobDocument(); err != nil {
t.Fatalf("LoadBlobDocument() should not throw error for an existing policy file. Error: %v", err)
}
}
func TestValidate_BlobDocument(t *testing.T) {
policyDoc := dummyBlobPolicyDocument()
if err := policyDoc.Validate(); err != nil {
t.Fatalf("Validate() returned error: %v", err)
}
}
func TestValidate_BlobDocument_Error(t *testing.T) {
// Sanity check
var nilPolicyDoc *BlobDocument
err := nilPolicyDoc.Validate()
if err == nil || err.Error() != "blob trust policy document cannot be nil" {
t.Fatalf("nil policyDoc should return error")
}
// empty Version
policyDoc := dummyBlobPolicyDocument()
policyDoc.Version = ""
err = policyDoc.Validate()
if err == nil || err.Error() != "blob trust policy document has empty version, version must be specified" {
t.Fatalf("empty version should return error")
}
// Invalid Version
policyDoc = dummyBlobPolicyDocument()
policyDoc.Version = "invalid"
err = policyDoc.Validate()
if err == nil || err.Error() != "blob trust policy document uses unsupported version \"invalid\"" {
t.Fatalf("invalid version should return error")
}
// No Policy Statements
policyDoc = dummyBlobPolicyDocument()
policyDoc.TrustPolicies = nil
err = policyDoc.Validate()
if err == nil || err.Error() != "blob trust policy document can not have zero trust policy statements" {
t.Fatalf("zero policy statements should return error")
}
// No Policy Statement Name
policyDoc = dummyBlobPolicyDocument()
policyDoc.TrustPolicies[0].Name = ""
err = policyDoc.Validate()
if err == nil || err.Error() != "blob trust policy: a trust policy statement is missing a name, every statement requires a name" {
t.Fatalf("policy statement with no name should return an error")
}
// multiple global rust policy
policyDoc = dummyBlobPolicyDocument()
policyStatement1 := policyDoc.TrustPolicies[0].clone()
policyStatement1.GlobalPolicy = true
policyStatement2 := policyDoc.TrustPolicies[0].clone()
policyStatement2.Name = "test-statement-name-2"
policyStatement2.GlobalPolicy = true
policyDoc.TrustPolicies = []BlobTrustPolicy{*policyStatement1, *policyStatement2}
err = policyDoc.Validate()
if err == nil || err.Error() != "multiple blob trust policy statements have globalPolicy set to true. Only one trust policy statement can be marked as global policy" {
t.Error(err)
t.Fatalf("multiple global blob policy should return error")
}
// Policy Document with duplicate policy statement names
policyDoc = dummyBlobPolicyDocument()
policyStatement1 = policyDoc.TrustPolicies[0].clone()
policyStatement2 = policyDoc.TrustPolicies[0].clone()
policyDoc.TrustPolicies = []BlobTrustPolicy{*policyStatement1, *policyStatement2}
err = policyDoc.Validate()
if err == nil || err.Error() != "multiple blob trust policy statements use the same name \"test-statement-name\", statement names must be unique" {
t.Fatalf("policy statements with same name should return error")
}
}
func TestGetApplicableTrustPolicy(t *testing.T) {
policyDoc := dummyBlobPolicyDocument()
policyStatement := policyDoc.TrustPolicies[0].clone()
policyStatement1 := policyStatement.clone()
policyStatement1.Name = "test-statement-name-1"
policyStatement1.GlobalPolicy = true
policyStatement2 := policyStatement.clone()
policyStatement2.Name = "test-statement-name-2"
policyDoc.TrustPolicies = []BlobTrustPolicy{*policyStatement, *policyStatement1, *policyStatement2}
validateGetApplicableTrustPolicy(t, policyDoc, "test-statement-name-2", policyStatement2)
validateGetApplicableTrustPolicy(t, policyDoc, "test-statement-name", policyStatement)
}
func TestGetApplicableTrustPolicy_Error(t *testing.T) {
policyDoc := dummyBlobPolicyDocument()
t.Run("empty policy name", func(t *testing.T) {
_, err := policyDoc.GetApplicableTrustPolicy("")
if err == nil || err.Error() != "policy name cannot be empty" {
t.Fatalf("GetApplicableTrustPolicy() returned error: %v", err)
}
})
t.Run("non existent policy name", func(t *testing.T) {
_, err := policyDoc.GetApplicableTrustPolicy("blaah")
if err == nil || err.Error() != "no applicable blob trust policy with name \"blaah\"" {
t.Fatalf("GetApplicableTrustPolicy() returned error: %v", err)
}
})
}
func TestGetGlobalTrustPolicy(t *testing.T) {
policyDoc := dummyBlobPolicyDocument()
policyDoc.TrustPolicies[0].GlobalPolicy = true
policy, err := policyDoc.GetGlobalTrustPolicy()
if err != nil {
t.Fatalf("GetGlobalTrustPolicy() returned error: %v", err)
}
if !reflect.DeepEqual(*policy, policyDoc.TrustPolicies[0]) {
t.Fatalf("GetGlobalTrustPolicy() returned unexpected policy")
}
}
func validateGetApplicableTrustPolicy(t *testing.T, policyDoc BlobDocument, policyName string, expectedPolicy *BlobTrustPolicy) {
policy, err := policyDoc.GetApplicableTrustPolicy(policyName)
if err != nil {
t.Fatalf("GetApplicableTrustPolicy() returned error: %v", err)
}
if reflect.DeepEqual(policy, *expectedPolicy) {
t.Fatalf("GetApplicableTrustPolicy() returned unexpected policy for %s", policyName)
}
}

254
verifier/trustpolicy/oci.go Normal file
View File

@ -0,0 +1,254 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package trustpolicy
import (
"errors"
"fmt"
"regexp"
"strings"
"github.com/notaryproject/notation-go/dir"
set "github.com/notaryproject/notation-go/internal/container"
"github.com/notaryproject/notation-go/internal/slices"
"github.com/notaryproject/notation-go/internal/trustpolicy"
)
// OCIDocument represents a trustpolicy.oci.json document for OCI artifacts
type OCIDocument struct {
// Version of the policy document
Version string `json:"version"`
// TrustPolicies include each policy statement
TrustPolicies []OCITrustPolicy `json:"trustPolicies"`
}
// OCITrustPolicy represents a policy statement in the OCI trust policy document
type OCITrustPolicy struct {
// Name of the policy statement
Name string `json:"name"`
// SignatureVerification setting for this policy statement
SignatureVerification SignatureVerification `json:"signatureVerification"`
// TrustStores this policy statement uses
TrustStores []string `json:"trustStores"`
// TrustedIdentities this policy statement pins
TrustedIdentities []string `json:"trustedIdentities"`
// RegistryScopes that this policy statement affects
RegistryScopes []string `json:"registryScopes"`
}
// Document represents a trustPolicy.json document
//
// Deprecated: Document exists for historical compatibility and
// should not be used. To create OCI Document, use [OCIDocument].
type Document = OCIDocument
// TrustPolicy represents a policy statement in the policy document
//
// Deprecated: TrustPolicy exists for historical compatibility and
// should not be used. To create OCI TrustPolicy, use [OCITrustPolicy].
type TrustPolicy = OCITrustPolicy
// LoadDocument loads a trust policy document from a local file system
//
// Deprecated: LoadDocument function exists for historical compatibility and
// should not be used. To load OCI Document, use [LoadOCIDocument] function.
var LoadDocument = LoadOCIDocument
var supportedOCIPolicyVersions = []string{"1.0"}
// LoadOCIDocument retrieves a trust policy document from the local file system.
// It attempts to read from [dir.PathOCITrustPolicy] first; if not found,
// it tries [dir.PathTrustPolicy].
// If both dir.PathOCITrustPolicy and dir.PathTrustPolicy exist,
// dir.PathOCITrustPolicy will be read.
func LoadOCIDocument() (*OCIDocument, error) {
var doc OCIDocument
// attempt to load the document from dir.PathOCITrustPolicy
if err := getDocument(dir.PathOCITrustPolicy, &doc); err != nil {
// if the document is not found at the first path, try the second path
if errors.As(err, &errPolicyNotExist{}) {
if err := getDocument(dir.PathTrustPolicy, &doc); err != nil {
return nil, err
}
return &doc, nil
}
// if an error occurred other than the document not found, return it
return nil, err
}
return &doc, nil
}
// Validate validates a policy document according to its version's rule set.
// if any rule is violated, returns an error
func (policyDoc *OCIDocument) Validate() error {
// sanity check
if policyDoc == nil {
return errors.New("oci trust policy document cannot be nil")
}
// Validate Version
if policyDoc.Version == "" {
return errors.New("oci trust policy document has empty version, version must be specified")
}
if !slices.Contains(supportedOCIPolicyVersions, policyDoc.Version) {
return fmt.Errorf("oci trust policy document uses unsupported version %q", policyDoc.Version)
}
// Validate the policy according to 1.0 rules
if len(policyDoc.TrustPolicies) == 0 {
return errors.New("oci trust policy document can not have zero trust policy statements")
}
policyNames := set.New[string]()
for _, statement := range policyDoc.TrustPolicies {
// Verify unique policy statement names across the policy document
if policyNames.Contains(statement.Name) {
return fmt.Errorf("multiple oci trust policy statements use the same name %q, statement names must be unique", statement.Name)
}
if err := validatePolicyCore(statement.Name, statement.SignatureVerification, statement.TrustStores, statement.TrustedIdentities); err != nil {
return fmt.Errorf("oci trust policy: %w", err)
}
policyNames.Add(statement.Name)
}
// Verify registry scopes are valid
if err := validateRegistryScopes(policyDoc); err != nil {
return err
}
return nil
}
// GetApplicableTrustPolicy returns a pointer to the deep copied [OCITrustPolicy]
// statement that applies to the given registry scope. If no applicable trust
// policy is found, returns an error.
// see https://github.com/notaryproject/specifications/tree/9c81dc773508dedc5a81c02c8d805de04f65050b/specs/trust-store-trust-policy.md#selecting-a-trust-policy-based-on-artifact-uri
func (policyDoc *OCIDocument) GetApplicableTrustPolicy(artifactReference string) (*OCITrustPolicy, error) {
artifactPath, err := getArtifactPathFromReference(artifactReference)
if err != nil {
return nil, err
}
var wildcardPolicy *OCITrustPolicy
var applicablePolicy *OCITrustPolicy
for _, policyStatement := range policyDoc.TrustPolicies {
if slices.Contains(policyStatement.RegistryScopes, trustpolicy.Wildcard) {
// we need to deep copy because we can't use the loop variable
// address. see https://stackoverflow.com/a/45967429
wildcardPolicy = (&policyStatement).clone()
} else if slices.Contains(policyStatement.RegistryScopes, artifactPath) {
applicablePolicy = (&policyStatement).clone()
}
}
if applicablePolicy != nil {
// a policy with exact match for registry scope takes precedence over
// a wildcard (*) policy.
return applicablePolicy, nil
} else if wildcardPolicy != nil {
return wildcardPolicy, nil
} else {
return nil, fmt.Errorf("artifact %q has no applicable oci trust policy statement. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: %s", artifactReference, trustPolicyLink)
}
}
// clone returns a pointer to the deep copied [OCITrustPolicy]
func (t *OCITrustPolicy) clone() *OCITrustPolicy {
return &OCITrustPolicy{
Name: t.Name,
SignatureVerification: t.SignatureVerification,
TrustedIdentities: append([]string(nil), t.TrustedIdentities...),
TrustStores: append([]string(nil), t.TrustStores...),
RegistryScopes: append([]string(nil), t.RegistryScopes...),
}
}
// validateRegistryScopes validates if the policy document is following the
// Notary Project spec rules for registry scopes
func validateRegistryScopes(policyDoc *OCIDocument) error {
registryScopeCount := make(map[string]int)
for _, statement := range policyDoc.TrustPolicies {
// Verify registry scopes are valid
if len(statement.RegistryScopes) == 0 {
return fmt.Errorf("oci trust policy statement %q has zero registry scopes, it must specify registry scopes with at least one value", statement.Name)
}
if len(statement.RegistryScopes) > 1 && slices.Contains(statement.RegistryScopes, trustpolicy.Wildcard) {
return fmt.Errorf("oci trust policy statement %q uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values", statement.Name)
}
for _, scope := range statement.RegistryScopes {
if scope != trustpolicy.Wildcard {
if err := validateRegistryScopeFormat(scope); err != nil {
return err
}
}
registryScopeCount[scope]++
}
}
// Verify one policy statement per registry scope
for key := range registryScopeCount {
if registryScopeCount[key] > 1 {
return fmt.Errorf("registry scope %q is present in multiple oci trust policy statements, one registry scope value can only be associated with one statement", key)
}
}
// No error
return nil
}
func getArtifactPathFromReference(artifactReference string) (string, error) {
// TODO support more types of URI like "domain.com/repository",
// "domain.com/repository:tag"
i := strings.LastIndex(artifactReference, "@")
if i < 0 {
return "", fmt.Errorf("artifact URI %q could not be parsed, make sure it is the fully qualified oci artifact URI without the scheme/protocol. e.g domain.com:80/my/repository@sha256:digest", artifactReference)
}
artifactPath := artifactReference[:i]
if err := validateRegistryScopeFormat(artifactPath); err != nil {
return "", err
}
return artifactPath, nil
}
// validateRegistryScopeFormat validates if a scope is following the format
// defined in distribution spec
func validateRegistryScopeFormat(scope string) error {
// Domain and Repository regexes are adapted from distribution
// implementation
// https://github.com/distribution/distribution/blob/main/reference/regexp.go#L31
domainRegexp := regexp.MustCompile(`^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?$`)
repositoryRegexp := regexp.MustCompile(`^[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`)
ensureMessage := "make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout"
errorMessage := "registry scope %q is not valid, " + ensureMessage
errorWildCardMessage := "registry scope %q with wild card(s) is not valid, " + ensureMessage
// Check for presence of * in scope
if len(scope) > 1 && strings.Contains(scope, "*") {
return fmt.Errorf(errorWildCardMessage, scope)
}
domain, repository, found := strings.Cut(scope, "/")
if !found {
return fmt.Errorf(errorMessage, scope)
}
if domain == "" || repository == "" || !domainRegexp.MatchString(domain) || !repositoryRegexp.MatchString(repository) {
return fmt.Errorf(errorMessage, scope)
}
// No errors
return nil
}

View File

@ -0,0 +1,394 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package trustpolicy
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/notaryproject/notation-go/dir"
)
func TestLoadOCIDocumentFromOldFileLocation(t *testing.T) {
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
path := filepath.Join(tempRoot, "trustpolicy.json")
policyJson, _ := json.Marshal(dummyOCIPolicyDocument())
if err := os.WriteFile(path, policyJson, 0600); err != nil {
t.Fatalf("TestLoadOCIDocument write policy file failed. Error: %v", err)
}
t.Cleanup(func() { os.RemoveAll(tempRoot) })
if _, err := LoadOCIDocument(); err != nil {
t.Fatalf("LoadOCIDocument() should not throw error for an existing policy file. Error: %v", err)
}
}
func TestLoadOCIDocumentFromNewFileLocation(t *testing.T) {
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
path := filepath.Join(tempRoot, "trustpolicy.oci.json")
policyJson, _ := json.Marshal(dummyOCIPolicyDocument())
if err := os.WriteFile(path, policyJson, 0600); err != nil {
t.Fatalf("TestLoadOCIDocument write policy file failed. Error: %v", err)
}
t.Cleanup(func() { os.RemoveAll(tempRoot) })
if _, err := LoadOCIDocument(); err != nil {
t.Fatalf("LoadOCIDocument() should not throw error for an existing policy file. Error: %v", err)
}
}
func TestLoadOCIDocumentError(t *testing.T) {
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
if _, err := LoadOCIDocument(); err == nil {
t.Fatalf("LoadOCIDocument() should throw error if OCI trust policy is not found")
}
}
// TestApplicableTrustPolicy tests filtering policies against registry scopes
func TestApplicableTrustPolicy(t *testing.T) {
policyDoc := dummyOCIPolicyDocument()
policyStatement := policyDoc.TrustPolicies[0]
policyStatement.Name = "test-statement-name-1"
registryScope := "registry.wabbit-networks.io/software/unsigned/net-utils"
registryUri := fmt.Sprintf("%s@sha256:hash", registryScope)
policyStatement.RegistryScopes = []string{registryScope}
policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "strict"}
policyDoc.TrustPolicies = []OCITrustPolicy{
policyStatement,
}
// existing Registry Scope
policy, err := (&policyDoc).GetApplicableTrustPolicy(registryUri)
if policy.Name != policyStatement.Name || err != nil {
t.Fatalf("GetApplicableTrustPolicy() should return %q for registry scope %q", policyStatement.Name, registryScope)
}
// non-existing Registry Scope
policy, err = (&policyDoc).GetApplicableTrustPolicy("non.existing.scope/repo@sha256:hash")
if policy != nil || err == nil || err.Error() != "artifact \"non.existing.scope/repo@sha256:hash\" has no applicable oci trust policy statement. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: https://notaryproject.dev/docs/quickstart/#create-a-trust-policy" {
t.Fatalf("GetApplicableTrustPolicy() should return nil for non existing registry scope")
}
// wildcard registry scope
wildcardStatement := OCITrustPolicy{
Name: "test-statement-name-2",
SignatureVerification: SignatureVerification{VerificationLevel: "skip"},
TrustStores: []string{},
TrustedIdentities: []string{},
RegistryScopes: []string{"*"},
}
policyDoc.TrustPolicies = []OCITrustPolicy{
policyStatement,
wildcardStatement,
}
policy, err = (&policyDoc).GetApplicableTrustPolicy("some.registry.that/has.no.policy@sha256:hash")
if policy.Name != wildcardStatement.Name || err != nil {
t.Fatalf("GetApplicableTrustPolicy() should return wildcard policy for registry scope \"some.registry.that/has.no.policy\"")
}
}
// TestValidatePolicyDocument calls policyDoc.Validate()
// and tests various validations on policy elements
func TestValidateInvalidPolicyDocument(t *testing.T) {
// Sanity check
var nilPolicyDoc *OCIDocument
err := nilPolicyDoc.Validate()
if err == nil || err.Error() != "oci trust policy document cannot be nil" {
t.Fatalf("nil policyDoc should return error")
}
// Invalid Version
policyDoc := dummyOCIPolicyDocument()
policyDoc.Version = "invalid"
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy document uses unsupported version \"invalid\"" {
t.Fatalf("invalid version should return error")
}
// No Policy Statements
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies = nil
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy document can not have zero trust policy statements" {
t.Fatalf("zero policy statements should return error")
}
// No Policy Statement Name
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].Name = ""
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: a trust policy statement is missing a name, every statement requires a name" {
t.Fatalf("policy statement with no name should return an error")
}
// No Registry Scopes
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].RegistryScopes = nil
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy statement \"test-statement-name\" has zero registry scopes, it must specify registry scopes with at least one value" {
t.Fatalf("policy statement with registry scopes should return error")
}
// Multiple policy statements with same registry scope
policyDoc = dummyOCIPolicyDocument()
policyStatement1 := policyDoc.TrustPolicies[0].clone()
policyStatement2 := policyDoc.TrustPolicies[0].clone()
policyStatement2.Name = "test-statement-name-2"
policyDoc.TrustPolicies = []OCITrustPolicy{*policyStatement1, *policyStatement2}
err = policyDoc.Validate()
if err == nil || err.Error() != "registry scope \"registry.acme-rockets.io/software/net-monitor\" is present in multiple oci trust policy statements, one registry scope value can only be associated with one statement" {
t.Fatalf("Policy statements with same registry scope should return error %q", err)
}
// Registry scopes with a wildcard
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].RegistryScopes = []string{"*", "registry.acme-rockets.io/software/net-monitor"}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy statement \"test-statement-name\" uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values" {
t.Fatalf("policy statement with more than a wildcard registry scope should return error")
}
// Invalid SignatureVerification
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].SignatureVerification = SignatureVerification{VerificationLevel: "invalid"}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has invalid signatureVerification: invalid signature verification level \"invalid\"" {
t.Fatalf("policy statement with invalid SignatureVerification should return error")
}
// Invalid SignatureVerification VerifyTimestamp
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].SignatureVerification.VerifyTimestamp = "invalid"
expectedErrMsg := "oci trust policy: trust policy statement \"test-statement-name\" has invalid signatureVerification: verifyTimestamp must be \"always\" or \"afterCertExpiry\", but got \"invalid\""
err = policyDoc.Validate()
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
// strict SignatureVerification should have a trust store
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].TrustStores = []string{}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" {
t.Fatalf("strict SignatureVerification should have a trust store")
}
// strict SignatureVerification should have trusted identities
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].TrustedIdentities = []string{}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" {
t.Fatalf("strict SignatureVerification should have trusted identities")
}
// skip SignatureVerification should not have trust store or trusted identities
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].SignatureVerification = SignatureVerification{VerificationLevel: "skip"}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped" {
t.Fatalf("strict SignatureVerification should have trusted identities")
}
// Empty Trusted Identity should throw error
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].TrustedIdentities = []string{""}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has an empty trusted identity" {
t.Fatalf("policy statement with empty trusted identity should return error")
}
// Trusted Identity without separator should throw error
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].TrustedIdentities = []string{"x509.subject"}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has trusted identity \"x509.subject\" missing separator" {
t.Fatalf("policy statement with trusted identity missing separator should return error")
}
// Empty Trusted Identity value should throw error
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].TrustedIdentities = []string{"x509.subject:"}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:\" without an identity value" {
t.Fatalf("policy statement with trusted identity missing identity value should return error")
}
// trust store/trusted identities are optional for skip SignatureVerification
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].SignatureVerification = SignatureVerification{VerificationLevel: "skip"}
policyDoc.TrustPolicies[0].TrustStores = []string{}
policyDoc.TrustPolicies[0].TrustedIdentities = []string{}
err = policyDoc.Validate()
if err != nil {
t.Fatalf("skip SignatureVerification should not require a trust store or trusted identities")
}
// Trust Store missing separator
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].TrustStores = []string{"ca"}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" has malformed trust store value \"ca\". The required format is <TrustStoreType>:<TrustStoreName>" {
t.Fatalf("policy statement with trust store missing separator should return error")
}
// Invalid Trust Store type
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].TrustStores = []string{"invalid:test-trust-store"}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" uses an unsupported trust store type \"invalid\" in trust store value \"invalid:test-trust-store\"" {
t.Fatalf("policy statement with invalid trust store type should return error")
}
// Empty Named Store
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].TrustStores = []string{"ca:"}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" uses an unsupported trust store name \"\" in trust store value \"ca:\". Named store name needs to follow [a-zA-Z0-9_.-]+ format" {
t.Fatalf("policy statement with trust store missing named store should return error")
}
// trusted identities with a wildcard
policyDoc = dummyOCIPolicyDocument()
policyDoc.TrustPolicies[0].TrustedIdentities = []string{"*", "test-identity"}
err = policyDoc.Validate()
if err == nil || err.Error() != "oci trust policy: trust policy statement \"test-statement-name\" uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values" {
t.Fatalf("policy statement with more than a wildcard trusted identity should return error")
}
// Policy Document with duplicate policy statement names
policyDoc = dummyOCIPolicyDocument()
policyStatement1 = policyDoc.TrustPolicies[0].clone()
policyStatement2 = policyDoc.TrustPolicies[0].clone()
policyStatement2.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"}
policyDoc.TrustPolicies = []OCITrustPolicy{*policyStatement1, *policyStatement2}
err = policyDoc.Validate()
if err == nil || err.Error() != "multiple oci trust policy statements use the same name \"test-statement-name\", statement names must be unique" {
t.Fatalf("policy statements with same name should return error")
}
}
// TestValidRegistryScopes tests valid scopes are accepted
func TestValidRegistryScopes(t *testing.T) {
policyDoc := dummyOCIPolicyDocument()
validScopes := []string{
"*", "example.com/rep", "example.com:8080/rep/rep2", "example.com/rep/subrep/subsub",
"10.10.10.10:8080/rep/rep2", "domain/rep", "domain:1234/rep",
}
for _, scope := range validScopes {
policyDoc.TrustPolicies[0].RegistryScopes = []string{scope}
err := policyDoc.Validate()
if err != nil {
t.Fatalf("valid registry scope should not return error. Error : %q", err)
}
}
}
// TestInvalidRegistryScopes tests invalid scopes are rejected
func TestInvalidRegistryScopes(t *testing.T) {
policyDoc := dummyOCIPolicyDocument()
invalidScopes := []string{
"", "1:1", "a,b", "abcd", "1111", "1,2", "example.com/rep:tag",
"example.com/rep/subrep/sub:latest", "example.com", "rep/rep2:latest",
"repository", "10.10.10.10", "10.10.10.10:8080/rep/rep2:latest",
}
for _, scope := range invalidScopes {
policyDoc.TrustPolicies[0].RegistryScopes = []string{scope}
err := policyDoc.Validate()
if err == nil || err.Error() != "registry scope \""+scope+"\" is not valid, make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" {
t.Fatalf("invalid registry scope should return error. Error : %q", err)
}
}
// Test invalid scope with wild card suffix
invalidWildCardScopes := []string{"example.com/*", "*/", "example*/", "ex*test"}
for _, scope := range invalidWildCardScopes {
policyDoc.TrustPolicies[0].RegistryScopes = []string{scope}
err := policyDoc.Validate()
if err == nil || err.Error() != "registry scope \""+scope+"\" with wild card(s) is not valid, make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" {
t.Fatalf("invalid registry scope should return error. Error : %q", err)
}
}
}
// TestValidateValidPolicyDocument tests a happy policy document
func TestValidateValidPolicyDocument(t *testing.T) {
policyDoc := dummyOCIPolicyDocument()
policyStatement1 := policyDoc.TrustPolicies[0].clone()
policyStatement2 := policyStatement1.clone()
policyStatement2.Name = "test-statement-name-2"
policyStatement2.RegistryScopes = []string{"registry.wabbit-networks.io/software/unsigned/net-utils"}
policyStatement2.SignatureVerification = SignatureVerification{VerificationLevel: "permissive"}
policyStatement3 := policyStatement1.clone()
policyStatement3.Name = "test-statement-name-3"
policyStatement3.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"}
policyStatement3.TrustStores = []string{}
policyStatement3.TrustedIdentities = []string{}
policyStatement3.SignatureVerification = SignatureVerification{VerificationLevel: "skip"}
policyStatement4 := policyStatement1.clone()
policyStatement4.Name = "test-statement-name-4"
policyStatement4.RegistryScopes = []string{"*"}
policyStatement4.TrustStores = []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store-2"}
policyStatement4.SignatureVerification = SignatureVerification{VerificationLevel: "audit"}
policyStatement5 := policyStatement1.clone()
policyStatement5.Name = "test-statement-name-5"
policyStatement5.RegistryScopes = []string{"registry.acme-rockets2.io/software"}
policyStatement5.TrustedIdentities = []string{"*"}
policyStatement5.SignatureVerification = SignatureVerification{VerificationLevel: "strict"}
policyStatement6 := policyStatement1.clone()
policyStatement6.Name = "test-statement-name-6"
policyStatement6.RegistryScopes = []string{"registry.acme-rockets.io/software/net-monitor6"}
policyStatement6.SignatureVerification.VerifyTimestamp = ""
policyStatement7 := policyStatement1.clone()
policyStatement7.Name = "test-statement-name-7"
policyStatement7.RegistryScopes = []string{"registry.acme-rockets.io/software/net-monitor7"}
policyStatement7.SignatureVerification.VerifyTimestamp = OptionAlways
policyStatement8 := policyStatement1.clone()
policyStatement8.Name = "test-statement-name-8"
policyStatement8.RegistryScopes = []string{"registry.acme-rockets.io/software/net-monitor8"}
policyStatement8.SignatureVerification.VerifyTimestamp = OptionAfterCertExpiry
policyDoc.TrustPolicies = []OCITrustPolicy{
*policyStatement1,
*policyStatement2,
*policyStatement3,
*policyStatement4,
*policyStatement5,
*policyStatement6,
*policyStatement7,
*policyStatement8,
}
err := policyDoc.Validate()
if err != nil {
t.Fatalf("validation failed on a good policy document. Error : %q", err)
}
}

View File

@ -21,8 +21,6 @@ import (
"fmt"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/notaryproject/notation-go/dir"
@ -65,6 +63,19 @@ const (
ActionSkip ValidationAction = "skip"
)
// TimestampOption is an enum for timestamp verifiction options such as Always,
// AfterCertExpiry.
type TimestampOption string
const (
// OptionAlways denotes always perform timestamp verification
OptionAlways TimestampOption = "always"
// OptionAfterCertExpiry denotes perform timestamp verification only if
// the signing certificate chain has expired
OptionAfterCertExpiry TimestampOption = "afterCertExpiry"
)
var (
LevelStrict = &VerificationLevel{
Name: "strict",
@ -134,191 +145,22 @@ var (
}
)
var supportedPolicyVersions = []string{"1.0"}
// Document represents a trustPolicy.json document
type Document struct {
// Version of the policy document
Version string `json:"version"`
// TrustPolicies include each policy statement
TrustPolicies []TrustPolicy `json:"trustPolicies"`
}
// TrustPolicy represents a policy statement in the policy document
type TrustPolicy struct {
// Name of the policy statement
Name string `json:"name"`
// RegistryScopes that this policy statement affects
RegistryScopes []string `json:"registryScopes"`
// SignatureVerification setting for this policy statement
SignatureVerification SignatureVerification `json:"signatureVerification"`
// TrustStores this policy statement uses
TrustStores []string `json:"trustStores,omitempty"`
// TrustedIdentities this policy statement pins
TrustedIdentities []string `json:"trustedIdentities,omitempty"`
}
// SignatureVerification represents verification configuration in a trust policy
type SignatureVerification struct {
VerificationLevel string `json:"level"`
Override map[ValidationType]ValidationAction `json:"override,omitempty"`
VerifyTimestamp TimestampOption `json:"verifyTimestamp,omitempty"`
}
// Validate validates a policy document according to its version's rule set.
// if any rule is violated, returns an error
func (policyDoc *Document) Validate() error {
// sanity check
if policyDoc == nil {
return errors.New("trust policy document cannot be nil")
}
type errPolicyNotExist struct{}
// Validate Version
if policyDoc.Version == "" {
return errors.New("trust policy document is missing or has empty version, it must be specified")
}
if !slices.Contains(supportedPolicyVersions, policyDoc.Version) {
return fmt.Errorf("trust policy document uses unsupported version %q", policyDoc.Version)
}
// Validate the policy according to 1.0 rules
if len(policyDoc.TrustPolicies) == 0 {
return errors.New("trust policy document can not have zero trust policy statements")
}
policyStatementNameCount := make(map[string]int)
for _, statement := range policyDoc.TrustPolicies {
// Verify statement name is valid
if statement.Name == "" {
return errors.New("a trust policy statement is missing a name, every statement requires a name")
}
policyStatementNameCount[statement.Name]++
// Verify signature verification is valid
verificationLevel, err := statement.SignatureVerification.GetVerificationLevel()
if err != nil {
return fmt.Errorf("trust policy statement %q has invalid signatureVerification: %w", statement.Name, err)
}
// Any signature verification other than "skip" needs a trust store and
// trusted identities
if verificationLevel.Name == "skip" {
if len(statement.TrustStores) > 0 || len(statement.TrustedIdentities) > 0 {
return fmt.Errorf("trust policy statement %q is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped", statement.Name)
}
} else {
if len(statement.TrustStores) == 0 || len(statement.TrustedIdentities) == 0 {
return fmt.Errorf("trust policy statement %q is either missing trust stores or trusted identities, both must be specified", statement.Name)
}
// Verify Trust Store is valid
if err := validateTrustStore(statement); err != nil {
return err
}
// Verify Trusted Identities are valid
if err := validateTrustedIdentities(statement); err != nil {
return err
}
}
}
// Verify registry scopes are valid
if err := validateRegistryScopes(policyDoc); err != nil {
return err
}
// Verify unique policy statement names across the policy document
for key := range policyStatementNameCount {
if policyStatementNameCount[key] > 1 {
return fmt.Errorf("multiple trust policy statements use the same name %q, statement names must be unique", key)
}
}
// No errors
return nil
func (e errPolicyNotExist) Error() string {
return fmt.Sprintf("trust policy is not present. To create a trust policy, see: %s", trustPolicyLink)
}
// GetApplicableTrustPolicy returns a pointer to the deep copied TrustPolicy
// statement that applies to the given registry scope. If no applicable trust
// policy is found, returns an error
// see https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.2/specs/trust-store-trust-policy.md#selecting-a-trust-policy-based-on-artifact-uri
func (trustPolicyDoc *Document) GetApplicableTrustPolicy(artifactReference string) (*TrustPolicy, error) {
artifactPath, err := getArtifactPathFromReference(artifactReference)
if err != nil {
return nil, err
}
var wildcardPolicy *TrustPolicy
var applicablePolicy *TrustPolicy
for _, policyStatement := range trustPolicyDoc.TrustPolicies {
if slices.Contains(policyStatement.RegistryScopes, trustpolicy.Wildcard) {
// we need to deep copy because we can't use the loop variable
// address. see https://stackoverflow.com/a/45967429
wildcardPolicy = (&policyStatement).clone()
} else if slices.Contains(policyStatement.RegistryScopes, artifactPath) {
applicablePolicy = (&policyStatement).clone()
}
}
if applicablePolicy != nil {
// a policy with exact match for registry scope takes precedence over
// a wildcard (*) policy.
return applicablePolicy, nil
} else if wildcardPolicy != nil {
return wildcardPolicy, nil
} else {
return nil, fmt.Errorf("artifact %q has no applicable trust policy. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: %s", artifactReference, trustPolicyLink)
}
}
// LoadDocument loads a trust policy document from a local file system
func LoadDocument() (*Document, error) {
path, err := dir.ConfigFS().SysPath(dir.PathTrustPolicy)
if err != nil {
return nil, err
}
// throw error if path is a directory or a symlink or does not exist.
fileInfo, err := os.Lstat(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("trust policy is not present. To create a trust policy, see: %s", trustPolicyLink)
}
return nil, err
}
mode := fileInfo.Mode()
if mode.IsDir() || mode&fs.ModeSymlink != 0 {
return nil, fmt.Errorf("trust policy is not a regular file (symlinks are not supported). To create a trust policy, see: %s", trustPolicyLink)
}
jsonFile, err := os.Open(path)
if err != nil {
if errors.Is(err, os.ErrPermission) {
return nil, fmt.Errorf("unable to read trust policy due to file permissions, please verify the permissions of %s", filepath.Join(dir.UserConfigDir, dir.PathTrustPolicy))
}
return nil, err
}
defer jsonFile.Close()
policyDocument := &Document{}
err = json.NewDecoder(jsonFile).Decode(policyDocument)
if err != nil {
return nil, fmt.Errorf("malformed trust policy. To create a trust policy, see: %s", trustPolicyLink)
}
return policyDocument, nil
}
// GetVerificationLevel returns VerificationLevel struct for the given
// SignatureVerification struct throws error if SignatureVerification is invalid
// GetVerificationLevel returns [VerificationLevel] for the given
// [SignatureVerification] struct.
// It throws error if SignatureVerification is invalid.
func (signatureVerification *SignatureVerification) GetVerificationLevel() (*VerificationLevel, error) {
if signatureVerification.VerificationLevel == "" {
return nil, errors.New("signature verification level is empty or missing in the trust policy statement")
@ -333,16 +175,13 @@ func (signatureVerification *SignatureVerification) GetVerificationLevel() (*Ver
if baseLevel == nil {
return nil, fmt.Errorf("invalid signature verification level %q", signatureVerification.VerificationLevel)
}
if len(signatureVerification.Override) == 0 {
// nothing to override, return the base verification level
return baseLevel, nil
}
if baseLevel == LevelSkip {
return nil, fmt.Errorf("signature verification level %q can't be used to customize signature verification", baseLevel.Name)
}
customVerificationLevel := &VerificationLevel{
Name: "custom",
Enforcement: make(map[ValidationType]ValidationAction),
@ -377,79 +216,139 @@ func (signatureVerification *SignatureVerification) GetVerificationLevel() (*Ver
if validationAction == "" {
return nil, fmt.Errorf("verification action %q in custom signature verification is not supported, supported values are %q", value, ValidationActions)
}
if validationType == TypeIntegrity {
return nil, fmt.Errorf("%q verification can not be overridden in custom signature verification", key)
} else if validationType != TypeRevocation && validationAction == ActionSkip {
return nil, fmt.Errorf("%q verification can not be skipped in custom signature verification", key)
}
customVerificationLevel.Enforcement[validationType] = validationAction
}
return customVerificationLevel, nil
}
// clone returns a pointer to the deeply copied TrustPolicy
func (t *TrustPolicy) clone() *TrustPolicy {
return &TrustPolicy{
Name: t.Name,
SignatureVerification: t.SignatureVerification,
RegistryScopes: append([]string(nil), t.RegistryScopes...),
TrustedIdentities: append([]string(nil), t.TrustedIdentities...),
TrustStores: append([]string(nil), t.TrustStores...),
func getDocument(path string, v any) error {
path, err := dir.ConfigFS().SysPath(path)
if err != nil {
return err
}
// throw error if path is a directory or a symlink or does not exist.
fileInfo, err := os.Lstat(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return errPolicyNotExist{}
}
return err
}
mode := fileInfo.Mode()
if mode.IsDir() || mode&fs.ModeSymlink != 0 {
return fmt.Errorf("trust policy is not a regular file (symlinks are not supported). To create a trust policy, see: %s", trustPolicyLink)
}
jsonFile, err := os.Open(path)
if err != nil {
if errors.Is(err, os.ErrPermission) {
return fmt.Errorf("unable to read trust policy due to file permissions, please verify the permissions of %s", path)
}
return err
}
defer jsonFile.Close()
err = json.NewDecoder(jsonFile).Decode(v)
if err != nil {
return fmt.Errorf("malformed trust policy. To create a trust policy, see: %s", trustPolicyLink)
}
return nil
}
func validatePolicyCore(name string, signatureVerification SignatureVerification, trustStores, trustedIdentities []string) error {
// Verify statement name is valid
if name == "" {
return errors.New("a trust policy statement is missing a name, every statement requires a name")
}
// Verify signature verification is valid
verificationLevel, err := signatureVerification.GetVerificationLevel()
if err != nil {
return fmt.Errorf("trust policy statement %q has invalid signatureVerification: %w", name, err)
}
if signatureVerification.VerifyTimestamp != "" &&
signatureVerification.VerifyTimestamp != OptionAlways &&
signatureVerification.VerifyTimestamp != OptionAfterCertExpiry {
return fmt.Errorf("trust policy statement %q has invalid signatureVerification: verifyTimestamp must be %q or %q, but got %q", name, OptionAlways, OptionAfterCertExpiry, signatureVerification.VerifyTimestamp)
}
// Any signature verification other than "skip" needs a trust store and
// trusted identities
if verificationLevel.Name == "skip" {
if len(trustStores) > 0 || len(trustedIdentities) > 0 {
return fmt.Errorf("trust policy statement %q is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped", name)
}
} else {
if len(trustStores) == 0 || len(trustedIdentities) == 0 {
return fmt.Errorf("trust policy statement %q is either missing trust stores or trusted identities, both must be specified", name)
}
// Verify Trust Store is valid
if err := validateTrustStore(name, trustStores); err != nil {
return err
}
// Verify Trusted Identities are valid
if err := validateTrustedIdentities(name, trustedIdentities); err != nil {
return err
}
}
return nil
}
// validateTrustStore validates if the policy statement is following the
// Notary Project spec rules for truststores
func validateTrustStore(statement TrustPolicy) error {
for _, trustStore := range statement.TrustStores {
// Notary Project spec rules for truststore
func validateTrustStore(policyName string, trustStores []string) error {
for _, trustStore := range trustStores {
storeType, namedStore, found := strings.Cut(trustStore, ":")
if !found {
return fmt.Errorf("trust policy statement %q has malformed trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", statement.Name, trustStore)
return fmt.Errorf("trust policy statement %q has malformed trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", policyName, trustStore)
}
if !isValidTrustStoreType(storeType) {
return fmt.Errorf("trust policy statement %q uses an unsupported trust store type %q in trust store value %q", statement.Name, storeType, trustStore)
return fmt.Errorf("trust policy statement %q uses an unsupported trust store type %q in trust store value %q", policyName, storeType, trustStore)
}
if !file.IsValidFileName(namedStore) {
return fmt.Errorf("trust policy statement %q uses an unsupported trust store name %q in trust store value %q. Named store name needs to follow [a-zA-Z0-9_.-]+ format", statement.Name, namedStore, trustStore)
return fmt.Errorf("trust policy statement %q uses an unsupported trust store name %q in trust store value %q. Named store name needs to follow [a-zA-Z0-9_.-]+ format", policyName, namedStore, trustStore)
}
}
return nil
}
// validateTrustedIdentities validates if the policy statement is following the
// Notary Project spec rules for trusted identities
func validateTrustedIdentities(statement TrustPolicy) error {
// If there is a wildcard in trusted identies, there shouldn't be any other
func validateTrustedIdentities(policyName string, tis []string) error {
// If there is a wildcard in trusted identities, there shouldn't be any other
//identities
if len(statement.TrustedIdentities) > 1 && slices.Contains(statement.TrustedIdentities, trustpolicy.Wildcard) {
return fmt.Errorf("trust policy statement %q uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values", statement.Name)
if len(tis) > 1 && slices.Contains(tis, trustpolicy.Wildcard) {
return fmt.Errorf("trust policy statement %q uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values", policyName)
}
var parsedDNs []parsedDN
// If there are trusted identities, verify they are valid
for _, identity := range statement.TrustedIdentities {
for _, identity := range tis {
if identity == "" {
return fmt.Errorf("trust policy statement %q has an empty trusted identity", statement.Name)
return fmt.Errorf("trust policy statement %q has an empty trusted identity", policyName)
}
if identity != trustpolicy.Wildcard {
identityPrefix, identityValue, found := strings.Cut(identity, ":")
if !found {
return fmt.Errorf("trust policy statement %q has trusted identity %q missing separator", statement.Name, identity)
return fmt.Errorf("trust policy statement %q has trusted identity %q missing separator", policyName, identity)
}
// notation natively supports x509.subject identities only
if identityPrefix == trustpolicy.X509Subject {
// identityValue cannot be empty
if identityValue == "" {
return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity value", statement.Name, identity)
return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity value", policyName, identity)
}
dn, err := pkix.ParseDistinguishedName(identityValue)
if err != nil {
return fmt.Errorf("trust policy statement %q has trusted identity %q with invalid identity value: %w", statement.Name, identity, err)
return fmt.Errorf("trust policy statement %q has trusted identity %q with invalid identity value: %w", policyName, identity, err)
}
parsedDNs = append(parsedDNs, parsedDN{RawString: identity, ParsedMap: dn})
}
@ -457,7 +356,7 @@ func validateTrustedIdentities(statement TrustPolicy) error {
}
// Verify there are no overlapping DNs
if err := validateOverlappingDNs(statement.Name, parsedDNs); err != nil {
if err := validateOverlappingDNs(policyName, parsedDNs); err != nil {
return err
}
@ -465,39 +364,6 @@ func validateTrustedIdentities(statement TrustPolicy) error {
return nil
}
// validateRegistryScopes validates if the policy document is following the
// Notary Project spec rules for registry scopes
func validateRegistryScopes(policyDoc *Document) error {
registryScopeCount := make(map[string]int)
for _, statement := range policyDoc.TrustPolicies {
// Verify registry scopes are valid
if len(statement.RegistryScopes) == 0 {
return fmt.Errorf("trust policy statement %q has zero registry scopes, it must specify registry scopes with at least one value", statement.Name)
}
if len(statement.RegistryScopes) > 1 && slices.Contains(statement.RegistryScopes, trustpolicy.Wildcard) {
return fmt.Errorf("trust policy statement %q uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values", statement.Name)
}
for _, scope := range statement.RegistryScopes {
if scope != trustpolicy.Wildcard {
if err := validateRegistryScopeFormat(scope); err != nil {
return err
}
}
registryScopeCount[scope]++
}
}
// Verify one policy statement per registry scope
for key := range registryScopeCount {
if registryScopeCount[key] > 1 {
return fmt.Errorf("registry scope %q is present in multiple trust policy statements, one registry scope value can only be associated with one statement", key)
}
}
// No error
return nil
}
func validateOverlappingDNs(policyName string, parsedDNs []parsedDN) error {
for i, dn1 := range parsedDNs {
for j, dn2 := range parsedDNs {
@ -506,12 +372,11 @@ func validateOverlappingDNs(policyName string, parsedDNs []parsedDN) error {
}
}
}
return nil
}
// isValidTrustStoreType returns true if the given string is a valid
// truststore.Type, otherwise false.
// [truststore.Type], otherwise false.
func isValidTrustStoreType(s string) bool {
for _, p := range truststore.Types {
if s == string(p) {
@ -521,53 +386,8 @@ func isValidTrustStoreType(s string) bool {
return false
}
func getArtifactPathFromReference(artifactReference string) (string, error) {
// TODO support more types of URI like "domain.com/repository",
// "domain.com/repository:tag"
i := strings.LastIndex(artifactReference, "@")
if i < 0 {
return "", fmt.Errorf("artifact URI %q could not be parsed, make sure it is the fully qualified OCI artifact URI without the scheme/protocol. e.g domain.com:80/my/repository@sha256:digest", artifactReference)
}
artifactPath := artifactReference[:i]
if err := validateRegistryScopeFormat(artifactPath); err != nil {
return "", err
}
return artifactPath, nil
}
// Internal type to hold raw and parsed Distinguished Names
// parsedDN holds raw and parsed Distinguished Names
type parsedDN struct {
RawString string
ParsedMap map[string]string
}
// validateRegistryScopeFormat validates if a scope is following the format
// defined in distribution spec
func validateRegistryScopeFormat(scope string) error {
// Domain and Repository regexes are adapted from distribution
// implementation
// https://github.com/distribution/distribution/blob/main/reference/regexp.go#L31
domainRegexp := regexp.MustCompile(`^(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?$`)
repositoryRegexp := regexp.MustCompile(`^[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?$`)
ensureMessage := "make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout"
errorMessage := "registry scope %q is not valid, " + ensureMessage
errorWildCardMessage := "registry scope %q with wild card(s) is not valid, " + ensureMessage
// Check for presence of * in scope
if len(scope) > 1 && strings.Contains(scope, "*") {
return fmt.Errorf(errorWildCardMessage, scope)
}
domain, repository, found := strings.Cut(scope, "/")
if !found {
return fmt.Errorf(errorMessage, scope)
}
if domain == "" || repository == "" || !domainRegexp.MatchString(domain) || !repositoryRegexp.MatchString(repository) {
return fmt.Errorf(errorMessage, scope)
}
// No errors
return nil
}

View File

@ -26,425 +26,201 @@ import (
"github.com/notaryproject/notation-go/dir"
)
func dummyPolicyStatement() (policyStatement TrustPolicy) {
policyStatement = TrustPolicy{
Name: "test-statement-name",
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
SignatureVerification: SignatureVerification{VerificationLevel: "strict"},
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
func dummyOCIPolicyDocument() OCIDocument {
return OCIDocument{
Version: "1.0",
TrustPolicies: []OCITrustPolicy{
{
Name: "test-statement-name",
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
SignatureVerification: SignatureVerification{VerificationLevel: "strict"},
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
},
},
}
return
}
func dummyPolicyDocument() (policyDoc Document) {
policyDoc = Document{
Version: "1.0",
TrustPolicies: []TrustPolicy{dummyPolicyStatement()},
func dummyBlobPolicyDocument() BlobDocument {
return BlobDocument{
Version: "1.0",
TrustPolicies: []BlobTrustPolicy{
{
Name: "test-statement-name",
SignatureVerification: SignatureVerification{VerificationLevel: "strict"},
TrustStores: []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store"},
TrustedIdentities: []string{"x509.subject:CN=Notation Test Root,O=Notary,L=Seattle,ST=WA,C=US"},
},
},
}
return
}
// TestValidateValidPolicyDocument tests a happy policy document
func TestValidateValidPolicyDocument(t *testing.T) {
policyDoc := dummyPolicyDocument()
policyStatement1 := dummyPolicyStatement()
policyStatement2 := dummyPolicyStatement()
policyStatement2.Name = "test-statement-name-2"
policyStatement2.RegistryScopes = []string{"registry.wabbit-networks.io/software/unsigned/net-utils"}
policyStatement2.SignatureVerification = SignatureVerification{VerificationLevel: "permissive"}
policyStatement3 := dummyPolicyStatement()
policyStatement3.Name = "test-statement-name-3"
policyStatement3.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"}
policyStatement3.TrustStores = []string{}
policyStatement3.TrustedIdentities = []string{}
policyStatement3.SignatureVerification = SignatureVerification{VerificationLevel: "skip"}
policyStatement4 := dummyPolicyStatement()
policyStatement4.Name = "test-statement-name-4"
policyStatement4.TrustStores = []string{"ca:valid-trust-store", "signingAuthority:valid-trust-store-2"}
policyStatement4.RegistryScopes = []string{"*"}
policyStatement4.SignatureVerification = SignatureVerification{VerificationLevel: "audit"}
policyStatement5 := dummyPolicyStatement()
policyStatement5.Name = "test-statement-name-5"
policyStatement5.RegistryScopes = []string{"registry.acme-rockets2.io/software"}
policyStatement5.TrustedIdentities = []string{"*"}
policyStatement5.SignatureVerification = SignatureVerification{VerificationLevel: "strict"}
policyDoc.TrustPolicies = []TrustPolicy{
policyStatement1,
policyStatement2,
policyStatement3,
policyStatement4,
policyStatement5,
// create testcase for validatePolicyCore method
func TestValidatePolicyCore(t *testing.T) {
policyName := "test-statement-name"
sigVerification := SignatureVerification{VerificationLevel: "strict"}
// valid policy
if err := validatePolicyCore(policyName, sigVerification, []string{"ca:valid-ts"}, []string{"*"}); err != nil {
t.Errorf("validatePolicyCore returned error: '%v'", err)
}
err := policyDoc.Validate()
if err != nil {
t.Fatalf("validation failed on a good policy document. Error : %q", err)
// check valid skip SignatureVerification
if err := validatePolicyCore(policyName, SignatureVerification{VerificationLevel: "skip"}, []string{}, []string{}); err != nil {
t.Errorf("validatePolicyCore returned error: '%v'", err)
}
// check skip SignatureVerification doesn't has trust store and trusted identity
expectedErr := "trust policy statement \"test-statement-name\" is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped"
if err := validatePolicyCore(policyName, SignatureVerification{VerificationLevel: "skip"}, []string{"ca:valid-ts"}, []string{}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
if err := validatePolicyCore(policyName, SignatureVerification{VerificationLevel: "skip"}, []string{}, []string{"x509:zoop"}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
// empty policy name
expectedErr = "a trust policy statement is missing a name, every statement requires a name"
if err := validatePolicyCore("", sigVerification, []string{"ca:valid-ts"}, []string{"*"}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
// invalid SignatureVerification
expectedErr = "trust policy statement \"test-statement-name\" has invalid signatureVerification: signature verification level is empty or missing in the trust policy statement"
if err := validatePolicyCore(policyName, SignatureVerification{}, []string{"ca:valid-ts"}, []string{"*"}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
// invalid trust-store or trust-policy
expectedErr = "trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified"
if err := validatePolicyCore(policyName, sigVerification, []string{}, []string{}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
if err := validatePolicyCore(policyName, sigVerification, []string{"ca:valid-ts"}, []string{}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
expectedErr = "trust policy statement \"test-statement-name\" uses an unsupported trust store type \"hola\" in trust store value \"hola:valid-ts\""
if err := validatePolicyCore(policyName, sigVerification, []string{"hola:valid-ts"}, []string{"hola"}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
expectedErr = "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject\" missing separator"
if err := validatePolicyCore(policyName, sigVerification, []string{"ca:valid-ts"}, []string{"x509.subject"}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
}
// TestValidateTrustedIdentities tests only valid x509.subjects are accepted
func TestValidateTrustStore(t *testing.T) {
// valid trust-store
if err := validateTrustStore("test-statement-name", []string{"ca:my-ts"}); err != nil {
t.Errorf("validateTrustStore returned error: '%v", err)
}
// empty trust-store
expectedErr := "trust policy statement \"test-statement-name\" has malformed trust store value \"\". The required format is <TrustStoreType>:<TrustStoreName>"
if err := validateTrustStore("test-statement-name", []string{""}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
// invalid trust-store type
expectedErr = "trust policy statement \"test-statement-name\" uses an unsupported trust store type \"unknown\" in trust store value \"unknown:my-ts\""
if err := validateTrustStore("test-statement-name", []string{"unknown:my-ts"}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
// invalid trust-store directory name
expectedErr = "trust policy statement \"test-statement-name\" uses an unsupported trust store name \"#@$@$\" in trust store value \"ca:#@$@$\". Named store name needs to follow [a-zA-Z0-9_.-]+ format"
if err := validateTrustStore("test-statement-name", []string{"ca:#@$@$"}); err == nil || err.Error() != expectedErr {
t.Errorf("expected error '%s' but not found", expectedErr)
}
}
// TestValidateTrustedIdentities tests only valid x509.subjects are accepted
func TestValidateTrustedIdentities(t *testing.T) {
// wildcard present with specific trusted identity throws error.
err := validateTrustedIdentities("test-statement-name", []string{"*", "C=US, ST=WA, O=wabbit-network.io"})
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values" {
t.Fatalf("trusted identities with wildcard and specific identityshould return error")
}
// If empty trust policy throws error.
err = validateTrustedIdentities("test-statement-name", []string{""})
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has an empty trusted identity" {
t.Fatalf("empty trusted identity should return error")
}
// No trusted identity prefix throws error
policyDoc := dummyPolicyDocument()
policyStatement := dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{"C=US, ST=WA, O=wabbit-network.io, OU=org1"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err := policyDoc.Validate()
err = validateTrustedIdentities("test-statement-name", []string{"C=US, ST=WA, O=wabbit-network.io, OU=org1"})
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"C=US, ST=WA, O=wabbit-network.io, OU=org1\" missing separator" {
t.Fatalf("trusted identity without separator should return error")
}
// Accept unknown identity prefixes
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{"unknown:my-trusted-idenity"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
err = validateTrustedIdentities("test-statement-name", []string{"unknown:my-trusted-identity"})
if err != nil {
t.Fatalf("unknown identity prefix should not return an error. Error: %q", err)
}
// Validate x509.subject identities
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
invalidDN := "x509.subject:,,,"
policyStatement.TrustedIdentities = []string{invalidDN}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:,,,\" with invalid identity value: parsing distinguished name (DN) \",,,\" failed with err: incomplete type, value pair. A valid DN must contain 'C', 'ST', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard" {
err = validateTrustedIdentities("test-statement-name", []string{invalidDN})
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:,,,\" with invalid identity value: parsing distinguished name (DN) \",,,\" failed with err: incomplete type, value pair. A valid DN must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum, and follow RFC 4514 standard" {
t.Fatalf("invalid x509.subject identity should return error. Error : %q", err)
}
// Validate x509.subject with no value
err = validateTrustedIdentities("test-statement-name", []string{"x509.subject:"})
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:\" without an identity value" {
t.Fatalf("x509.subject identity without value should return error. Error : %q", err)
}
// Validate duplicate RDNs
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
invalidDN = "x509.subject:C=US,C=IN"
policyStatement.TrustedIdentities = []string{invalidDN}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
err = validateTrustedIdentities("test-statement-name", []string{invalidDN})
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:C=US,C=IN\" with invalid identity value: distinguished name (DN) \"C=US,C=IN\" has duplicate RDN attribute for \"C\", DN can only have unique RDN attributes" {
t.Fatalf("invalid x509.subject identity should return error. Error : %q", err)
}
// Validate mandatory RDNs
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
invalidDN = "x509.subject:C=US,ST=WA"
policyStatement.TrustedIdentities = []string{invalidDN}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:C=US,ST=WA\" with invalid identity value: distinguished name (DN) \"C=US,ST=WA\" has no mandatory RDN attribute for \"O\", it must contain 'C', 'ST', and 'O' RDN attributes at a minimum" {
err = validateTrustedIdentities("test-statement-name", []string{invalidDN})
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:C=US,ST=WA\" with invalid identity value: distinguished name (DN) \"C=US,ST=WA\" has no mandatory RDN attribute for \"O\", it must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum" {
t.Fatalf("invalid x509.subject identity should return error. Error : %q", err)
}
// DN may have optional RDNs
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
validDN := "x509.subject:C=US,ST=WA,O=MyOrg,CustomRDN=CustomValue"
policyStatement.TrustedIdentities = []string{validDN}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
err = validateTrustedIdentities("test-statement-name", []string{validDN})
if err != nil {
t.Fatalf("valid x509.subject identity should not return error. Error : %q", err)
}
// Validate rfc4514 DNs
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
validDN1 := "x509.subject:C=US,ST=WA,O=MyOrg"
validDN2 := "x509.subject:C=US,ST=WA,O= My. Org"
validDN3 := "x509.subject:C=US,ST=WA,O=My \"special\" Org \\, \\; \\\\ others"
policyStatement.TrustedIdentities = []string{validDN1, validDN2, validDN3}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
err = validateTrustedIdentities("test-statement-name", []string{validDN1, validDN2, validDN3})
if err != nil {
t.Fatalf("valid x509.subject identity should not return error. Error : %q", err)
}
// Validate overlapping DNs
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
validDN1 = "x509.subject:C=US,ST=WA,O=MyOrg"
validDN2 = "x509.subject:C=US,ST=WA,O=MyOrg,X=Y"
policyStatement.TrustedIdentities = []string{validDN1, validDN2}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
err = validateTrustedIdentities("test-statement-name", []string{validDN1, validDN2})
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has overlapping x509 trustedIdentities, \"x509.subject:C=US,ST=WA,O=MyOrg\" overlaps with \"x509.subject:C=US,ST=WA,O=MyOrg,X=Y\"" {
t.Fatalf("overlapping DNs should return error")
}
// Validate multi-valued RDNs
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
multiValduedRDN := "x509.subject:C=US+ST=WA,O=MyOrg"
policyStatement.TrustedIdentities = []string{multiValduedRDN}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
multiValuedRUN := "x509.subject:C=US+ST=WA,O=MyOrg"
err = validateTrustedIdentities("test-statement-name", []string{multiValuedRUN})
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:C=US+ST=WA,O=MyOrg\" with invalid identity value: distinguished name (DN) \"C=US+ST=WA,O=MyOrg\" has multi-valued RDN attributes, remove multi-valued RDN attributes as they are not supported" {
t.Fatalf("multi-valued RDN should return error. Error : %q", err)
}
}
// TestInvalidRegistryScopes tests invalid scopes are rejected
func TestInvalidRegistryScopes(t *testing.T) {
invalidScopes := []string{
"", "1:1", "a,b", "abcd", "1111", "1,2", "example.com/rep:tag",
"example.com/rep/subrep/sub:latest", "example.com", "rep/rep2:latest",
"repository", "10.10.10.10", "10.10.10.10:8080/rep/rep2:latest",
}
for _, scope := range invalidScopes {
policyDoc := dummyPolicyDocument()
policyStatement := dummyPolicyStatement()
policyStatement.RegistryScopes = []string{scope}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err := policyDoc.Validate()
if err == nil || err.Error() != "registry scope \""+scope+"\" is not valid, make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" {
t.Fatalf("invalid registry scope should return error. Error : %q", err)
}
}
// Test invalid scope with wild card suffix
invalidWildCardScopes := []string{"example.com/*", "*/", "example*/", "ex*test"}
for _, scope := range invalidWildCardScopes {
policyDoc := dummyPolicyDocument()
policyStatement := dummyPolicyStatement()
policyStatement.RegistryScopes = []string{scope}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err := policyDoc.Validate()
if err == nil || err.Error() != "registry scope \""+scope+"\" with wild card(s) is not valid, make sure it is a fully qualified repository without the scheme, protocol or tag. For example domain.com/my/repository or a local scope like local/myOCILayout" {
t.Fatalf("invalid registry scope should return error. Error : %q", err)
}
}
}
// TestValidRegistryScopes tests valid scopes are accepted
func TestValidRegistryScopes(t *testing.T) {
validScopes := []string{
"*", "example.com/rep", "example.com:8080/rep/rep2", "example.com/rep/subrep/subsub",
"10.10.10.10:8080/rep/rep2", "domain/rep", "domain:1234/rep",
}
for _, scope := range validScopes {
policyDoc := dummyPolicyDocument()
policyStatement := dummyPolicyStatement()
policyStatement.RegistryScopes = []string{scope}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err := policyDoc.Validate()
if err != nil {
t.Fatalf("valid registry scope should not return error. Error : %q", err)
}
}
}
// TestValidatePolicyDocument calls policyDoc.Validate()
// and tests various validations on policy eliments
func TestValidateInvalidPolicyDocument(t *testing.T) {
// Sanity check
var nilPolicyDoc *Document
err := nilPolicyDoc.Validate()
if err == nil || err.Error() != "trust policy document cannot be nil" {
t.Fatalf("nil policyDoc should return error")
}
// Invalid Version
policyDoc := dummyPolicyDocument()
policyDoc.Version = "invalid"
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy document uses unsupported version \"invalid\"" {
t.Fatalf("invalid version should return error")
}
// No Policy Satements
policyDoc = dummyPolicyDocument()
policyDoc.TrustPolicies = nil
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy document can not have zero trust policy statements" {
t.Fatalf("zero policy statements should return error")
}
// No Policy Satement Name
policyDoc = dummyPolicyDocument()
policyStatement := dummyPolicyStatement()
policyStatement.Name = ""
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "a trust policy statement is missing a name, every statement requires a name" {
t.Fatalf("policy statement with no name should return an error")
}
// No Registry Scopes
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.RegistryScopes = nil
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has zero registry scopes, it must specify registry scopes with at least one value" {
t.Fatalf("policy statement with registry scopes should return error")
}
// Multiple policy statements with same registry scope
policyDoc = dummyPolicyDocument()
policyStatement1 := dummyPolicyStatement()
policyStatement2 := dummyPolicyStatement()
policyStatement2.Name = "test-statement-name-2"
policyDoc.TrustPolicies = []TrustPolicy{policyStatement1, policyStatement2}
err = policyDoc.Validate()
if err == nil || err.Error() != "registry scope \"registry.acme-rockets.io/software/net-monitor\" is present in multiple trust policy statements, one registry scope value can only be associated with one statement" {
t.Fatalf("Policy statements with same registry scope should return error %q", err)
}
// Registry scopes with a wildcard
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.RegistryScopes = []string{"*", "registry.acme-rockets.io/software/net-monitor"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses wildcard registry scope '*', a wildcard scope cannot be used in conjunction with other scope values" {
t.Fatalf("policy statement with more than a wildcard registry scope should return error")
}
// Invlaid SignatureVerification
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "invalid"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has invalid signatureVerification: invalid signature verification level \"invalid\"" {
t.Fatalf("policy statement with invalid SignatureVerification should return error")
}
// strict SignatureVerification should have a trust store
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.TrustStores = []string{}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" {
t.Fatalf("strict SignatureVerification should have a trust store")
}
// strict SignatureVerification should have trusted identities
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" is either missing trust stores or trusted identities, both must be specified" {
t.Fatalf("strict SignatureVerification should have trusted identities")
}
// skip SignatureVerification should not have trust store or trusted identities
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "skip"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" is set to skip signature verification but configured with trust stores and/or trusted identities, remove them if signature verification needs to be skipped" {
t.Fatalf("strict SignatureVerification should have trusted identities")
}
// Empty Trusted Identity should throw error
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{""}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has an empty trusted identity" {
t.Fatalf("policy statement with empty trusted identity should return error")
}
// Trusted Identity without spearator should throw error
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{"x509.subject"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject\" missing separator" {
t.Fatalf("policy statement with trusted identity missing separator should return error")
}
// Empty Trusted Identity value should throw error
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{"x509.subject:"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has trusted identity \"x509.subject:\" without an identity value" {
t.Fatalf("policy statement with trusted identity missing identity value should return error")
}
// trust store/trusted identites are optional for skip SignatureVerification
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "skip"}
policyStatement.TrustStores = []string{}
policyStatement.TrustedIdentities = []string{}
err = policyDoc.Validate()
if err != nil {
t.Fatalf("skip SignatureVerification should not require a trust store or trusted identities")
}
// Trust Store missing separator
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.TrustStores = []string{"ca"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" has malformed trust store value \"ca\". The required format is <TrustStoreType>:<TrustStoreName>" {
t.Fatalf("policy statement with trust store missing separator should return error")
}
// Invalid Trust Store type
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.TrustStores = []string{"invalid:test-trust-store"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses an unsupported trust store type \"invalid\" in trust store value \"invalid:test-trust-store\"" {
t.Fatalf("policy statement with invalid trust store type should return error")
}
// Empty Named Store
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.TrustStores = []string{"ca:"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses an unsupported trust store name \"\" in trust store value \"ca:\". Named store name needs to follow [a-zA-Z0-9_.-]+ format" {
t.Fatalf("policy statement with trust store missing named store should return error")
}
// trusted identities with a wildcard
policyDoc = dummyPolicyDocument()
policyStatement = dummyPolicyStatement()
policyStatement.TrustedIdentities = []string{"*", "test-identity"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement}
err = policyDoc.Validate()
if err == nil || err.Error() != "trust policy statement \"test-statement-name\" uses a wildcard trusted identity '*', a wildcard identity cannot be used in conjunction with other values" {
t.Fatalf("policy statement with more than a wildcard trusted identity should return error")
}
// Policy Document with duplicate policy statement names
policyDoc = dummyPolicyDocument()
policyStatement1 = dummyPolicyStatement()
policyStatement2 = dummyPolicyStatement()
policyStatement2.RegistryScopes = []string{"registry.acme-rockets.io/software/legacy/metrics"}
policyDoc.TrustPolicies = []TrustPolicy{policyStatement1, policyStatement2}
err = policyDoc.Validate()
if err == nil || err.Error() != "multiple trust policy statements use the same name \"test-statement-name\", statement names must be unique" {
t.Fatalf("policy statements with same name should return error")
}
}
func TestGetVerificationLevel(t *testing.T) {
tests := []struct {
verificationLevel SignatureVerification
@ -467,7 +243,7 @@ func TestGetVerificationLevel(t *testing.T) {
} else {
for index, action := range tt.verificationActions {
if action != level.Enforcement[ValidationTypes[index]] {
t.Errorf("%q verification action should be %q for Verification Level %q", ValidationTypes[index], action, tt.verificationLevel)
t.Errorf("%q verification action should be %q for Verification Level %v", ValidationTypes[index], action, tt.verificationLevel)
}
}
}
@ -517,7 +293,7 @@ func TestCustomVerificationLevel(t *testing.T) {
}
for index, action := range tt.verificationActions {
if action != level.Enforcement[ValidationTypes[index]] {
t.Errorf("%q verification action should be %q for custom verification %q", ValidationTypes[index], action, tt.customVerification)
t.Errorf("%q verification action should be %q for custom verification %v", ValidationTypes[index], action, tt.customVerification)
}
}
}
@ -525,83 +301,70 @@ func TestCustomVerificationLevel(t *testing.T) {
}
}
// TestApplicableTrustPolicy tests filtering policies against registry scopes
func TestApplicableTrustPolicy(t *testing.T) {
policyDoc := dummyPolicyDocument()
policyStatement := dummyPolicyStatement()
policyStatement.Name = "test-statement-name-1"
registryScope := "registry.wabbit-networks.io/software/unsigned/net-utils"
registryUri := fmt.Sprintf("%s@sha256:hash", registryScope)
policyStatement.RegistryScopes = []string{registryScope}
policyStatement.SignatureVerification = SignatureVerification{VerificationLevel: "strict"}
policyDoc.TrustPolicies = []TrustPolicy{
policyStatement,
func TestGetDocument(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
// existing Registry Scope
policy, err := (&policyDoc).GetApplicableTrustPolicy(registryUri)
if policy.Name != policyStatement.Name || err != nil {
t.Fatalf("getApplicableTrustPolicy should return %q for registry scope %q", policyStatement.Name, registryScope)
dir.UserConfigDir = "/"
var ociDoc OCIDocument
var blobDoc BlobDocument
tests := []struct {
name string
expectedDocument any
actualDocument any
}{
{
name: "valid OCI policy file",
expectedDocument: dummyOCIPolicyDocument(),
actualDocument: &ociDoc,
},
{
name: "valid Blob policy file",
expectedDocument: dummyBlobPolicyDocument(),
actualDocument: &blobDoc,
},
}
// non-existing Registry Scope
policy, err = (&policyDoc).GetApplicableTrustPolicy("non.existing.scope/repo@sha256:hash")
if policy != nil || err == nil || err.Error() != "artifact \"non.existing.scope/repo@sha256:hash\" has no applicable trust policy. Trust policy applicability for a given artifact is determined by registryScopes. To create a trust policy, see: https://notaryproject.dev/docs/quickstart/#create-a-trust-policy" {
t.Fatalf("getApplicableTrustPolicy should return nil for non existing registry scope")
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tempRoot := t.TempDir()
path := filepath.Join(tempRoot, "trustpolicy.json")
policyJson, _ := json.Marshal(tt.expectedDocument)
if err := os.WriteFile(path, policyJson, 0600); err != nil {
t.Fatalf("TestGetDocument write policy file failed. Error: %v", err)
}
t.Cleanup(func() { os.RemoveAll(tempRoot) })
// wildcard registry scope
wildcardStatement := dummyPolicyStatement()
wildcardStatement.Name = "test-statement-name-2"
wildcardStatement.RegistryScopes = []string{"*"}
wildcardStatement.TrustStores = []string{}
wildcardStatement.TrustedIdentities = []string{}
wildcardStatement.SignatureVerification = SignatureVerification{VerificationLevel: "skip"}
policyDoc.TrustPolicies = []TrustPolicy{
policyStatement,
wildcardStatement,
}
policy, err = (&policyDoc).GetApplicableTrustPolicy("some.registry.that/has.no.policy@sha256:hash")
if policy.Name != wildcardStatement.Name || err != nil {
t.Fatalf("getApplicableTrustPolicy should return wildcard policy for registry scope \"some.registry.that/has.no.policy\"")
if err := getDocument(path, tt.actualDocument); err != nil {
t.Fatalf("getDocument() should not throw error for an existing policy file. Error: %v", err)
}
})
}
}
func TestLoadDocument(t *testing.T) {
func TestGetDocumentErrors(t *testing.T) {
dir.UserConfigDir = "/"
t.Run("non-existing policy file", func(t *testing.T) {
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
if _, err := LoadDocument(); err == nil || err.Error() != fmt.Sprintf("trust policy is not present. To create a trust policy, see: %s", trustPolicyLink) {
t.Fatalf("TestLoadPolicyDocument should throw error for non existent policy")
var doc OCIDocument
if err := getDocument("blaah", &doc); err == nil || err.Error() != fmt.Sprintf("trust policy is not present. To create a trust policy, see: %s", trustPolicyLink) {
t.Fatalf("getDocument() should throw error for non existent policy")
}
})
t.Run("invalid json file", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
path := filepath.Join(tempRoot, "invalid.json")
if err := os.WriteFile(path, []byte(`{"invalid`), 0600); err != nil {
t.Fatalf("TestLoadPolicyDocument create invalid policy file failed. Error: %v", err)
t.Fatalf("creation of invalid policy file failed. Error: %v", err)
}
if _, err := LoadDocument(); err == nil {
t.Fatalf("TestLoadPolicyDocument should throw error for invalid policy file. Error: %v", err)
}
})
t.Cleanup(func() { os.RemoveAll(tempRoot) })
t.Run("valid policy file", func(t *testing.T) {
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
path := filepath.Join(tempRoot, "trustpolicy.json")
policyDoc1 := dummyPolicyDocument()
policyJson, _ := json.Marshal(policyDoc1)
if err := os.WriteFile(path, policyJson, 0600); err != nil {
t.Fatalf("TestLoadPolicyDocument create valid policy file failed. Error: %v", err)
}
if _, err := LoadDocument(); err != nil {
t.Fatalf("TestLoadPolicyDocument should not throw error for an existing policy file. Error: %v", err)
var doc OCIDocument
if err := getDocument(path, &doc); err == nil || err.Error() != fmt.Sprintf("malformed trust policy. To create a trust policy, see: %s", trustPolicyLink) {
t.Fatalf("getDocument() should throw error for invalid policy file. Error: %v", err)
}
})
@ -610,15 +373,16 @@ func TestLoadDocument(t *testing.T) {
t.Skip("skipping test on Windows")
}
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
policyJson, _ := json.Marshal([]byte("Some String"))
path := filepath.Join(tempRoot, "trustpolicy.json")
if err := os.WriteFile(path, policyJson, 0000); err != nil {
t.Fatalf("TestLoadPolicyDocument write policy file failed. Error: %v", err)
t.Fatalf("creation of invalid permission policy file failed. Error: %v", err)
}
_, err := LoadDocument()
if err == nil || err.Error() != fmt.Sprintf("unable to read trust policy due to file permissions, please verify the permissions of %s/trustpolicy.json", tempRoot) {
t.Fatalf("TestLoadPolicyDocument should throw error for a policy file with bad permissions. Error: %v", err)
expectedErrMsg := fmt.Sprintf("unable to read trust policy due to file permissions, please verify the permissions of %s", path)
var doc OCIDocument
if err := getDocument(path, &doc); err == nil || err.Error() != expectedErrMsg {
t.Errorf("getDocument() should throw error for a policy file with bad permissions. "+
"Expected error: '%v'qq but found '%v'", expectedErrMsg, err.Error())
}
})
@ -627,12 +391,18 @@ func TestLoadDocument(t *testing.T) {
t.Skip("skipping test on Windows")
}
tempRoot := t.TempDir()
dir.UserConfigDir = tempRoot
path := filepath.Join(tempRoot, "trustpolicy.json")
if err := os.WriteFile(path, []byte(`{"invalid`), 0600); err != nil {
t.Fatalf("creation of policy file failed. Error: %v", err)
}
os.Symlink("some/filepath", filepath.Join(tempRoot, "trustpolicy.json"))
_, err := LoadDocument()
if err == nil || !strings.HasPrefix(err.Error(), "trust policy is not a regular file (symlinks are not supported)") {
t.Fatalf("TestLoadPolicyDocument should throw error for a symlink policy file. Error: %v", err)
symlinkPath := filepath.Join(tempRoot, "invalid.json")
if err := os.Symlink(path, symlinkPath); err != nil {
t.Fatalf("creation of symlink for policy file failed. Error: %v", err)
}
var doc OCIDocument
if err := getDocument(symlinkPath, &doc); err == nil || !strings.HasPrefix(err.Error(), "trust policy is not a regular file (symlinks are not supported)") {
t.Fatalf("getDocument() should throw error for a symlink policy file. Error: %v", err)
}
})
}

View File

@ -15,6 +15,7 @@
package truststore
import (
"bytes"
"context"
"crypto/x509"
"errors"
@ -29,34 +30,35 @@ import (
"github.com/notaryproject/notation-go/internal/slices"
)
// Type is an enum for trust store types supported such as
// "ca" and "signingAuthority"
// Type is an enum for trust store types supported
type Type string
const (
TypeCA Type = "ca"
TypeSigningAuthority Type = "signingAuthority"
TypeTSA Type = "tsa"
)
var (
Types = []Type{
TypeCA,
TypeSigningAuthority,
TypeTSA,
}
)
// X509TrustStore provide list and get behaviors for the trust store
// X509TrustStore provides list and get behaviors for the trust store
type X509TrustStore interface {
// GetCertificates returns certificates under storeType/namedStore
GetCertificates(ctx context.Context, storeType Type, namedStore string) ([]*x509.Certificate, error)
}
// NewX509TrustStore generates a new X509TrustStore
// NewX509TrustStore generates a new [X509TrustStore]
func NewX509TrustStore(trustStorefs dir.SysFS) X509TrustStore {
return &x509TrustStore{trustStorefs}
}
// x509TrustStore implements X509TrustStore
// x509TrustStore implements [X509TrustStore]
type x509TrustStore struct {
trustStorefs dir.SysFS
}
@ -104,6 +106,14 @@ func (trustStore *x509TrustStore) GetCertificates(ctx context.Context, storeType
if err := ValidateCertificates(certs); err != nil {
return nil, CertificateError{InnerError: err, Msg: fmt.Sprintf("failed to validate the trusted certificate %s in trust store %s of type %s", certFileName, namedStore, storeType)}
}
// we require TSA certificates in trust store to be root CA certificates
if storeType == TypeTSA {
for _, cert := range certs {
if err := isRootCACertificate(cert); err != nil {
return nil, CertificateError{InnerError: err, Msg: fmt.Sprintf("trusted certificate %s in trust store %s of type %s is invalid: %v", certFileName, namedStore, storeType, err.Error())}
}
}
}
certificates = append(certificates, certs...)
}
if len(certificates) < 1 {
@ -135,3 +145,14 @@ func ValidateCertificates(certs []*x509.Certificate) error {
func isValidStoreType(storeType Type) bool {
return slices.Contains(Types, storeType)
}
// isRootCACertificate returns nil if cert is a root CA certificate
func isRootCACertificate(cert *x509.Certificate) error {
if err := cert.CheckSignatureFrom(cert); err != nil {
return fmt.Errorf("certificate with subject %q is not a root CA certificate: %w", cert.Subject, err)
}
if !bytes.Equal(cert.RawSubject, cert.RawIssuer) {
return fmt.Errorf("certificate with subject %q is not a root CA certificate: issuer (%s) and subject (%s) are not the same", cert.Subject, cert.Issuer, cert.Subject)
}
return nil
}

View File

@ -28,13 +28,10 @@ var trustStore = NewX509TrustStore(dir.NewSysFS(filepath.FromSlash("../testdata/
// TestLoadTrustStore tests a valid trust store
func TestLoadValidTrustStore(t *testing.T) {
certs, err := trustStore.GetCertificates(context.Background(), "ca", "valid-trust-store")
_, err := trustStore.GetCertificates(context.Background(), "ca", "valid-trust-store")
if err != nil {
t.Fatalf("could not get certificates from trust store. %q", err)
}
if len(certs) != 4 {
t.Fatalf("unexpected number of certificates in the trust store, expected: %d, got: %d", 4, len(certs))
}
}
// TestLoadValidTrustStoreWithSelfSignedSigningCertificate tests a valid trust store with self-signed signing certificate
@ -101,3 +98,31 @@ func TestValidateCertsWithLeafCert(t *testing.T) {
t.Fatalf("leaf cert in a trust store should return error %q, got: %v", expectedErr, err)
}
}
func TestGetCertFromValidTsaTrustStore(t *testing.T) {
// testing ../testdata/truststore/x509/tsa/test-nonCA/globalsignRoot.cer
_, err := trustStore.GetCertificates(context.Background(), "tsa", "test-timestamp")
if err != nil {
t.Fatalf("expected nil error, but got %s", err)
}
}
func TestGetCertFromInvalidTsaTrustStore(t *testing.T) {
t.Run("non CA certificate", func(t *testing.T) {
// testing ../testdata/truststore/x509/tsa/test-nonCA/wabbit-networks.io
expectedErrMsg := `trusted certificate wabbit-networks.io.crt in trust store test-nonCA of type tsa is invalid: certificate with subject "CN=wabbit-networks.io,O=Notary,L=Seattle,ST=WA,C=US" is not a root CA certificate: x509: invalid signature: parent certificate cannot sign this kind of certificate`
_, err := trustStore.GetCertificates(context.Background(), "tsa", "test-nonCA")
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected error: %s, but got %s", expectedErrMsg, err)
}
})
t.Run("not self-issued", func(t *testing.T) {
//testing ../testdata/truststore/x509/tsa/test-nonSelfIssued/nonSelfIssued.crt
expectedErrMsg := `trusted certificate nonSelfIssued.crt in trust store test-nonSelfIssued of type tsa is invalid: certificate with subject "CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US" is not a root CA certificate: issuer (CN=Notation Test Revokable RSA Chain Cert Root,O=Notary,L=Seattle,ST=WA,C=US) and subject (CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US) are not the same`
_, err := trustStore.GetCertificates(context.Background(), "tsa", "test-nonSelfIssued")
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected error: %s, but got %s", expectedErrMsg, err)
}
})
}

View File

@ -11,11 +11,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package verifier provides an implementation of notation.Verifier interface
// Package verifier provides implementations of [notation.Verifier] and
// [notation.BlobVerifier] interfaces.
package verifier
import (
"context"
"crypto"
"crypto/x509"
"encoding/json"
"errors"
@ -29,8 +31,10 @@ import (
"oras.land/oras-go/v2/content"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/notation-core-go/revocation/purpose"
revocationresult "github.com/notaryproject/notation-core-go/revocation/result"
"github.com/notaryproject/notation-core-go/signature"
nx509 "github.com/notaryproject/notation-core-go/x509"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/internal/envelope"
@ -43,66 +47,193 @@ import (
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation-go/verifier/truststore"
pluginframework "github.com/notaryproject/notation-plugin-framework-go/plugin"
"github.com/notaryproject/tspclient-go"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
// verifier implements notation.Verifier and notation.verifySkipper
var algorithms = map[crypto.Hash]digest.Algorithm{
crypto.SHA256: digest.SHA256,
crypto.SHA384: digest.SHA384,
crypto.SHA512: digest.SHA512,
}
// verifier implements [notation.Verifier], [notation.BlobVerifier] and
// notation.verifySkipper interfaces.
type verifier struct {
trustPolicyDoc *trustpolicy.Document
trustStore truststore.X509TrustStore
pluginManager plugin.Manager
revocationClient revocation.Revocation
ociTrustPolicyDoc *trustpolicy.OCIDocument
blobTrustPolicyDoc *trustpolicy.BlobDocument
trustStore truststore.X509TrustStore
pluginManager plugin.Manager
revocationClient revocation.Revocation
revocationCodeSigningValidator revocation.Validator
revocationTimestampingValidator revocation.Validator
}
// VerifierOptions specifies additional parameters that can be set when using
// the NewWithOptions constructor
// the [NewVerifierWithOptions] constructor
type VerifierOptions struct {
// RevocationClient is an implementation of revocation.Revocation to use for
// verifying revocation
// verifying revocation of code signing certificate chain
//
// Deprecated: RevocationClient exists for backwards compatibility and
// should not be used. To perform code signing certificate chain revocation
// check, use [RevocationCodeSigningValidator].
RevocationClient revocation.Revocation
// RevocationCodeSigningValidator is used for verifying revocation of
// code signing certificate chain with context.
RevocationCodeSigningValidator revocation.Validator
// RevocationTimestampingValidator is used for verifying revocation of
// timestamping certificate chain with context.
RevocationTimestampingValidator revocation.Validator
// OCITrustpolicy is the trust policy document for OCI artifacts.
OCITrustPolicy *trustpolicy.OCIDocument
// BlobTrustPolicy is the trust policy document for Blob artifacts.
BlobTrustPolicy *trustpolicy.BlobDocument
// PluginManager manages plugins installed on the system.
PluginManager plugin.Manager
}
// NewFromConfig returns a verifier based on local file system
func NewFromConfig() (notation.Verifier, error) {
// NewOCIVerifierFromConfig returns an OCI verifier based on local file system
func NewOCIVerifierFromConfig() (*verifier, error) {
// load trust policy
policyDocument, err := trustpolicy.LoadDocument()
policyDocument, err := trustpolicy.LoadOCIDocument()
if err != nil {
return nil, err
}
// load trust store
x509TrustStore := truststore.NewX509TrustStore(dir.ConfigFS())
return New(policyDocument, x509TrustStore, plugin.NewCLIManager(dir.PluginFS()))
return NewVerifierWithOptions(x509TrustStore, VerifierOptions{
OCITrustPolicy: policyDocument,
PluginManager: plugin.NewCLIManager(dir.PluginFS()),
})
}
// New creates a new verifier given trustPolicy, trustStore and pluginManager
func New(trustPolicy *trustpolicy.Document, trustStore truststore.X509TrustStore, pluginManager plugin.Manager) (notation.Verifier, error) {
return NewWithOptions(trustPolicy, trustStore, pluginManager, VerifierOptions{})
// NewBlobVerifierFromConfig returns a Blob verifier based on local file system
func NewBlobVerifierFromConfig() (*verifier, error) {
// load blob trust policy
policyDocument, err := trustpolicy.LoadBlobDocument()
if err != nil {
return nil, err
}
// load trust store
x509TrustStore := truststore.NewX509TrustStore(dir.ConfigFS())
return NewVerifierWithOptions(x509TrustStore, VerifierOptions{
BlobTrustPolicy: policyDocument,
PluginManager: plugin.NewCLIManager(dir.PluginFS()),
})
}
// NewWithOptions creates a new verifier given trustPolicy, trustStore,
// pluginManager, and VerifierOptions
func NewWithOptions(trustPolicy *trustpolicy.Document, trustStore truststore.X509TrustStore, pluginManager plugin.Manager, opts VerifierOptions) (notation.Verifier, error) {
revocationClient := opts.RevocationClient
if revocationClient == nil {
var err error
revocationClient, err = revocation.New(&http.Client{Timeout: 2 * time.Second})
if err != nil {
// NewWithOptions creates a new verifier given ociTrustPolicy, trustStore,
// pluginManager, and VerifierOptions.
//
// Deprecated: NewWithOptions function exists for historical compatibility and
// should not be used. To create verifier, use [NewVerifierWithOptions] function.
func NewWithOptions(ociTrustPolicy *trustpolicy.OCIDocument, trustStore truststore.X509TrustStore, pluginManager plugin.Manager, opts VerifierOptions) (notation.Verifier, error) {
opts.OCITrustPolicy = ociTrustPolicy
opts.PluginManager = pluginManager
return NewVerifierWithOptions(trustStore, opts)
}
// NewVerifierWithOptions creates a new verifier given trustStore and
// verifierOptions.
func NewVerifierWithOptions(trustStore truststore.X509TrustStore, verifierOptions VerifierOptions) (*verifier, error) {
ociTrustPolicy := verifierOptions.OCITrustPolicy
blobTrustPolicy := verifierOptions.BlobTrustPolicy
if trustStore == nil {
return nil, errors.New("trustStore cannot be nil")
}
if ociTrustPolicy == nil && blobTrustPolicy == nil {
return nil, errors.New("ociTrustPolicy and blobTrustPolicy both cannot be nil")
}
if ociTrustPolicy != nil {
if err := ociTrustPolicy.Validate(); err != nil {
return nil, err
}
}
if trustPolicy == nil || trustStore == nil {
return nil, errors.New("trustPolicy or trustStore cannot be nil")
if blobTrustPolicy != nil {
if err := blobTrustPolicy.Validate(); err != nil {
return nil, err
}
}
if err := trustPolicy.Validate(); err != nil {
v := &verifier{
ociTrustPolicyDoc: ociTrustPolicy,
blobTrustPolicyDoc: blobTrustPolicy,
trustStore: trustStore,
pluginManager: verifierOptions.PluginManager,
}
if err := v.setRevocation(verifierOptions); err != nil {
return nil, err
}
return &verifier{
trustPolicyDoc: trustPolicy,
trustStore: trustStore,
pluginManager: pluginManager,
revocationClient: revocationClient,
}, nil
return v, nil
}
// NewFromConfig returns an OCI verifier based on local file system.
//
// Deprecated: NewFromConfig function exists for historical compatibility and
// should not be used. To create an OCI verifier, use [NewOCIVerifierFromConfig]
// function.
func NewFromConfig() (notation.Verifier, error) {
return NewOCIVerifierFromConfig()
}
// New creates a new verifier given ociTrustPolicy, trustStore and pluginManager.
//
// Deprecated: New function exists for historical compatibility and
// should not be used. To create verifier, use [NewVerifier] function.
func New(ociTrustPolicy *trustpolicy.OCIDocument, trustStore truststore.X509TrustStore, pluginManager plugin.Manager) (notation.Verifier, error) {
return NewVerifierWithOptions(trustStore, VerifierOptions{
OCITrustPolicy: ociTrustPolicy,
PluginManager: pluginManager,
})
}
// setRevocation sets revocation validators of v
func (v *verifier) setRevocation(verifierOptions VerifierOptions) error {
// timestamping validator
revocationTimestampingValidator := verifierOptions.RevocationTimestampingValidator
var err error
if revocationTimestampingValidator == nil {
revocationTimestampingValidator, err = revocation.NewWithOptions(revocation.Options{
OCSPHTTPClient: &http.Client{Timeout: 2 * time.Second},
CertChainPurpose: purpose.Timestamping,
})
if err != nil {
return err
}
}
v.revocationTimestampingValidator = revocationTimestampingValidator
// code signing validator
revocationCodeSigningValidator := verifierOptions.RevocationCodeSigningValidator
if revocationCodeSigningValidator != nil {
v.revocationCodeSigningValidator = revocationCodeSigningValidator
return nil
}
revocationClient := verifierOptions.RevocationClient
if revocationClient != nil {
v.revocationClient = revocationClient
return nil
}
// both RevocationCodeSigningValidator and RevocationClient are nil
revocationCodeSigningValidator, err = revocation.NewWithOptions(revocation.Options{
OCSPHTTPClient: &http.Client{Timeout: 2 * time.Second},
CertChainPurpose: purpose.CodeSigning,
})
if err != nil {
return err
}
v.revocationCodeSigningValidator = revocationCodeSigningValidator
return nil
}
// SkipVerify validates whether the verification level is skip.
@ -110,23 +241,105 @@ func (v *verifier) SkipVerify(ctx context.Context, opts notation.VerifierVerifyO
logger := log.GetLogger(ctx)
logger.Debugf("Check verification level against artifact %v", opts.ArtifactReference)
trustPolicy, err := v.trustPolicyDoc.GetApplicableTrustPolicy(opts.ArtifactReference)
trustPolicy, err := v.ociTrustPolicyDoc.GetApplicableTrustPolicy(opts.ArtifactReference)
if err != nil {
return false, nil, notation.ErrorNoApplicableTrustPolicy{Msg: err.Error()}
}
logger.Infof("Trust policy configuration: %+v", trustPolicy)
// ignore the error since we already validated the policy document
verificationLevel, _ := trustPolicy.SignatureVerification.GetVerificationLevel()
// verificationLevel is skip
if reflect.DeepEqual(verificationLevel, trustpolicy.LevelSkip) {
logger.Debug("Skipping signature verification")
return true, trustpolicy.LevelSkip, nil
}
return false, verificationLevel, nil
}
// Verify verifies the signature blob `signature` against the target OCI
// VerifyBlob verifies the signature of given blob, and returns the outcome upon
// successful verification.
func (v *verifier) VerifyBlob(ctx context.Context, descGenFunc notation.BlobDescriptorGenerator, signature []byte, opts notation.BlobVerifierVerifyOptions) (*notation.VerificationOutcome, error) {
logger := log.GetLogger(ctx)
logger.Debugf("Verify signature of media type %v", opts.SignatureMediaType)
if v.blobTrustPolicyDoc == nil {
return nil, errors.New("blobTrustPolicyDoc is nil")
}
var trustPolicy *trustpolicy.BlobTrustPolicy
var err error
if opts.TrustPolicyName == "" {
trustPolicy, err = v.blobTrustPolicyDoc.GetGlobalTrustPolicy()
} else {
trustPolicy, err = v.blobTrustPolicyDoc.GetApplicableTrustPolicy(opts.TrustPolicyName)
}
if err != nil {
return nil, notation.ErrorNoApplicableTrustPolicy{Msg: err.Error()}
}
logger.Infof("Trust policy configuration: %+v", trustPolicy)
// ignore the error since we already validated the policy document
verificationLevel, _ := trustPolicy.SignatureVerification.GetVerificationLevel()
outcome := &notation.VerificationOutcome{
RawSignature: signature,
VerificationLevel: verificationLevel,
}
// verificationLevel is skip
if reflect.DeepEqual(verificationLevel, trustpolicy.LevelSkip) {
logger.Debug("Skipping signature verification")
return outcome, nil
}
err = v.processSignature(ctx, signature, opts.SignatureMediaType, trustPolicy.Name, trustPolicy.TrustedIdentities, trustPolicy.TrustStores, trustPolicy.SignatureVerification, opts.PluginConfig, outcome)
if err != nil {
outcome.Error = err
return outcome, err
}
payload := &envelope.Payload{}
err = json.Unmarshal(outcome.EnvelopeContent.Payload.Content, payload)
if err != nil {
logger.Error("Failed to unmarshal the payload content in the signature blob to envelope.Payload")
outcome.Error = err
return outcome, err
}
cryptoHash := outcome.EnvelopeContent.SignerInfo.SignatureAlgorithm.Hash()
digestAlgo, ok := algorithms[cryptoHash]
if !ok {
logger.Error("Unsupported hashing algorithm: %v", cryptoHash)
err := fmt.Errorf("unsupported hashing algorithm: %v", cryptoHash)
outcome.Error = err
return outcome, err
}
desc, err := descGenFunc(digestAlgo)
if err != nil {
errMsg := fmt.Sprintf("failed to generate descriptor for given artifact. Error: %s", err)
logger.Error(errMsg)
descErr := errors.New(errMsg)
outcome.Error = descErr
return outcome, descErr
}
if desc.Digest != payload.TargetArtifact.Digest || desc.Size != payload.TargetArtifact.Size ||
(desc.MediaType != "" && desc.MediaType != payload.TargetArtifact.MediaType) {
logger.Infof("payload present in the signature: %+v", payload.TargetArtifact)
logger.Infof("payload derived from the blob: %+v", desc)
outcome.Error = errors.New("integrity check failed. signature does not match the given blob")
}
if len(opts.UserMetadata) > 0 {
err := verifyUserMetadata(logger, payload, opts.UserMetadata)
if err != nil {
outcome.Error = err
}
}
return outcome, outcome.Error
}
// Verify verifies the signature associated to the target OCI
// artifact with manifest descriptor `desc`, and returns the outcome upon
// successful verification.
// If nil signature is present and the verification level is not 'skip',
@ -138,10 +351,15 @@ func (v *verifier) Verify(ctx context.Context, desc ocispec.Descriptor, signatur
logger := log.GetLogger(ctx)
logger.Debugf("Verify signature against artifact %v referenced as %s in signature media type %v", desc.Digest, artifactRef, envelopeMediaType)
trustPolicy, err := v.trustPolicyDoc.GetApplicableTrustPolicy(artifactRef)
if v.ociTrustPolicyDoc == nil {
return nil, errors.New("ociTrustPolicyDoc is nil")
}
trustPolicy, err := v.ociTrustPolicyDoc.GetApplicableTrustPolicy(artifactRef)
if err != nil {
return nil, notation.ErrorNoApplicableTrustPolicy{Msg: err.Error()}
}
logger.Infof("Trust policy configuration: %+v", trustPolicy)
// ignore the error since we already validated the policy document
verificationLevel, _ := trustPolicy.SignatureVerification.GetVerificationLevel()
@ -155,7 +373,7 @@ func (v *verifier) Verify(ctx context.Context, desc ocispec.Descriptor, signatur
logger.Debug("Skipping signature verification")
return outcome, nil
}
err = v.processSignature(ctx, signature, envelopeMediaType, trustPolicy, pluginConfig, outcome)
err = v.processSignature(ctx, signature, envelopeMediaType, trustPolicy.Name, trustPolicy.TrustedIdentities, trustPolicy.TrustStores, trustPolicy.SignatureVerification, pluginConfig, outcome)
if err != nil {
outcome.Error = err
@ -171,7 +389,7 @@ func (v *verifier) Verify(ctx context.Context, desc ocispec.Descriptor, signatur
}
if !content.Equal(payload.TargetArtifact, desc) {
logger.Infof("payload.TargetArtifact in signature: %+v", payload.TargetArtifact)
logger.Infof("Target artifact in signature payload: %+v", payload.TargetArtifact)
logger.Infof("Target artifact that want to be verified: %+v", desc)
outcome.Error = errors.New("content descriptor mismatch")
}
@ -186,7 +404,7 @@ func (v *verifier) Verify(ctx context.Context, desc ocispec.Descriptor, signatur
return outcome, outcome.Error
}
func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelopeMediaType string, trustPolicy *trustpolicy.TrustPolicy, pluginConfig map[string]string, outcome *notation.VerificationOutcome) error {
func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelopeMediaType, policyName string, trustedIdentities, trustStores []string, signatureVerification trustpolicy.SignatureVerification, pluginConfig map[string]string, outcome *notation.VerificationOutcome) error {
logger := log.GetLogger(ctx)
// verify integrity first. notation will always verify integrity no matter
@ -209,7 +427,7 @@ func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelop
var installedPlugin pluginframework.VerifyPlugin
if verificationPluginName != "" {
logger.Debugf("Finding verification plugin %s", verificationPluginName)
logger.Debugf("Finding verification plugin %q", verificationPluginName)
verificationPluginMinVersion, err := getVerificationPluginMinVersion(&outcome.EnvelopeContent.SignerInfo)
if err != nil && err != errExtendedAttributeNotExist {
return notation.ErrorVerificationInconclusive{Msg: fmt.Sprintf("error while getting plugin minimum version, error: %s", err)}
@ -254,7 +472,18 @@ func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelop
// verify x509 trust store based authenticity
logger.Debug("Validating cert chain")
authenticityResult := verifyAuthenticity(ctx, trustPolicy, v.trustStore, outcome)
trustCerts, err := loadX509TrustStores(ctx, outcome.EnvelopeContent.SignerInfo.SignedAttributes.SigningScheme, policyName, trustStores, v.trustStore)
var authenticityResult *notation.ValidationResult
if err != nil {
authenticityResult = &notation.ValidationResult{
Error: err,
Type: trustpolicy.TypeAuthenticity,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeAuthenticity],
}
} else {
// verify authenticity
authenticityResult = verifyAuthenticity(trustCerts, outcome)
}
outcome.VerificationResults = append(outcome.VerificationResults, authenticityResult)
logVerificationResult(logger, authenticityResult)
if isCriticalFailure(authenticityResult) {
@ -265,7 +494,7 @@ func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelop
// to perform this verification rather than a plugin)
if !slices.Contains(pluginCapabilities, pluginframework.CapabilityTrustedIdentityVerifier) {
logger.Debug("Validating trust identity")
err = verifyX509TrustedIdentities(outcome.EnvelopeContent.SignerInfo.CertificateChain, trustPolicy)
err = verifyX509TrustedIdentities(policyName, trustedIdentities, outcome.EnvelopeContent.SignerInfo.CertificateChain)
if err != nil {
authenticityResult.Error = err
logVerificationResult(logger, authenticityResult)
@ -286,7 +515,7 @@ func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelop
// verify authentic timestamp
logger.Debug("Validating authentic timestamp")
authenticTimestampResult := verifyAuthenticTimestamp(outcome)
authenticTimestampResult := verifyAuthenticTimestamp(ctx, policyName, trustStores, signatureVerification, v.trustStore, v.revocationTimestampingValidator, outcome)
outcome.VerificationResults = append(outcome.VerificationResults, authenticTimestampResult)
logVerificationResult(logger, authenticTimestampResult)
if isCriticalFailure(authenticTimestampResult) {
@ -300,7 +529,7 @@ func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelop
!slices.Contains(pluginCapabilities, pluginframework.CapabilityRevocationCheckVerifier) {
logger.Debug("Validating revocation")
revocationResult := verifyRevocation(outcome, v.revocationClient, logger)
revocationResult := v.verifyRevocation(ctx, outcome)
outcome.VerificationResults = append(outcome.VerificationResults, revocationResult)
logVerificationResult(logger, revocationResult)
if isCriticalFailure(revocationResult) {
@ -323,18 +552,71 @@ func (v *verifier) processSignature(ctx context.Context, sigBlob []byte, envelop
if len(capabilitiesToVerify) > 0 {
logger.Debugf("Executing verification plugin %q with capabilities %v", verificationPluginName, capabilitiesToVerify)
response, err := executePlugin(ctx, installedPlugin, trustPolicy, capabilitiesToVerify, outcome.EnvelopeContent, pluginConfig)
response, err := executePlugin(ctx, installedPlugin, capabilitiesToVerify, outcome.EnvelopeContent, trustedIdentities, pluginConfig)
if err != nil {
return err
return fmt.Errorf("failed to verify with plugin %s: %w", verificationPluginName, err)
}
return processPluginResponse(logger, capabilitiesToVerify, response, outcome)
return processPluginResponse(capabilitiesToVerify, response, outcome)
}
}
return nil
}
func processPluginResponse(logger log.Logger, capabilitiesToVerify []pluginframework.Capability, response *pluginframework.VerifySignatureResponse, outcome *notation.VerificationOutcome) error {
func (v *verifier) verifyRevocation(ctx context.Context, outcome *notation.VerificationOutcome) *notation.ValidationResult {
logger := log.GetLogger(ctx)
if v.revocationCodeSigningValidator == nil && v.revocationClient == nil {
return &notation.ValidationResult{
Type: trustpolicy.TypeRevocation,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeRevocation],
Error: fmt.Errorf("unable to check revocation status, code signing revocation validator cannot be nil"),
}
}
var authenticSigningTime time.Time
if outcome.EnvelopeContent.SignerInfo.SignedAttributes.SigningScheme == signature.SigningSchemeX509SigningAuthority {
authenticSigningTime, _ = outcome.EnvelopeContent.SignerInfo.AuthenticSigningTime()
}
var certResults []*revocationresult.CertRevocationResult
var err error
if v.revocationCodeSigningValidator != nil {
certResults, err = v.revocationCodeSigningValidator.ValidateContext(ctx, revocation.ValidateContextOptions{
CertChain: outcome.EnvelopeContent.SignerInfo.CertificateChain,
AuthenticSigningTime: authenticSigningTime,
})
} else {
certResults, err = v.revocationClient.Validate(outcome.EnvelopeContent.SignerInfo.CertificateChain, authenticSigningTime)
}
if err != nil {
logger.Debug("Error while checking revocation status, err: %s", err.Error())
return &notation.ValidationResult{
Type: trustpolicy.TypeRevocation,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeRevocation],
Error: fmt.Errorf("unable to check revocation status, err: %s", err.Error()),
}
}
result := &notation.ValidationResult{
Type: trustpolicy.TypeRevocation,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeRevocation],
}
finalResult, problematicCertSubject := revocationFinalResult(certResults, outcome.EnvelopeContent.SignerInfo.CertificateChain, logger)
switch finalResult {
case revocationresult.ResultOK:
logger.Debug("No verification impacting errors encountered while checking revocation, status is OK")
case revocationresult.ResultRevoked:
result.Error = fmt.Errorf("signing certificate with subject %q is revoked", problematicCertSubject)
default:
// revocationresult.ResultUnknown
result.Error = fmt.Errorf("signing certificate with subject %q revocation status is unknown", problematicCertSubject)
}
return result
}
func processPluginResponse(capabilitiesToVerify []pluginframework.Capability, response *pluginframework.VerifySignatureResponse, outcome *notation.VerificationOutcome) error {
verificationPluginName, err := getVerificationPlugin(&outcome.EnvelopeContent.SignerInfo)
if err != nil {
return err
@ -442,18 +724,7 @@ func verifyIntegrity(sigBlob []byte, envelopeMediaType string, outcome *notation
}
}
func verifyAuthenticity(ctx context.Context, trustPolicy *trustpolicy.TrustPolicy, x509TrustStore truststore.X509TrustStore, outcome *notation.VerificationOutcome) *notation.ValidationResult {
// verify authenticity
trustCerts, err := loadX509TrustStores(ctx, outcome.EnvelopeContent.SignerInfo.SignedAttributes.SigningScheme, trustPolicy, x509TrustStore)
if err != nil {
return &notation.ValidationResult{
Error: err,
Type: trustpolicy.TypeAuthenticity,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeAuthenticity],
}
}
func verifyAuthenticity(trustCerts []*x509.Certificate, outcome *notation.VerificationOutcome) *notation.ValidationResult {
if len(trustCerts) < 1 {
return &notation.ValidationResult{
Error: notation.ErrorVerificationInconclusive{Msg: "no trusted certificates are found to verify authenticity"},
@ -461,7 +732,7 @@ func verifyAuthenticity(ctx context.Context, trustPolicy *trustpolicy.TrustPolic
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeAuthenticity],
}
}
_, err = signature.VerifyAuthenticity(&outcome.EnvelopeContent.SignerInfo, trustCerts)
_, err := signature.VerifyAuthenticity(&outcome.EnvelopeContent.SignerInfo, trustCerts)
if err != nil {
switch err.(type) {
case *signature.SignatureAuthenticityError:
@ -514,105 +785,83 @@ func verifyExpiry(outcome *notation.VerificationOutcome) *notation.ValidationRes
}
}
func verifyAuthenticTimestamp(outcome *notation.VerificationOutcome) *notation.ValidationResult {
invalidTimestamp := false
var err error
func verifyAuthenticTimestamp(ctx context.Context, policyName string, trustStores []string, signatureVerification trustpolicy.SignatureVerification, x509TrustStore truststore.X509TrustStore, r revocation.Validator, outcome *notation.VerificationOutcome) *notation.ValidationResult {
logger := log.GetLogger(ctx)
if signerInfo := outcome.EnvelopeContent.SignerInfo; signerInfo.SignedAttributes.SigningScheme == signature.SigningSchemeX509 {
// TODO verify RFC3161 TSA signature if present (not in RC1)
// https://github.com/notaryproject/notation-go/issues/78
if len(signerInfo.UnsignedAttributes.TimestampSignature) == 0 {
// if there is no TSA signature, then every certificate should be
// valid at the time of verification
now := time.Now()
for _, cert := range signerInfo.CertificateChain {
if now.Before(cert.NotBefore) {
invalidTimestamp = true
err = fmt.Errorf("certificate %q is not valid yet, it will be valid from %q", cert.Subject, cert.NotBefore.Format(time.RFC1123Z))
break
}
if now.After(cert.NotAfter) {
invalidTimestamp = true
err = fmt.Errorf("certificate %q is not valid anymore, it was expired at %q", cert.Subject, cert.NotAfter.Format(time.RFC1123Z))
break
}
}
}
} else if signerInfo.SignedAttributes.SigningScheme == signature.SigningSchemeX509SigningAuthority {
authenticSigningTime := signerInfo.SignedAttributes.SigningTime
// TODO use authenticSigningTime from signerInfo
// https://github.com/notaryproject/notation-core-go/issues/38
for _, cert := range signerInfo.CertificateChain {
if authenticSigningTime.Before(cert.NotBefore) || authenticSigningTime.After(cert.NotAfter) {
invalidTimestamp = true
err = fmt.Errorf("certificate %q was not valid when the digital signature was produced at %q", cert.Subject, authenticSigningTime.Format(time.RFC1123Z))
break
}
}
}
if invalidTimestamp {
signerInfo := outcome.EnvelopeContent.SignerInfo
// under signing scheme notary.x509
if signerInfo.SignedAttributes.SigningScheme == signature.SigningSchemeX509 {
logger.Debug("Under signing scheme notary.x509...")
return &notation.ValidationResult{
Error: err,
Error: verifyTimestamp(ctx, policyName, trustStores, signatureVerification, x509TrustStore, r, outcome),
Type: trustpolicy.TypeAuthenticTimestamp,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeAuthenticTimestamp],
}
}
// under signing scheme notary.x509.signingAuthority
logger.Debug("Under signing scheme notary.x509.signingAuthority...")
authenticSigningTime := signerInfo.SignedAttributes.SigningTime
for _, cert := range signerInfo.CertificateChain {
if authenticSigningTime.Before(cert.NotBefore) || authenticSigningTime.After(cert.NotAfter) {
return &notation.ValidationResult{
Error: fmt.Errorf("certificate %q was not valid when the digital signature was produced at %q", cert.Subject, authenticSigningTime.Format(time.RFC1123Z)),
Type: trustpolicy.TypeAuthenticTimestamp,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeAuthenticTimestamp],
}
}
}
// success
return &notation.ValidationResult{
Type: trustpolicy.TypeAuthenticTimestamp,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeAuthenticTimestamp],
}
}
func verifyRevocation(outcome *notation.VerificationOutcome, r revocation.Revocation, logger log.Logger) *notation.ValidationResult {
if r == nil {
return &notation.ValidationResult{
Type: trustpolicy.TypeRevocation,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeRevocation],
Error: fmt.Errorf("unable to check revocation status, revocation client cannot be nil"),
}
}
authenticSigningTime, err := outcome.EnvelopeContent.SignerInfo.AuthenticSigningTime()
if err != nil {
logger.Debugf("not using authentic signing time due to error retrieving AuthenticSigningTime, err: %v", err)
authenticSigningTime = time.Time{}
}
certResults, err := r.Validate(outcome.EnvelopeContent.SignerInfo.CertificateChain, authenticSigningTime)
if err != nil {
logger.Debug("error while checking revocation status, err: %s", err.Error())
return &notation.ValidationResult{
Type: trustpolicy.TypeRevocation,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeRevocation],
Error: fmt.Errorf("unable to check revocation status, err: %s", err.Error()),
}
}
result := &notation.ValidationResult{
Type: trustpolicy.TypeRevocation,
Action: outcome.VerificationLevel.Enforcement[trustpolicy.TypeRevocation],
}
// revocationFinalResult returns the final revocation result and problematic
// certificate subject if the final result is not ResultOK
func revocationFinalResult(certResults []*revocationresult.CertRevocationResult, certChain []*x509.Certificate, logger log.Logger) (revocationresult.Result, string) {
finalResult := revocationresult.ResultUnknown
numOKResults := 0
var problematicCertSubject string
revokedFound := false
var revokedCertSubject string
for i := len(certResults) - 1; i >= 0; i-- {
if len(certResults[i].ServerResults) > 0 && certResults[i].ServerResults[0].Error != nil {
logger.Debugf("error for certificate #%d in chain with subject %v for server %q: %v", (i + 1), outcome.EnvelopeContent.SignerInfo.CertificateChain[i].Subject.String(), certResults[i].ServerResults[0].Server, certResults[i].ServerResults[0].Error)
cert := certChain[i]
certResult := certResults[i]
if certResult.RevocationMethod == revocationresult.RevocationMethodOCSPFallbackCRL {
// log the fallback warning
logger.Warnf("OCSP check failed with unknown error and fallback to CRL check for certificate #%d in chain with subject %q", (i + 1), cert.Subject)
}
for _, serverResult := range certResult.ServerResults {
if serverResult.Error != nil {
// log individual server errors
if certResult.RevocationMethod == revocationresult.RevocationMethodOCSPFallbackCRL && serverResult.RevocationMethod == revocationresult.RevocationMethodOCSP {
// when the final revocation method is OCSPFallbackCRL,
// the OCSP server results should not be logged as an error
// since the CRL revocation check can succeed.
logger.Debugf("Certificate #%d in chain with subject %q encountered an error for revocation method %s at URL %q: %v", (i + 1), cert.Subject, revocationresult.RevocationMethodOCSP, serverResult.Server, serverResult.Error)
continue
}
logger.Errorf("Certificate #%d in chain with subject %q encountered an error for revocation method %s at URL %q: %v", (i + 1), cert.Subject, serverResult.RevocationMethod, serverResult.Server, serverResult.Error)
}
}
if certResults[i].Result == revocationresult.ResultOK || certResults[i].Result == revocationresult.ResultNonRevokable {
if certResult.Result == revocationresult.ResultOK || certResult.Result == revocationresult.ResultNonRevokable {
numOKResults++
} else {
finalResult = certResults[i].Result
problematicCertSubject = outcome.EnvelopeContent.SignerInfo.CertificateChain[i].Subject.String()
if certResults[i].Result == revocationresult.ResultRevoked {
finalResult = certResult.Result
problematicCertSubject = cert.Subject.String()
if certResult.Result == revocationresult.ResultRevoked {
revokedFound = true
revokedCertSubject = problematicCertSubject
}
}
if i < len(certResults)-1 && certResult.Result == revocationresult.ResultNonRevokable {
logger.Warnf("Certificate #%d in the chain with subject %q neither has an OCSP nor a CRL revocation method.", (i + 1), cert.Subject)
}
}
if revokedFound {
problematicCertSubject = revokedCertSubject
@ -621,21 +870,10 @@ func verifyRevocation(outcome *notation.VerificationOutcome, r revocation.Revoca
if numOKResults == len(certResults) {
finalResult = revocationresult.ResultOK
}
switch finalResult {
case revocationresult.ResultOK:
logger.Debug("no verification impacting errors encountered while checking revocation, status is OK")
case revocationresult.ResultRevoked:
result.Error = fmt.Errorf("signing certificate with subject %q is revoked", problematicCertSubject)
default:
// revocationresult.ResultUnknown
result.Error = fmt.Errorf("signing certificate with subject %q revocation status is unknown", problematicCertSubject)
}
return result
return finalResult, problematicCertSubject
}
func executePlugin(ctx context.Context, installedPlugin pluginframework.VerifyPlugin, trustPolicy *trustpolicy.TrustPolicy, capabilitiesToVerify []pluginframework.Capability, envelopeContent *signature.EnvelopeContent, pluginConfig map[string]string) (*pluginframework.VerifySignatureResponse, error) {
func executePlugin(ctx context.Context, installedPlugin pluginframework.VerifyPlugin, capabilitiesToVerify []pluginframework.Capability, envelopeContent *signature.EnvelopeContent, trustedIdentities []string, pluginConfig map[string]string) (*pluginframework.VerifySignatureResponse, error) {
logger := log.GetLogger(ctx)
// sanity check
if installedPlugin == nil {
@ -663,7 +901,7 @@ func executePlugin(ctx context.Context, installedPlugin pluginframework.VerifyPl
// https://github.com/notaryproject/notation-core-go/issues/38
}
signature := pluginframework.Signature{
sig := pluginframework.Signature{
CriticalAttributes: pluginframework.CriticalAttributes{
ContentType: payloadInfo.ContentType,
SigningScheme: string(signerInfo.SignedAttributes.SigningScheme),
@ -676,36 +914,36 @@ func executePlugin(ctx context.Context, installedPlugin pluginframework.VerifyPl
}
policy := pluginframework.TrustPolicy{
TrustedIdentities: trustPolicy.TrustedIdentities,
TrustedIdentities: trustedIdentities,
SignatureVerification: capabilitiesToVerify,
}
req := &pluginframework.VerifySignatureRequest{
ContractVersion: pluginframework.ContractVersion,
Signature: signature,
Signature: sig,
TrustPolicy: policy,
PluginConfig: pluginConfig,
}
return installedPlugin.VerifySignature(ctx, req)
}
func verifyX509TrustedIdentities(certs []*x509.Certificate, trustPolicy *trustpolicy.TrustPolicy) error {
if slices.Contains(trustPolicy.TrustedIdentities, trustpolicyInternal.Wildcard) {
func verifyX509TrustedIdentities(policyName string, trustedIdentities []string, certs []*x509.Certificate) error {
if slices.Contains(trustedIdentities, trustpolicyInternal.Wildcard) {
return nil
}
var trustedX509Identities []map[string]string
for _, identity := range trustPolicy.TrustedIdentities {
for _, identity := range trustedIdentities {
identityPrefix, identityValue, found := strings.Cut(identity, ":")
if !found {
return fmt.Errorf("trust policy statement %q has trusted identity %q missing separator", trustPolicy.Name, identity)
return fmt.Errorf("trust policy statement %q has trusted identity %q missing separator", policyName, identity)
}
// notation natively supports x509.subject identities only
if identityPrefix == trustpolicyInternal.X509Subject {
// identityValue cannot be empty
if identityValue == "" {
return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity value", trustPolicy.Name, identity)
return fmt.Errorf("trust policy statement %q has trusted identity %q without an identity value", policyName, identity)
}
parsedSubject, err := pkix.ParseDistinguishedName(identityValue)
if err != nil {
@ -716,7 +954,7 @@ func verifyX509TrustedIdentities(certs []*x509.Certificate, trustPolicy *trustpo
}
if len(trustedX509Identities) == 0 {
return fmt.Errorf("no x509 trusted identities are configured in the trust policy %q", trustPolicy.Name)
return fmt.Errorf("no x509 trusted identities are configured in the trust policy %q", policyName)
}
leafCert := certs[0] // trusted identities only supported on the leaf cert
@ -732,7 +970,7 @@ func verifyX509TrustedIdentities(certs []*x509.Certificate, trustPolicy *trustpo
}
}
return fmt.Errorf("signing certificate from the digital signature does not match the X.509 trusted identities %q defined in the trust policy %q", trustedX509Identities, trustPolicy.Name)
return fmt.Errorf("signing certificate from the digital signature does not match the X.509 trusted identities %q defined in the trust policy %q", trustedX509Identities, policyName)
}
func logVerificationResult(logger log.Logger, result *notation.ValidationResult) {
@ -750,3 +988,143 @@ func logVerificationResult(logger log.Logger, result *notation.ValidationResult)
func isRequiredVerificationPluginVer(pluginVer string, minPluginVer string) bool {
return semver.Compare("v"+pluginVer, "v"+minPluginVer) != -1
}
// verifyTimestamp provides core verification logic of authentic timestamp under
// signing scheme `notary.x509`.
func verifyTimestamp(ctx context.Context, policyName string, trustStores []string, signatureVerification trustpolicy.SignatureVerification, x509TrustStore truststore.X509TrustStore, r revocation.Validator, outcome *notation.VerificationOutcome) error {
logger := log.GetLogger(ctx)
signerInfo := outcome.EnvelopeContent.SignerInfo
performTimestampVerification := true
// check if tsa trust store is configured in trust policy
tsaEnabled, err := isTSATrustStoreInPolicy(policyName, trustStores)
if err != nil {
return fmt.Errorf("failed to check tsa trust store configuration in turst policy with error: %w", err)
}
if !tsaEnabled {
logger.Info("Timestamp verification disabled: no tsa trust store is configured in trust policy")
performTimestampVerification = false
}
// check based on 'verifyTimestamp' field
timeOfVerification := time.Now()
if performTimestampVerification &&
signatureVerification.VerifyTimestamp == trustpolicy.OptionAfterCertExpiry {
// check if signing cert chain has expired
var expired bool
for _, cert := range signerInfo.CertificateChain {
if timeOfVerification.After(cert.NotAfter) {
expired = true
break
}
}
if !expired {
logger.Infof("Timestamp verification disabled: verifyTimestamp is set to %q and signing cert chain unexpired", trustpolicy.OptionAfterCertExpiry)
performTimestampVerification = false
}
}
// timestamp verification disabled, signing cert chain MUST be valid
// at time of verification
if !performTimestampVerification {
for _, cert := range signerInfo.CertificateChain {
if timeOfVerification.Before(cert.NotBefore) {
return fmt.Errorf("verification time is before certificate %q validity period, it will be valid from %q", cert.Subject, cert.NotBefore.Format(time.RFC1123Z))
}
if timeOfVerification.After(cert.NotAfter) {
return fmt.Errorf("verification time is after certificate %q validity period, it was expired at %q", cert.Subject, cert.NotAfter.Format(time.RFC1123Z))
}
}
// success
return nil
}
// Performing timestamp verification
logger.Debug("Performing timestamp verification...")
// 1. Timestamp countersignature MUST be present
logger.Debug("Checking timestamp countersignature existence...")
if len(signerInfo.UnsignedAttributes.TimestampSignature) == 0 {
return errors.New("no timestamp countersignature was found in the signature envelope")
}
// 2. Verify the timestamp countersignature
logger.Debug("Verifying the timestamp countersignature...")
signedToken, err := tspclient.ParseSignedToken(signerInfo.UnsignedAttributes.TimestampSignature)
if err != nil {
return fmt.Errorf("failed to parse timestamp countersignature with error: %w", err)
}
info, err := signedToken.Info()
if err != nil {
return fmt.Errorf("failed to get the timestamp TSTInfo with error: %w", err)
}
timestamp, err := info.Validate(signerInfo.Signature)
if err != nil {
return fmt.Errorf("failed to get timestamp from timestamp countersignature with error: %w", err)
}
trustTSACerts, err := loadX509TSATrustStores(ctx, outcome.EnvelopeContent.SignerInfo.SignedAttributes.SigningScheme, policyName, trustStores, x509TrustStore)
if err != nil {
return fmt.Errorf("failed to load tsa trust store with error: %w", err)
}
if len(trustTSACerts) == 0 {
return errors.New("no trusted TSA certificate found in trust store")
}
rootCertPool := x509.NewCertPool()
for _, trustedCerts := range trustTSACerts {
rootCertPool.AddCert(trustedCerts)
}
tsaCertChain, err := signedToken.Verify(ctx, x509.VerifyOptions{
CurrentTime: timestamp.Value,
Roots: rootCertPool,
})
if err != nil {
return fmt.Errorf("failed to verify the timestamp countersignature with error: %w", err)
}
// 3. Validate timestamping certificate chain
logger.Debug("Validating timestamping certificate chain...")
if err := nx509.ValidateTimestampingCertChain(tsaCertChain); err != nil {
return fmt.Errorf("failed to validate the timestamping certificate chain with error: %w", err)
}
logger.Debug("The subject of TSA signing certificate is: ", tsaCertChain[0].Subject)
// 4. Check the timestamp against the signing certificate chain
logger.Debug("Checking the timestamp against the signing certificate chain...")
logger.Debugf("Timestamp range: %s", timestamp.Format(time.RFC3339))
for _, cert := range signerInfo.CertificateChain {
if !timestamp.BoundedAfter(cert.NotBefore) {
return fmt.Errorf("timestamp can be before certificate %q validity period, it will be valid from %q", cert.Subject, cert.NotBefore.Format(time.RFC1123Z))
}
if !timestamp.BoundedBefore(cert.NotAfter) {
return fmt.Errorf("timestamp can be after certificate %q validity period, it was expired at %q", cert.Subject, cert.NotAfter.Format(time.RFC1123Z))
}
if timeOfVerification.After(cert.NotAfter) {
logger.Debugf("Certificate %q expired at %q, but timestamp is within certificate validity period", cert.Subject, cert.NotAfter.Format(time.RFC1123Z))
}
}
// 5. Perform the timestamping certificate chain revocation check
logger.Debug("Checking timestamping certificate chain revocation...")
certResults, err := r.ValidateContext(ctx, revocation.ValidateContextOptions{
CertChain: tsaCertChain,
})
if err != nil {
return fmt.Errorf("failed to check timestamping certificate chain revocation with error: %w", err)
}
finalResult, problematicCertSubject := revocationFinalResult(certResults, tsaCertChain, logger)
switch finalResult {
case revocationresult.ResultOK:
logger.Debug("No verification impacting errors encountered while checking timestamping certificate chain revocation, status is OK")
case revocationresult.ResultRevoked:
return fmt.Errorf("timestamping certificate with subject %q is revoked", problematicCertSubject)
default:
// revocationresult.ResultUnknown
return fmt.Errorf("timestamping certificate with subject %q revocation status is unknown", problematicCertSubject)
}
// success
logger.Debug("Timestamp verification: Success")
return nil
}

File diff suppressed because it is too large Load Diff