Compare commits

..

139 Commits

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
Junjie Gao 1a5b3e354f
ci: enable ci for release branch (#409)
To enable a safe way to create a release branch, we need to enable CI for the release branch as proposed in #408

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-05-30 08:16:17 -07:00
Junjie Gao 254dfcde66
bump: bump up notation-core-go v1.0.3 (#407)
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-05-29 14:00:44 +08:00
Junjie Gao b7fde5134d
fix: error message for dangling reference index (#402) 2024-05-29 11:51:05 +08:00
Junjie Gao b8508d04e9
test: improve test coverage to 80% (#405)
Test:
- improved test coverage to over 80.16%

Fix:
- added ORAS copy library to copy the OCI Layout test data to a
temporary directory and avoid generating test time temporary files in
the repository directory.

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-05-28 10:33:03 -07:00
dependabot[bot] 5e98995bd1
build(deps): bump golang.org/x/crypto from 0.22.0 to 0.23.0 (#403) 2024-05-22 16:54:35 +00:00
dependabot[bot] 378ee8371c
build(deps): bump golang.org/x/crypto from 0.21.0 to 0.22.0 (#396) 2024-04-27 22:52:28 +00:00
dependabot[bot] a901939275
build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.7 to 3.4.8 (#399) 2024-04-27 22:50:09 +00:00
dependabot[bot] 97a5a86d56
build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.6 to 3.4.7 (#395) 2024-04-18 16:52:40 +00:00
dependabot[bot] 442ece7b0d
build(deps): bump golang.org/x/mod from 0.16.0 to 0.17.0 (#397) 2024-04-18 16:50:46 +00:00
Toddy Mladenov 2d65f6e3cb
Moved org maintainers to emeritus (#393)
Addressing https://github.com/notaryproject/.github/issues/66 and
https://github.com/notaryproject/.github/issues/68 also addressed
https://github.com/notaryproject/.github/issues/67 and
https://github.com/notaryproject/.github/issues/69

---------

Signed-off-by: Toddy Mladenov <toddysm@gmail.com>
2024-04-04 10:46:21 -07:00
dependabot[bot] fbf15e6c8c
build(deps): bump actions/stale from 8 to 9 (#391) 2024-04-02 04:23:42 +00:00
Patrick Zheng 57ff8e68a0
bump: bump golang and dependency versions (#392)
bumping up oras-go to v2.5.0 along with golang version to v1.21.

Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-03-27 09:52:54 -07:00
Pritesh Bandi b8136e2c80
fix: Add contract version to plugin sign request and plugin verify request (#390)
Add contract version to plugin sign request and plugin verify request.
As per [specification](https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md) `contractVersion` is a mandatory field.

Signed-off-by: Pritesh Bandi <priteshbandi@gmail.com>
2024-03-21 12:40:21 -07:00
dependabot[bot] e686d8b995
build(deps): bump golang.org/x/mod from 0.15.0 to 0.16.0 (#388) 2024-03-21 17:04:45 +00:00
dependabot[bot] 85df759836
build(deps): bump golang.org/x/crypto from 0.20.0 to 0.21.0 (#389) 2024-03-18 17:55:42 +00:00
Yi Zha d9a44b5901
chore: add GitHub action for stale issues and PRs (#365)
This PR enables the `notation-go` repo to run stale action at 1:30 every
day to label or close stale PRs and issues. See guideline
https://github.com/marketplace/actions/close-stale-issues

This is the definition for stale PRs or issues that we discussed during
community call and to be updated in [contributing
PR](https://github.com/notaryproject/.github/pull/25).

"A stale issue is one that remains inactive or without updates for a
period of 60 days. A stale pull request (PR) is one that remains
inactive or without updates for a period of 45 days. When an issue or PR
becomes stale, it is labelled as `stale`. Normally maintainers will
comment on stale issues or PRs to prompt participants to take action. If
there is no activity for additional 30 days, this issue or PR will be
closed. If an update/comment occur on stale issues or pull requests, the
stale label will be removed, and the timer will restart"

Signed-off-by: Yi Zha <yizha1@microsoft.com>
2024-03-18 10:52:39 -07:00
Pritesh Bandi ec42378613
feat: add support for signing blob (#379)
This PR adds support for signing blobs.

Signed-off-by: Pritesh Bandi <priteshbandi@gmail.com>
2024-03-14 11:16:03 -07:00
dependabot[bot] 7fa8404e79
build(deps): bump golang.org/x/crypto from 0.19.0 to 0.20.0 (#387) 2024-03-06 17:49:01 +00:00
Pritesh Bandi 2efb4a76bb
chore: updated/added deprecation message (#382)
* updated/added deprecation message.
* Also removed usage of deprecated types in `algorithms.go` file

Signed-off-by: Pritesh Bandi <priteshbandi@gmail.com>
2024-03-06 09:45:49 -08:00
dependabot[bot] a86ca0d20c
build(deps): bump golang.org/x/mod from 0.14.0 to 0.15.0 (#384) 2024-02-29 06:51:52 +00:00
dependabot[bot] 345951e59c
build(deps): bump github.com/opencontainers/image-spec from 1.1.0-rc6 to 1.1.0 (#385) 2024-02-29 06:45:36 +00:00
dependabot[bot] 4f3cb65cc0
build(deps): bump golang.org/x/crypto from 0.18.0 to 0.19.0 (#383) 2024-02-29 06:43:37 +00:00
Pritesh Bandi 9ff189134f
chore: start using plugin-framework package (#372)
**Note:** we introduced a small breaking change; the type for
`GenerateSignatureResponse#SigningAlgorithm` has been changed from
`string` to `plugin.SignatureAlgorithm`

---------

Signed-off-by: Pritesh Bandi <priteshbandi@gmail.com>
2024-02-01 11:30:33 -08:00
Patrick Zheng 4606472ebd
bump: bump up oras-go and image-spec (#381)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-02-01 15:39:33 +08:00
Junjie Gao 1752918878
fix: update error message (#380)
Previous:
```
Error: describe-key command failed: failed to execute the describe-key command for plugin azure-kv: CertificateNotFound: A certificate with (name/id) self-signed-pkcs13/versions/70747b2064c0488e936eba7a29acc4c6 was not found in this key vault. If you recently deleted this certificate you may be able to recover it using the correct recovery command. For help resolving this issue, please see https://go.microsoft.com/fwlink/?linkid=2125182
```

Current:
```
Error: failed to execute the describe-key command for plugin azure-kv: CertificateNotFound: A certificate with (name/id) self-signed-pkcs13/versions/70747b2064c0488e936eba7a29acc4c6 was not found in this key vault. If you recently deleted this certificate you may be able to recover it using the correct recovery command. For help resolving this issue, please see https://go.microsoft.com/fwlink/?linkid=2125182
```

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-01-29 09:01:16 -08:00
Patrick Zheng b7cd8a01fc
bump: bump up notation-core-go (#377)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-01-24 10:55:58 +08:00
Junjie Gao 690448ee67
fix: update PluginExecutableFileError type (#375)
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-01-18 21:19:49 +08:00
Junjie Gao d52ca7162a
fix: improve plugin error message (#371)
- added `PluginUnknownError`, `PluginValidityError`,
`PluginDirectoryError`. For each error types, Notation CLI should
provide a recommanded suggestion to solve them.
- improved error message

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

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

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-01-18 09:24:19 +08:00
dependabot[bot] 66da1e313c
build(deps): bump golang.org/x/crypto from 0.17.0 to 0.18.0 (#373) 2024-01-16 05:16:42 +00:00
Toddy Mladenov e56ee18161
Updated CODEOWNERS and MAINTAINERS files (#370)
Addressing https://github.com/notaryproject/.github/issues/55
https://github.com/notaryproject/.github/issues/56 and
https://github.com/notaryproject/.github/issues/57

Signed-off-by: Toddy Mladenov <toddysm@gmail.com>
2024-01-14 14:05:42 -08:00
Patrick Zheng b315de42f9
feat: plugin install iteration 2 (#369)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-01-12 10:17:49 +08:00
dependabot[bot] 706eab815e
build(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#367) 2023-12-26 05:35:10 +00:00
Patrick Zheng 85a5bb9826
feat: add install method to plugin CLIManager (#364)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2023-12-18 21:23:18 +08:00
dependabot[bot] 966c6b7e42
build(deps): bump golang.org/x/crypto from 0.15.0 to 0.16.0 (#366) 2023-12-08 13:38:38 +00:00
Feynman Zhou e8c8d224b2
docs: update README to align with the new project brand (#343)
Signed-off-by: Feynman Zhou <feynmanzhou@microsoft.com>
Signed-off-by: Feynman Zhou <feynman@kubesphere.io>
Co-authored-by: Pritesh Bandi <priteshbandi@gmail.com>
2023-11-27 15:42:13 +08:00
Patrick Zheng 5de0d58b21
feat: add uninstall to CLIManager (#363)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2023-11-23 11:15:46 +08:00
dependabot[bot] ce0c457700
build(deps): bump golang.org/x/crypto from 0.14.0 to 0.15.0 (#362) 2023-11-20 22:30:13 +00:00
dependabot[bot] 1bc5a3f8c4
build(deps): bump golang.org/x/mod from 0.13.0 to 0.14.0 (#361) 2023-11-07 04:55:49 +00:00
Patrick Zheng 765d02b5be
fix: update error message from notation-go (#345)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2023-10-28 08:57:34 +08:00
dependabot[bot] cb6f009f97
build(deps): bump github.com/notaryproject/notation-core-go from 1.0.0 to 1.0.1 (#358) 2023-10-27 21:30:35 +00:00
dependabot[bot] 36a0831e46
build(deps): bump oras.land/oras-go/v2 from 2.3.0 to 2.3.1 (#359) 2023-10-27 21:27:47 +00:00
dependabot[bot] effa7cb950
build(deps): bump golang.org/x/crypto from 0.13.0 to 0.14.0 (#355) 2023-10-27 07:24:02 +00:00
dependabot[bot] 18b3c680b4
build(deps): bump github.com/opencontainers/image-spec from 1.1.0-rc4 to 1.1.0-rc5 (#352) 2023-10-10 18:38:05 +00:00
dependabot[bot] f89ec21bc8
build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.5 to 3.4.6 (#351) 2023-10-10 18:34:58 +00:00
dependabot[bot] 0ff7d26fb9
build(deps): bump golang.org/x/mod from 0.12.0 to 0.13.0 (#356) 2023-10-10 18:23:00 +00:00
Junjie Gao 60d9cdcc59
bump: update oras-go to v2.3.0 (#347)
- Update oras-go to v2.3.0.
- Replace oras.Pack() with oras.PackManifest() as it is deprecated in
v2.3.0.
- Generate an empty config blob manually, as oras.PackManifest() does
not generate the config blob with the notation artifact type as the
media type.

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

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2023-09-15 09:20:01 +08:00
dependabot[bot] c6bc5e0d38
build(deps): bump golang.org/x/crypto from 0.12.0 to 0.13.0 (#350) 2023-09-14 22:13:59 +00:00
Patrick Zheng b684acb231
chore: update go version to 1.20 (#349)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2023-09-14 15:08:39 -07:00
dependabot[bot] f1706c2868
build(deps): bump golang.org/x/crypto from 0.11.0 to 0.12.0 (#344) 2023-08-22 06:20:43 +00:00
Shiwei Zhang 553b866ed4
bump: upgrade notation-core-go to v1.0.0 (#342)
bump up `notation-core-go` from `v1.0.0-rc.4` to `v1.0.0`.

Signed-off-by: Shiwei Zhang <shizh@microsoft.com>
2023-07-25 08:12:38 +08:00
Sajay Antony f2cdfee211
errors: add error for wild card scope validation (#340)
Signed-off-by: Sajay Antony <sajaya@microsoft.com>
2023-07-19 15:06:39 -07:00
Patrick Zheng 99bc2bc420
fix: quick fix to use correct sign/verify plugin (#338)
Changes in this PR:
1. `signer.NewFromPlugin(plugin plugin.SignPlugin, ...)`
2. `verifer.executePlugin(ctx context.Context, installedPlugin
plugin.VerifyPlugin, ...)`

---------

Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2023-07-19 15:23:36 +08:00
Patrick Zheng 3981f69fb5
chore: add license header to files and github action workflow to check license (#334) 2023-07-18 10:12:41 +08:00
dependabot[bot] 052e405b51
build(deps): bump golang.org/x/crypto from 0.10.0 to 0.11.0 (#331) 2023-07-18 01:03:57 +00:00
dependabot[bot] abaaa0bc7c
build(deps): bump golang.org/x/mod from 0.11.0 to 0.12.0 (#333) 2023-07-17 15:03:01 +00:00
dependabot[bot] 757d01c8bb
build(deps): bump oras.land/oras-go/v2 from 2.2.0 to 2.2.1 (#332)
Bumps [oras.land/oras-go/v2](https://github.com/oras-project/oras-go)
from 2.2.0 to 2.2.1.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/oras-project/oras-go/releases">oras.land/oras-go/v2's
releases</a>.</em></p>
<blockquote>
<h2>v2.2.1</h2>
<h2>Bug Fixes</h2>
<ul>
<li>Fix the bug where the &quot;blobs&quot; directory was not being
ensured during the initialization of <a
href="https://pkg.go.dev/oras.land/oras-go/v2@v2.2.0/content/oci#Store"><code>oci.Store</code></a>
(<a
href="https://redirect.github.com/oras-project/oras-go/issues/520">#520</a>)</li>
</ul>
<h2>Other Changes</h2>
<ul>
<li>Improve error messages</li>
<li>Upgrade <code>image-spec</code> to <a
href="https://github.com/opencontainers/image-spec/releases/tag/v1.1.0-rc4"><code>v1.1.0-rc4</code></a>
to mitigate the missing annotation keys issue</li>
</ul>
<blockquote>
<p><strong>Note</strong>: <code>oras-go</code> remains on
<code>distribution-spec</code> <a
href="https://github.com/opencontainers/distribution-spec/releases/tag/v1.1.0-rc1">v1.1.0-rc1</a>
and has not implemented <a
href="https://github.com/opencontainers/distribution-spec/releases/tag/v1.1.0-rc2">v1.1.0-rc2</a>
yet.</p>
</blockquote>
<h2>Detailed Commits</h2>
<ul>
<li>build(deps): bump golang.org/x/sync from 0.2.0 to 0.3.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/oras-project/oras-go/pull/524">oras-project/oras-go#524</a></li>
<li>fix(oci): create blobs dir and define blobs dir name constant by <a
href="https://github.com/sparr"><code>@​sparr</code></a> in <a
href="https://redirect.github.com/oras-project/oras-go/pull/520">oras-project/oras-go#520</a></li>
<li>fix: improve errors for <code>ReadAll</code> and
<code>loadIndexfile</code> by <a
href="https://github.com/sparr"><code>@​sparr</code></a> in <a
href="https://redirect.github.com/oras-project/oras-go/pull/526">oras-project/oras-go#526</a></li>
<li>refactor: upgrade go mod to <code>image-spec v1.1.0-rc4</code> and
fix the missing types by <a
href="https://github.com/Wwwsylvia"><code>@​Wwwsylvia</code></a> in <a
href="https://redirect.github.com/oras-project/oras-go/pull/536">oras-project/oras-go#536</a></li>
<li>chore: enable workflows for release branches by <a
href="https://github.com/Wwwsylvia"><code>@​Wwwsylvia</code></a> in <a
href="https://redirect.github.com/oras-project/oras-go/pull/537">oras-project/oras-go#537</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/sparr"><code>@​sparr</code></a> made
their first contribution in <a
href="https://redirect.github.com/oras-project/oras-go/pull/520">oras-project/oras-go#520</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/oras-project/oras-go/compare/v2.2.0...v2.2.1">https://github.com/oras-project/oras-go/compare/v2.2.0...v2.2.1</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a1527322f4"><code>a152732</code></a>
chore: enable workflows for release branches (<a
href="https://redirect.github.com/oras-project/oras-go/issues/537">#537</a>)</li>
<li><a
href="cd01930ccd"><code>cd01930</code></a>
refactor: upgrade go mod to <code>image-spec v1.1.0-rc4</code> and fix
the missing types...</li>
<li><a
href="f3d7906854"><code>f3d7906</code></a>
fix: improve errors for <code>ReadAll</code> and
<code>loadIndexfile</code> (<a
href="https://redirect.github.com/oras-project/oras-go/issues/526">#526</a>)</li>
<li><a
href="94805254ef"><code>9480525</code></a>
fix(oci): create blobs dir and define blobs dir name constant (<a
href="https://redirect.github.com/oras-project/oras-go/issues/520">#520</a>)</li>
<li><a
href="f1c44e178f"><code>f1c44e1</code></a>
build(deps): bump golang.org/x/sync from 0.2.0 to 0.3.0 (<a
href="https://redirect.github.com/oras-project/oras-go/issues/524">#524</a>)</li>
<li>See full diff in <a
href="https://github.com/oras-project/oras-go/compare/v2.2.0...v2.2.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=oras.land/oras-go/v2&package-manager=go_modules&previous-version=2.2.0&new-version=2.2.1)](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 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>
2023-07-12 11:46:48 -07:00
Patrick Zheng 2882bafab6
fix: quick fix typo in error msg (#328)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2023-06-30 22:42:36 -07:00
Pritesh Bandi 432c931622
fix: update timeout for OCSP call to 2 seconds (#327)
updated timeout for OCSP call to 2 seconds.

Updates based on updates in https://github.com/notaryproject/notaryproject/pull/249
2023-06-27 17:55:14 -07:00
Patrick Zheng 983e97dc54
fix: fixed error messages of trust policy (#326)
A quick PR to fix error messages of trust policy.

Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2023-06-26 08:57:53 -07:00
dependabot[bot] a973c8b50a
build(deps): bump github.com/go-ldap/ldap/v3 from 3.4.4 to 3.4.5 (#321) 2023-06-26 08:58:36 +00:00
dependabot[bot] d65bba32ae
build(deps): bump golang.org/x/crypto from 0.9.0 to 0.10.0 (#324) 2023-06-26 02:13:20 +00:00
dependabot[bot] 6df5e38e05
build(deps): bump golang.org/x/mod from 0.10.0 to 0.11.0 (#325) 2023-06-26 02:09:57 +00:00
Yi Zha fa4ddc8f86
chore: add issue template (#293)
Add issue template for `notation-go`, which is copied from `notation` repo.

Signed-off-by: Yi Zha <yizha1@microsoft.com>
2023-06-14 08:21:55 -07:00
124 changed files with 10215 additions and 2104 deletions

16
.github/.codecov.yml vendored
View File

@ -1,5 +1,21 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
coverage:
status:
project:
default:
target: 80%
patch:
default:
target: 80%

View File

@ -0,0 +1,60 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: 🐛 Bug or Issue
description: Something is not working as expected or not working at all! Report it here!
labels: [bug, triage]
body:
- type: markdown
attributes:
value: |
Thank you for taking the time to fill out this issue report. 🛑 Please check existing issues first before continuing: https://github.com/notaryproject/notation-go/issues
- type: textarea
id: verbatim
validations:
required: true
attributes:
label: "What is not working as expected?"
description: "In your own words, describe what the issue is."
- type: textarea
id: expect
validations:
required: true
attributes:
label: "What did you expect to happen?"
description: "A clear and concise description of what you expected to happen."
- type: textarea
id: reproduce
validations:
required: true
attributes:
label: "How can we reproduce it?"
description: "Detailed steps to reproduce the behavior, code snippets are welcome."
- type: textarea
id: environment
validations:
required: true
attributes:
label: Describe your environment
description: "OS and Golang version"
- type: textarea
id: version
validations:
required: true
attributes:
label: What is the version of your notation-go Library?
description: "Check the `go.mod` file for the library version."
- type: markdown
attributes:
value: |
If you want to contribute to this project, we will be happy to guide you through the contribution process especially when you already have a good proposal or understanding of how to fix this issue. Join us at https://slack.cncf.io/ and choose #notary-project channel.

18
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,18 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
blank_issues_enabled: false
contact_links:
- name: Ask a question
url: https://slack.cncf.io/
about: "Join #notary-project channel on CNCF Slack"

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

View File

@ -1,3 +1,16 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
version: 2
updates:
- package-ecosystem: "gomod"

44
.github/licenserc.yml vendored Normal file
View File

@ -0,0 +1,44 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
header:
license:
spdx-id: Apache-2.0
content: |
Copyright The Notary Project Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
paths-ignore:
- '**/*.md'
- 'CODEOWNERS'
- 'LICENSE'
- 'MAINTAINERS'
- 'go.mod'
- 'go.sum'
- '**/testdata/**'
comment: on-failure
dependency:
files:
- go.mod

View File

@ -1,3 +1,16 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
on:
issues:
types:

View File

@ -1,11 +1,30 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: build
on:
push:
branches: main
branches:
- main
- release-*
pull_request:
branches: main
branches:
- main
- release-*
jobs:
build:
uses: notaryproject/notation-core-go/.github/workflows/reusable-build.yml@main
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,10 +1,27 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: "CodeQL"
on:
push:
branches: main
branches:
- main
- release-*
pull_request:
branches: main
branches:
- main
- release-*
schedule:
- cron: '29 2 * * 5'

32
.github/workflows/license-checker.yml vendored Normal file
View File

@ -0,0 +1,32 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: License Checker
on:
push:
branches:
- main
- release-*
pull_request:
branches:
- main
- release-*
permissions:
contents: write
pull-requests: write
jobs:
check-license:
uses: notaryproject/notation-core-go/.github/workflows/reusable-license-checker.yml@main

33
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,33 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: "Close stale issues and PRs"
on:
schedule:
- cron: "30 1 * * *"
jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v9
with:
stale-issue-message: "This issue is stale because it has been opened for 60 days with no activity. Remove stale label or comment. Otherwise, it will be closed in 30 days."
stale-pr-message: "This PR is stale because it has been opened for 45 days with no activity. Remove stale label or comment. Otherwise, it will be closed in 30 days."
close-issue-message: "Issue closed due to no activity in the past 30 days."
close-pr-message: "PR closed due to no activity in the past 30 days."
days-before-issue-stale: 60
days-before-pr-stale: 45
days-before-issue-close: 30
days-before-pr-close: 30
exempt-all-milestones: true

18
.gitignore vendored
View File

@ -1,3 +1,16 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Code Editors
.vscode
.idea
@ -5,4 +18,7 @@
*.sublime-workspace
# Custom
coverage.txt
coverage.txt
# tmp directory was generated by example_remoteVerify_test.go
tmp/

View File

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

View File

@ -1,14 +1,21 @@
# Org-Level Maintainers (in alphabetical order)
# Pattern: [First Name] [Last Name] <[Email Address]> ([GitHub Handle])
Justin Cormack <justin.cormack@docker.com> (@justincormack)
Niaz Khan <niazfk@amazon.com> (@niazfk)
Steve Lasker <StevenLasker@hotmail.com> (@stevelasker)
Pritesh Bandi <priteshbandi@gmail.com> (@priteshbandi)
Shiwei Zhang <shizh@microsoft.com> (@shizhMSFT)
Toddy Mladenov <toddysm@gmail.com> (@toddysm)
Vani Rao <vaninrao@amazon.com> (@vaninrao10)
Yi Zha <yizha1@microsoft.com> (@yizha1)
# Repo-Level Maintainers (in alphabetical order)
# Note: This is for the notaryproject/notation-go repo
Junjie Gao <junjiegao@microsoft.com> (@JeyJeyGao)
Milind Gokarn <gokarnm@amazon.com> (@gokarnm)
Patrick Zheng <patrickzheng@microsoft.com> (@Two-Hearts)
Pritesh Bandi <priteshbandi@gmail.com> (@priteshbandi)
Rakesh Gariganti <garigant@amazon.com> (@rgnote)
Shiwei Zhang <shizh@microsoft.com> (@shizhMSFT)
# Emeritus Org Maintainers (in alphabetical order)
Justin Cormack <justin.cormack@docker.com> (@justincormack)
Steve Lasker <StevenLasker@hotmail.com> (@stevelasker)
# Emeritus Repo-Level Maintainers (in alphabetical order)
Junjie Gao <junjiegao@microsoft.com> (@JeyJeyGao)

View File

@ -1,3 +1,16 @@
# Copyright The Notary Project Authors.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}'
@ -16,7 +29,7 @@ clean:
.PHONY: check-line-endings
check-line-endings: ## check line endings
! find . -name "*.go" -type f -exec file "{}" ";" | grep CRLF
! find scripts -name "*.sh" -type f -exec file "{}" ";" | grep CRLF
! find . -name "*.sh" -type f -exec file "{}" ";" | grep CRLF
.PHONY: fix-line-endings
fix-line-endings: ## fix line endings

View File

@ -1,20 +1,28 @@
# Notation
# notation-go
[![Build Status](https://github.com/notaryproject/notation-go/actions/workflows/build.yml/badge.svg?event=push&branch=main)](https://github.com/notaryproject/notation-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain)
[![codecov](https://codecov.io/gh/notaryproject/notation-go/branch/main/graph/badge.svg)](https://codecov.io/gh/notaryproject/notation-go)
[![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)
A collection of libraries for supporting Notation sign, verify, push, pull of oci artifacts. Based on Notary Project standard.
notation-go contains libraries for signing and verification of artifacts as per [Notary Project specifications](https://github.com/notaryproject/specifications). notation-go is being used by [notation](https://github.com/notaryproject/notation) CLI for signing and verifying artifacts.
notation-go reached a stable release as of July 2023 and continues to be actively developed and maintained.
Please visit [README](https://github.com/notaryproject/.github/blob/main/README.md) to know more about Notary Project.
> [!NOTE]
> The Notary Project documentation is available [here](https://notaryproject.dev/docs/).
## Table of Contents
- [Core Documents](#core-documents)
- [Documentation](#documentation)
- [Code of Conduct](#code-of-conduct)
- [License](#license)
## Documentation
## Core Documents
* [Governance for Notation](https://github.com/notaryproject/notary/blob/master/GOVERNANCE.md)
* [Maintainers and reviewers list](https://github.com/notaryproject/notary/blob/master/MAINTAINERS)
Library documentation is available at [Go Reference](https://pkg.go.dev/github.com/notaryproject/notation-go).
## Code of Conduct

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

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (

View File

@ -1,9 +1,23 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/notaryproject/notation-go/dir"
@ -20,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"

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package config provides the ability to load and save config.json and
// signingkeys.json.
package config

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
@ -38,3 +51,15 @@ func TestSaveFile(t *testing.T) {
t.Fatal("save config file failed.")
}
}
func TestLoadNonExistedConfig(t *testing.T) {
dir.UserConfigDir = "./testdata/non-existed"
got, err := LoadConfig()
if err != nil {
t.Fatalf("LoadConfig() error. err = %v", err)
}
if !reflect.DeepEqual(got, NewConfig()) {
t.Errorf("loadFile() = %v, want %v", got, NewConfig())
}
}

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

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
@ -37,9 +50,6 @@ type KeySuite struct {
*ExternalKey
}
var errorKeyNameEmpty = errors.New("key name cannot be empty")
var errKeyNotFound = errors.New("signing key not found")
// SigningKeys reflects the signingkeys.json file.
type SigningKeys struct {
Default *string `json:"default,omitempty"`
@ -54,13 +64,12 @@ func NewSigningKeys() *SigningKeys {
// Add adds new signing key
func (s *SigningKeys) Add(name, keyPath, certPath string, markDefault bool) error {
if name == "" {
return errorKeyNameEmpty
return ErrKeyNameEmpty
}
_, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return err
}
ks := KeySuite{
Name: name,
X509KeyPair: &X509KeyPair{
@ -75,25 +84,20 @@ func (s *SigningKeys) Add(name, keyPath, certPath string, markDefault bool) erro
func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName string, pluginConfig map[string]string, markDefault bool) error {
logger := log.GetLogger(ctx)
logger.Debugf("Adding key with name %v and plugin name %v", keyName, pluginName)
if keyName == "" {
return errorKeyNameEmpty
return ErrKeyNameEmpty
}
if id == "" {
return errors.New("missing key id")
}
if pluginName == "" {
return errors.New("plugin name cannot be empty")
}
mgr := plugin.NewCLIManager(dir.PluginFS())
_, err := mgr.Get(ctx, pluginName)
if err != nil {
return err
}
ks := KeySuite{
Name: keyName,
ExternalKey: &ExternalKey{
@ -102,7 +106,6 @@ func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName str
PluginConfig: pluginConfig,
},
}
if err = s.add(ks, markDefault); err != nil {
logger.Error("Failed to add key with error: %v", err)
return err
@ -114,14 +117,12 @@ func (s *SigningKeys) AddPlugin(ctx context.Context, keyName, id, pluginName str
// Get returns signing key for the given name
func (s *SigningKeys) Get(keyName string) (KeySuite, error) {
if keyName == "" {
return KeySuite{}, errorKeyNameEmpty
return KeySuite{}, ErrKeyNameEmpty
}
idx := slices.IndexIsser(s.Keys, keyName)
if idx < 0 {
return KeySuite{}, errKeyNotFound
return KeySuite{}, KeyNotFoundError{KeyName: keyName}
}
return s.Keys[idx], nil
}
@ -129,9 +130,8 @@ func (s *SigningKeys) Get(keyName string) (KeySuite, error) {
func (s *SigningKeys) GetDefault() (KeySuite, error) {
if s.Default == nil {
return KeySuite{}, errors.New("default signing key not set." +
" Please set default singing key or specify a key name")
" Please set default signing key or specify a key name")
}
return s.Get(*s.Default)
}
@ -140,12 +140,11 @@ func (s *SigningKeys) Remove(keyName ...string) ([]string, error) {
var deletedNames []string
for _, name := range keyName {
if name == "" {
return deletedNames, errorKeyNameEmpty
return deletedNames, ErrKeyNameEmpty
}
idx := slices.IndexIsser(s.Keys, name)
if idx < 0 {
return deletedNames, errors.New(name + ": not found")
return deletedNames, KeyNotFoundError{KeyName: name}
}
s.Keys = slices.Delete(s.Keys, idx)
deletedNames = append(deletedNames, name)
@ -159,13 +158,11 @@ func (s *SigningKeys) Remove(keyName ...string) ([]string, error) {
// UpdateDefault updates default signing key
func (s *SigningKeys) UpdateDefault(keyName string) error {
if keyName == "" {
return errorKeyNameEmpty
return ErrKeyNameEmpty
}
if !slices.ContainsIsser(s.Keys, keyName) {
return fmt.Errorf("key with name '%s' not found", keyName)
return KeyNotFoundError{KeyName: keyName}
}
s.Default = &keyName
return nil
}
@ -176,11 +173,9 @@ func (s *SigningKeys) Save() error {
if err != nil {
return err
}
if err := validateKeys(s); err != nil {
return err
}
return save(path, s)
}
@ -195,11 +190,9 @@ func LoadSigningKeys() (*SigningKeys, error) {
}
return nil, err
}
if err := validateKeys(&config); err != nil {
return nil, err
}
return &config, nil
}
@ -211,11 +204,9 @@ func LoadExecSaveSigningKeys(fn func(keys *SigningKeys) error) error {
if err != nil {
return err
}
if err := fn(signingKeys); err != nil {
return err
}
return signingKeys.Save()
}
@ -228,12 +219,10 @@ func (s *SigningKeys) add(key KeySuite, markDefault bool) error {
if slices.ContainsIsser(s.Keys, key.Name) {
return fmt.Errorf("signing key with name %q already exists", key.Name)
}
s.Keys = append(s.Keys, key)
if markDefault {
s.Default = &key.Name
}
return nil
}
@ -249,17 +238,14 @@ func validateKeys(config *SigningKeys) error {
}
uniqueKeyNames.Add(key.Name)
}
if config.Default != nil {
defaultKey := *config.Default
if len(defaultKey) == 0 {
return fmt.Errorf("malformed %s: default key name cannot be empty", dir.PathSigningKeys)
}
if !uniqueKeyNames.Contains(defaultKey) {
return fmt.Errorf("malformed %s: default key '%s' not found", dir.PathSigningKeys, defaultKey)
}
}
return nil
}

View File

@ -1,9 +1,23 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"os"
"path/filepath"
"reflect"
@ -54,11 +68,11 @@ func TestLoadSigningKeysInfo(t *testing.T) {
}
if !reflect.DeepEqual(sampleSigningKeysInfo.Default, got.Default) {
t.Fatal("singingKeysInfo test failed.")
t.Fatal("signingKeysInfo test failed.")
}
if !reflect.DeepEqual(sampleSigningKeysInfo.Keys, got.Keys) {
t.Fatal("singingKeysInfo test failed.")
t.Fatal("signingKeysInfo test failed.")
}
})
@ -297,14 +311,22 @@ func TestGet(t *testing.T) {
})
t.Run("NonExistent", func(t *testing.T) {
if _, err := sampleSigningKeysInfo.Get("nonExistent"); err == nil {
_, err := sampleSigningKeysInfo.Get("nonExistent")
if err == nil {
t.Error("expected Get() to fail for nonExistent key name")
}
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
t.Error("expected Get() to return ErrorKeyNotFound")
}
})
t.Run("InvalidName", func(t *testing.T) {
if _, err := sampleSigningKeysInfo.Get(""); err == nil {
t.Error("expected Get() to fail for invalid key name")
t.Run("EmptyName", func(t *testing.T) {
_, err := sampleSigningKeysInfo.Get("")
if err == nil {
t.Error("expected Get() to fail for empty key name")
}
if !errors.Is(err, ErrKeyNameEmpty) {
t.Error("expected Get() to return ErrorKeyNameEmpty")
}
})
}
@ -345,14 +367,22 @@ func TestUpdateDefault(t *testing.T) {
})
t.Run("NonExistent", func(t *testing.T) {
if err := sampleSigningKeysInfo.UpdateDefault("nonExistent"); err == nil {
err := sampleSigningKeysInfo.UpdateDefault("nonExistent")
if err == nil {
t.Error("expected Get() to fail for nonExistent key name")
}
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
t.Error("expected Get() to return ErrorKeyNotFound")
}
})
t.Run("InvalidName", func(t *testing.T) {
if err := sampleSigningKeysInfo.UpdateDefault(""); err == nil {
t.Error("expected Get() to fail for invalid key name")
t.Run("EmptyName", func(t *testing.T) {
err := sampleSigningKeysInfo.UpdateDefault("")
if err == nil {
t.Error("expected Get() to fail for empty key name")
}
if !errors.Is(err, ErrKeyNameEmpty) {
t.Error("expected Get() to return ErrorKeyNameEmpty")
}
})
}
@ -369,21 +399,28 @@ func TestRemove(t *testing.T) {
if _, err := testSigningKeysInfo.Get(testKeyName); err == nil {
t.Error("Delete() filed to delete key")
}
if keys[0] != testKeyName {
t.Error("Delete() deleted key name mismatch")
}
})
t.Run("NonExistent", func(t *testing.T) {
if _, err := testSigningKeysInfo.Remove(testKeyName); err == nil {
_, err := testSigningKeysInfo.Remove("nonExistent")
if err == nil {
t.Error("expected Get() to fail for nonExistent key name")
}
if !errors.Is(err, KeyNotFoundError{KeyName: "nonExistent"}) {
t.Error("expected Get() to return ErrorKeyNotFound")
}
})
t.Run("InvalidName", func(t *testing.T) {
if _, err := testSigningKeysInfo.Remove(""); err == nil {
t.Error("expected Get() to fail for invalid key name")
t.Run("EmptyName", func(t *testing.T) {
_, err := testSigningKeysInfo.Remove("")
if err == nil {
t.Error("expected Get() to fail for empty key name")
}
if !errors.Is(err, ErrKeyNameEmpty) {
t.Error("expected Get() to return ErrorKeyNameEmpty")
}
})
}

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dir
import (
@ -38,10 +51,17 @@ func NewSysFS(root string) SysFS {
// ConfigFS is the config SysFS
func ConfigFS() SysFS {
return NewSysFS(UserConfigDir)
return NewSysFS(userConfigDirPath())
}
// PluginFS is the plugin SysFS
func PluginFS() SysFS {
return NewSysFS(filepath.Join(UserLibexecDir, PathPlugins))
return NewSysFS(filepath.Join(userLibexecDirPath(), PathPlugins))
}
// CacheFS is the cache SysFS.
//
// To get the root of crl file cache, use `CacheFS().SysFS(PathCRLCache)`.
func CacheFS() SysFS {
return NewSysFS(userCacheDirPath())
}

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dir
import (
@ -6,7 +19,7 @@ import (
"testing"
)
func Test_sysFS_SysPath(t *testing.T) {
func TestSysFS_SysPath(t *testing.T) {
wantPath := filepath.FromSlash("/path/notation/config.json")
fsys := NewSysFS("/path/notation")
path, err := fsys.SysPath(PathConfigFile)
@ -18,7 +31,7 @@ func Test_sysFS_SysPath(t *testing.T) {
}
}
func Test_OsFs(t *testing.T) {
func TestOsFs(t *testing.T) {
wantData := []byte("data")
fsys := NewSysFS("./testdata")
@ -36,3 +49,36 @@ func Test_OsFs(t *testing.T) {
t.Fatalf("SysFS read failed. got data = %v, want %v", data, wantData)
}
}
func TestConfigFS(t *testing.T) {
configFS := ConfigFS()
path, err := configFS.SysPath(PathConfigFile)
if err != nil {
t.Fatalf("SysPath() failed. err = %v", err)
}
if path != filepath.Join(UserConfigDir, PathConfigFile) {
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(UserConfigDir, PathConfigFile))
}
}
func TestPluginFS(t *testing.T) {
pluginFS := PluginFS()
path, err := pluginFS.SysPath("plugin")
if err != nil {
t.Fatalf("SysPath() failed. err = %v", err)
}
if path != filepath.Join(userLibexecDirPath(), PathPlugins, "plugin") {
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, filepath.Join(userLibexecDirPath(), PathPlugins, "plugin"))
}
}
func TestCRLFileCacheFS(t *testing.T) {
cacheFS := CacheFS()
path, err := cacheFS.SysPath(PathCRLCache)
if err != nil {
t.Fatalf("SysPath() failed. err = %v", err)
}
if path != filepath.Join(UserCacheDir, PathCRLCache) {
t.Fatalf(`SysPath() failed. got: %q, want: %q`, path, UserConfigDir)
}
}

View File

@ -1,5 +1,18 @@
// package dir implements Notation directory structure.
// [directory spec]: https://github.com/notaryproject/notation/blob/main/specs/directory.md
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package dir implements Notation directory structure.
// [directory spec]: https://notaryproject.dev/docs/user-guides/how-to/directory-structure/
//
// Example:
//
@ -18,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
@ -31,6 +44,7 @@ import (
var (
UserConfigDir string // Absolute path of user level {NOTATION_CONFIG}
UserLibexecDir string // Absolute path of user level {NOTATION_LIBEXEC}
UserCacheDir string // Absolute path of user level {NOTATION_CACHE}
)
const (
@ -44,10 +58,15 @@ const (
PathConfigFile = "config.json"
// PathSigningKeys is the signingkeys file relative path.
PathSigningKeys = "signingkeys.json"
// PathTrustPolicy is the trust policy file relative path.
// PathTrustPolicy is the OCI trust policy file relative path.
//
// Deprecated: PathTrustPolicy exists for historical compatibility and should not be used.
// To get OCI trust policy path, use PathOCITrustPolicy.
PathTrustPolicy = "trustpolicy.json"
// PathPlugins is the plugins directory relative path.
PathPlugins = "plugins"
// PathOCITrustPolicy is the OCI trust policy file relative path.
PathOCITrustPolicy = "trustpolicy.oci.json"
// PathBlobTrustPolicy is the Blob trust policy file relative path.
PathBlobTrustPolicy = "trustpolicy.blob.json"
// LocalKeysDir is the directory name for local key relative path.
LocalKeysDir = "localkeys"
// LocalCertificateExtension defines the extension of the certificate files.
@ -58,23 +77,62 @@ const (
TrustStoreDir = "truststore"
)
var userConfigDir = os.UserConfigDir // for unit test
// The relative path to {NOTATION_LIBEXEC}
const (
// PathPlugins is the plugins directory relative path.
PathPlugins = "plugins"
)
func init() {
loadUserPath()
// The relative path to {NOTATION_CACHE}
const (
// PathCRLCache is the crl file cache directory relative path.
PathCRLCache = "crl"
)
// for unit tests
var (
userConfigDir = os.UserConfigDir
userCacheDir = os.UserCacheDir
)
// userConfigDirPath returns the user level {NOTATION_CONFIG} path.
func userConfigDirPath() string {
if UserConfigDir == "" {
userDir, err := userConfigDir()
if err != nil {
// fallback to current directory
UserConfigDir = "." + notation
return UserConfigDir
}
// set user config
UserConfigDir = filepath.Join(userDir, notation)
}
return UserConfigDir
}
// loadUserPath function defines UserConfigDir and UserLibexecDir.
func loadUserPath() {
// set user config
userDir, err := userConfigDir()
if err != nil {
panic(err)
// userLibexecDirPath returns the user level {NOTATION_LIBEXEC} path.
func userLibexecDirPath() string {
if UserLibexecDir == "" {
// set user libexec
UserLibexecDir = userConfigDirPath()
}
UserConfigDir = filepath.Join(userDir, notation)
return UserLibexecDir
}
// set user libexec
UserLibexecDir = UserConfigDir
// userCacheDirPath returns the user level {NOTATION_CACHE} path.
func userCacheDirPath() string {
if UserCacheDir == "" {
userDir, err := userCacheDir()
if err != nil {
// fallback to current directory
UserCacheDir = filepath.Join("."+notation, "cache")
return UserCacheDir
}
// set user cache
UserCacheDir = filepath.Join(userDir, notation)
}
return UserCacheDir
}
// LocalKeyPath returns the local key and local cert relative paths.
@ -87,7 +145,7 @@ func LocalKeyPath(name string) (keyPath, certPath string) {
//
// items includes named-store and cert-file names.
// the directory follows the pattern of
// {NOTATION_CONFIG}/truststore/x509/{named-store}/{cert-file}
// {NOTATION_CONFIG}/truststore/x509/{store-type}/{named-store}/{cert-file}
func X509TrustStoreDir(items ...string) string {
pathItems := []string{TrustStoreDir, "x509"}
pathItems = append(pathItems, items...)

View File

@ -1,30 +1,83 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dir
import (
"os"
"path/filepath"
"testing"
)
func mockGetUserConfig() (string, error) {
func mockUserPath() (string, error) {
return "/path/", nil
}
func Test_loadPath(t *testing.T) {
wantDir := filepath.FromSlash("/path/notation")
userConfigDir = mockGetUserConfig
loadUserPath()
if UserConfigDir != wantDir {
t.Fatalf(`loadPath() UserConfigDir is incorrect. got: %q, want: %q`, UserConfigDir, wantDir)
}
func setup() {
UserConfigDir = ""
UserLibexecDir = ""
UserCacheDir = ""
}
if UserLibexecDir != UserConfigDir {
t.Fatalf(`loadPath() UserLibexecDir is incorrect. got: %q, want: %q`, UserLibexecDir, wantDir)
func Test_UserConfigDirPath(t *testing.T) {
userConfigDir = mockUserPath
setup()
got := userConfigDirPath()
if got != "/path/notation" {
t.Fatalf(`UserConfigDirPath() = %q, want "/path/notation"`, got)
}
}
func Test_NoHomeVariable(t *testing.T) {
t.Setenv("HOME", "")
t.Setenv("XDG_CONFIG_HOME", "")
t.Setenv("XDG_CACHE_HOME", "")
setup()
userConfigDir = os.UserConfigDir
got := userConfigDirPath()
if got != ".notation" {
t.Fatalf(`userConfigDirPath() = %q, want ".notation"`, got)
}
got = userCacheDirPath()
want := filepath.Join("."+notation, "cache")
if got != want {
t.Fatalf(`userCacheDirPath() = %q, want %q`, got, want)
}
}
func Test_UserLibexecDirPath(t *testing.T) {
userConfigDir = mockUserPath
setup()
got := userLibexecDirPath()
if got != "/path/notation" {
t.Fatalf(`UserConfigDirPath() = %q, want "/path/notation"`, got)
}
}
func Test_UserCacheDirPath(t *testing.T) {
userCacheDir = mockUserPath
setup()
got := userCacheDirPath()
if got != "/path/notation" {
t.Fatalf(`UserCacheDirPath() = %q, want "/path/notation"`, got)
}
}
func TestLocalKeyPath(t *testing.T) {
userConfigDir = mockGetUserConfig
loadUserPath()
userConfigDir = mockUserPath
setup()
_ = userConfigDirPath()
_ = userLibexecDirPath()
gotKeyPath, gotCertPath := LocalKeyPath("web")
if gotKeyPath != "localkeys/web.key" {
t.Fatalf(`LocalKeyPath() gotKeyPath = %q, want "localkeys/web.key"`, gotKeyPath)
@ -35,8 +88,10 @@ func TestLocalKeyPath(t *testing.T) {
}
func TestX509TrustStoreDir(t *testing.T) {
userConfigDir = mockGetUserConfig
loadUserPath()
userConfigDir = mockUserPath
setup()
_ = userConfigDirPath()
_ = userLibexecDirPath()
if got := X509TrustStoreDir("ca", "web"); got != "truststore/x509/ca/web" {
t.Fatalf(`X509TrustStoreDir() = %q, want "truststore/x509/ca/web"`, got)
}

View File

@ -1,12 +1,31 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notation
// ErrorPushSignatureFailed is used when failed to push signature to the
// target registry.
type ErrorPushSignatureFailed struct {
//
// Deprecated: Use PushSignatureFailedError instead.
type ErrorPushSignatureFailed = PushSignatureFailedError
// PushSignatureFailedError is used when failed to push signature to the
// target registry.
type PushSignatureFailedError struct {
Msg string
}
func (e ErrorPushSignatureFailed) Error() string {
func (e PushSignatureFailedError) Error() string {
if e.Msg != "" {
return "failed to push signature to registry with error: " + e.Msg
}
@ -15,11 +34,17 @@ func (e ErrorPushSignatureFailed) Error() string {
// ErrorVerificationInconclusive is used when signature verification fails due
// to a runtime error (e.g. a network error)
type ErrorVerificationInconclusive struct {
//
// Deprecated: Use VerificationInconclusiveError instead.
type ErrorVerificationInconclusive = VerificationInconclusiveError
// VerificationInconclusiveError is used when signature verification fails due
// to a runtime error (e.g. a network error)
type VerificationInconclusiveError struct {
Msg string
}
func (e ErrorVerificationInconclusive) Error() string {
func (e VerificationInconclusiveError) Error() string {
if e.Msg != "" {
return e.Msg
}
@ -28,11 +53,17 @@ func (e ErrorVerificationInconclusive) Error() string {
// ErrorNoApplicableTrustPolicy is used when there is no trust policy that
// applies to the given artifact
type ErrorNoApplicableTrustPolicy struct {
//
// Deprecated: Use NoApplicableTrustPolicyError instead.
type ErrorNoApplicableTrustPolicy = NoApplicableTrustPolicyError
// NoApplicableTrustPolicyError is used when there is no trust policy that
// applies to the given artifact
type NoApplicableTrustPolicyError struct {
Msg string
}
func (e ErrorNoApplicableTrustPolicy) Error() string {
func (e NoApplicableTrustPolicyError) Error() string {
if e.Msg != "" {
return e.Msg
}
@ -41,11 +72,17 @@ func (e ErrorNoApplicableTrustPolicy) Error() string {
// ErrorSignatureRetrievalFailed is used when notation is unable to retrieve the
// digital signature/s for the given artifact
type ErrorSignatureRetrievalFailed struct {
//
// Deprecated: Use SignatureRetrievalFailedError instead.
type ErrorSignatureRetrievalFailed = SignatureRetrievalFailedError
// SignatureRetrievalFailedError is used when notation is unable to retrieve the
// digital signature/s for the given artifact
type SignatureRetrievalFailedError struct {
Msg string
}
func (e ErrorSignatureRetrievalFailed) Error() string {
func (e SignatureRetrievalFailedError) Error() string {
if e.Msg != "" {
return e.Msg
}
@ -54,11 +91,17 @@ func (e ErrorSignatureRetrievalFailed) Error() string {
// ErrorVerificationFailed is used when it is determined that the digital
// signature/s is not valid for the given artifact
type ErrorVerificationFailed struct {
//
// Deprecated: Use VerificationFailedError instead.
type ErrorVerificationFailed = VerificationFailedError
// VerificationFailedError is used when it is determined that the digital
// signature/s is not valid for the given artifact
type VerificationFailedError struct {
Msg string
}
func (e ErrorVerificationFailed) Error() string {
func (e VerificationFailedError) Error() string {
if e.Msg != "" {
return e.Msg
}
@ -67,11 +110,17 @@ func (e ErrorVerificationFailed) Error() string {
// ErrorUserMetadataVerificationFailed is used when the signature does not
// contain the user specified metadata
type ErrorUserMetadataVerificationFailed struct {
//
// Deprecated: Use UserMetadataVerificationFailedError instead.
type ErrorUserMetadataVerificationFailed = UserMetadataVerificationFailedError
// UserMetadataVerificationFailedError is used when the signature does not
// contain the user specified metadata
type UserMetadataVerificationFailedError struct {
Msg string
}
func (e ErrorUserMetadataVerificationFailed) Error() string {
func (e UserMetadataVerificationFailedError) Error() string {
if e.Msg != "" {
return e.Msg
}

170
errors_test.go Normal file
View File

@ -0,0 +1,170 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notation
import "testing"
func TestErrorMessages(t *testing.T) {
tests := []struct {
name string
err error
want string
}{
{
name: "ErrorPushSignatureFailed with message",
err: ErrorPushSignatureFailed{Msg: "test message"},
want: "failed to push signature to registry with error: test message",
},
{
name: "ErrorPushSignatureFailed without message",
err: ErrorPushSignatureFailed{},
want: "failed to push signature to registry",
},
{
name: "ErrorVerificationInconclusive with message",
err: ErrorVerificationInconclusive{Msg: "test message"},
want: "test message",
},
{
name: "ErrorVerificationInconclusive without message",
err: ErrorVerificationInconclusive{},
want: "signature verification was inclusive due to an unexpected error",
},
{
name: "ErrorNoApplicableTrustPolicy with message",
err: ErrorNoApplicableTrustPolicy{Msg: "test message"},
want: "test message",
},
{
name: "ErrorNoApplicableTrustPolicy without message",
err: ErrorNoApplicableTrustPolicy{},
want: "there is no applicable trust policy for the given artifact",
},
{
name: "ErrorSignatureRetrievalFailed with message",
err: ErrorSignatureRetrievalFailed{Msg: "test message"},
want: "test message",
},
{
name: "ErrorSignatureRetrievalFailed without message",
err: ErrorSignatureRetrievalFailed{},
want: "unable to retrieve the digital signature from the registry",
},
{
name: "ErrorVerificationFailed with message",
err: ErrorVerificationFailed{Msg: "test message"},
want: "test message",
},
{
name: "ErrorVerificationFailed without message",
err: ErrorVerificationFailed{},
want: "signature verification failed",
},
{
name: "ErrorUserMetadataVerificationFailed with message",
err: ErrorUserMetadataVerificationFailed{Msg: "test message"},
want: "test message",
},
{
name: "ErrorUserMetadataVerificationFailed without message",
err: ErrorUserMetadataVerificationFailed{},
want: "unable to find specified metadata in the signature",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.want {
t.Errorf("Error() = %v, want %v", got, tt.want)
}
})
}
}
func TestCustomErrorPrintsCorrectMessage(t *testing.T) {
tests := []struct {
name string
err error
want string
}{
{
name: "PushSignatureFailedError with message",
err: PushSignatureFailedError{Msg: "test message"},
want: "failed to push signature to registry with error: test message",
},
{
name: "PushSignatureFailedError without message",
err: PushSignatureFailedError{},
want: "failed to push signature to registry",
},
{
name: "VerificationInconclusiveError with message",
err: VerificationInconclusiveError{Msg: "test message"},
want: "test message",
},
{
name: "VerificationInconclusiveError without message",
err: VerificationInconclusiveError{},
want: "signature verification was inclusive due to an unexpected error",
},
{
name: "NoApplicableTrustPolicyError with message",
err: NoApplicableTrustPolicyError{Msg: "test message"},
want: "test message",
},
{
name: "NoApplicableTrustPolicyError without message",
err: NoApplicableTrustPolicyError{},
want: "there is no applicable trust policy for the given artifact",
},
{
name: "SignatureRetrievalFailedError with message",
err: SignatureRetrievalFailedError{Msg: "test message"},
want: "test message",
},
{
name: "SignatureRetrievalFailedError without message",
err: SignatureRetrievalFailedError{},
want: "unable to retrieve the digital signature from the registry",
},
{
name: "VerificationFailedError with message",
err: VerificationFailedError{Msg: "test message"},
want: "test message",
},
{
name: "VerificationFailedError without message",
err: VerificationFailedError{},
want: "signature verification failed",
},
{
name: "UserMetadataVerificationFailedError with message",
err: UserMetadataVerificationFailedError{Msg: "test message"},
want: "test message",
},
{
name: "UserMetadataVerificationFailedError without message",
err: UserMetadataVerificationFailedError{},
want: "unable to find specified metadata in the signature",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.want {
t.Errorf("Error() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notation_test
import (
@ -33,9 +46,9 @@ func Example_localSign() {
// exampleSigner is a notation.Signer given key and X509 certificate chain.
// Users should replace `exampleCertTuple.PrivateKey` with their own private
// key and replace `exampleCerts` with the corresponding full certificate
// chain, following the Notary certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
exampleSigner, err := signer.New(exampleCertTuple.PrivateKey, exampleCerts)
// chain, following the Notary Project certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
if err != nil {
panic(err) // Handle error
}

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notation_test
import (
@ -18,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"},
@ -60,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
}
@ -159,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

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notation_test
import (
@ -5,12 +18,13 @@ import (
"crypto/x509"
"fmt"
"oras.land/oras-go/v2/registry/remote"
"github.com/notaryproject/notation-core-go/signature/cose"
"github.com/notaryproject/notation-core-go/testhelper"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/signer"
"oras.land/oras-go/v2/registry/remote"
)
// Both COSE ("application/cose") and JWS ("application/jose+json")
@ -31,9 +45,9 @@ func Example_remoteSign() {
// exampleSigner is a notation.Signer given key and X509 certificate chain.
// Users should replace `exampleCertTuple.PrivateKey` with their own private
// key and replace `exampleCerts` with the corresponding full certificate
// chain, following the Notary certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
exampleSigner, err := signer.New(exampleCertTuple.PrivateKey, exampleCerts)
// chain, following the Notary Project certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleSigner, err := signer.NewGenericSigner(exampleCertTuple.PrivateKey, exampleCerts)
if err != nil {
panic(err) // Handle error
}
@ -56,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

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notation_test
import (
@ -24,10 +37,10 @@ func Example_remoteVerify() {
// examplePolicyDocument is an example of a valid trust policy document.
// trust policy document should follow this spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-policy
examplePolicyDocument := trustpolicy.Document{
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-policy
examplePolicyDocument := trustpolicy.OCIDocument{
Version: "1.0",
TrustPolicies: []trustpolicy.TrustPolicy{
TrustPolicies: []trustpolicy.OCITrustPolicy{
{
Name: "test-statement-name",
RegistryScopes: []string{"*"},
@ -39,9 +52,9 @@ func Example_remoteVerify() {
}
// generateTrustStore generates a trust store directory for demo purpose.
// Users could use the default trust store from Notary and add trusted
// certificates into it following the trust store spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/trust-store-trust-policy.md#trust-store
// Users should configure their own trust store and add trusted certificates
// into it following the trust store spec:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/trust-store-trust-policy.md#trust-store
if err := generateTrustStore(); err != nil {
panic(err) // Handle error
}
@ -88,8 +101,8 @@ func generateTrustStore() error {
// an example of a valid X509 self-signed certificate for demo purpose ONLY.
// Users should replace `exampleX509Certificate` with their own trusted
// certificate and add to the trust store, following the
// Notary certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0-rc.1/specs/signature-specification.md#certificate-requirements
// Notary Project certificate requirements:
// https://github.com/notaryproject/notaryproject/blob/v1.0.0/specs/signature-specification.md#certificate-requirements
exampleX509Certificate := `-----BEGIN CERTIFICATE-----
MIIDQDCCAiigAwIBAgIBUTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJVUzEL
MAkGA1UECBMCV0ExEDAOBgNVBAcTB1NlYXR0bGUxDzANBgNVBAoTBk5vdGFyeTEP

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

27
go.mod
View File

@ -1,23 +1,26 @@
module github.com/notaryproject/notation-go
go 1.19
go 1.23.0
require (
github.com/go-ldap/ldap/v3 v3.4.4
github.com/notaryproject/notation-core-go v1.0.0-rc.4
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-rc.3
github.com/veraison/go-cose v1.1.0
golang.org/x/crypto v0.9.0
golang.org/x/mod v0.10.0
oras.land/oras-go/v2 v2.2.0
github.com/opencontainers/image-spec v1.1.1
github.com/veraison/go-cose v1.3.0
golang.org/x/crypto v0.39.0
golang.org/x/mod v0.25.0
oras.land/oras-go/v2 v2.6.0
)
require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
github.com/golang-jwt/jwt/v4 v4.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.2.0 // indirect
golang.org/x/sync v0.14.0 // indirect
)

85
go.sum
View File

@ -1,47 +1,60 @@
github.com/Azure/go-ntlmssp v0.0.0-20220621081337-cb9428e4ac1e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.4 h1:qPjipEpt+qDa6SI/h1fzuGWoRUY+qqQ9sOZq67/PYUs=
github.com/go-ldap/ldap/v3 v3.4.4/go.mod h1:fe1MsuN5eJJ1FeLT/LEBVdWfNWKh459R7aXgXtJC+aI=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/notaryproject/notation-core-go v1.0.0-rc.4 h1:gzo4JzKRMLGoOeOhPXxoudjL79Mi9X6flS8qJbRtZ+k=
github.com/notaryproject/notation-core-go v1.0.0-rc.4/go.mod h1:PEHrnhW0mEIVpyYdXqAJoJAaUgfz757tqxB3LG4qcag=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/notaryproject/notation-core-go v1.3.0 h1:mWJaw1QBpBxpjLSiKOjzbZvB+xh2Abzk14FHWQ+9Kfs=
github.com/notaryproject/notation-core-go v1.3.0/go.mod h1:hzvEOit5lXfNATGNBT8UQRx2J6Fiw/dq/78TQL8aE64=
github.com/notaryproject/notation-plugin-framework-go v1.0.0 h1:6Qzr7DGXoCgXEQN+1gTZWuJAZvxh3p8Lryjn5FaLzi4=
github.com/notaryproject/notation-plugin-framework-go v1.0.0/go.mod h1:RqWSrTOtEASCrGOEffq0n8pSg2KOgKYiWqFWczRSics=
github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4=
github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0-rc.3 h1:GT9Xon8YrLxz6N7sErbN81V8J4lOQKGUZQmI3ioviqU=
github.com/opencontainers/image-spec v1.1.0-rc.3/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o=
github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7rk=
github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
oras.land/oras-go/v2 v2.2.0 h1:E1fqITD56Eg5neZbxBtAdZVgDHD6wBabJo6xESTcQyo=
oras.land/oras-go/v2 v2.2.0/go.mod h1:pXjn0+KfarspMHHNR3A56j3tgvr+mxArHuI8qVn59v8=
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package set
// Set is a map as a set data structure.

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package envelope
import (

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package envelope
import (

View File

@ -1,8 +1,153 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import "regexp"
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"regexp"
"strings"
)
const (
// tempFileNamePrefix is the prefix of the temporary file
tempFileNamePrefix = "notation-*"
)
// ErrNotRegularFile is returned when the file is not an regular file.
var ErrNotRegularFile = errors.New("not regular file")
// ErrNotDirectory is returned when the path is not a directory.
var ErrNotDirectory = errors.New("not directory")
// IsValidFileName checks if a file name is cross-platform compatible
func IsValidFileName(fileName string) bool {
return regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`).MatchString(fileName)
}
// CopyToDir copies the src file to dst dir. All parent directories are created
// with permissions 0755.
//
// Source file's read and execute permissions are preserved for everyone.
// Write permission is preserved for owner. Group and others cannot write.
// Existing file will be overwritten.
func CopyToDir(src, dst string) error {
sourceFileInfo, err := os.Stat(src)
if err != nil {
return err
}
if !sourceFileInfo.Mode().IsRegular() {
return ErrNotRegularFile
}
source, err := os.Open(src)
if err != nil {
return err
}
defer source.Close()
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}
dstFile := filepath.Join(dst, filepath.Base(src))
destination, err := os.Create(dstFile)
if err != nil {
return err
}
defer destination.Close()
err = destination.Chmod(sourceFileInfo.Mode() & os.FileMode(0755))
if err != nil {
return err
}
_, err = io.Copy(destination, source)
return err
}
// CopyDirToDir copies contents in src dir to dst dir. Only regular files are
// copied. Existing files will be overwritten.
func CopyDirToDir(src, dst string) error {
fi, err := os.Stat(src)
if err != nil {
return err
}
if !fi.Mode().IsDir() {
return ErrNotDirectory
}
return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// skip sub-directories
if d.IsDir() && d.Name() != filepath.Base(path) {
return fs.SkipDir
}
info, err := d.Info()
if err != nil {
return err
}
// only copy regular files
if info.Mode().IsRegular() {
return CopyToDir(path, dst)
}
return nil
})
}
// TrimFileExtension returns the file name without extension.
//
// For example,
//
// when input is xyz.exe, output is xyz
//
// when input is xyz.tar.gz, output is xyz.tar
func TrimFileExtension(fileName string) string {
return strings.TrimSuffix(fileName, filepath.Ext(fileName))
}
// WriteFile writes content to a temporary file and moves it to path.
// If path already exists and is a file, WriteFile overwrites it.
//
// Parameters:
// - tempDir is the directory to create the temporary file. It should be
// in the same mount point as path. If tempDir is empty, the default
// directory for temporary files is used.
// - path is the destination file path.
// - content is the content to write.
func WriteFile(tempDir, path string, content []byte) (writeErr error) {
tempFile, err := os.CreateTemp(tempDir, tempFileNamePrefix)
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
defer func() {
// remove the temp file in case of error
if writeErr != nil {
tempFile.Close()
os.Remove(tempFile.Name())
}
}()
if _, err := tempFile.Write(content); err != nil {
return fmt.Errorf("failed to write content to temp file: %w", err)
}
// close before moving
if err := tempFile.Close(); err != nil {
return fmt.Errorf("failed to close temp file: %w", err)
}
// rename is atomic on UNIX-like platforms
return os.Rename(tempFile.Name(), path)
}

214
internal/file/file_test.go Normal file
View File

@ -0,0 +1,214 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import (
"bytes"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestCopyToDir(t *testing.T) {
t.Run("copy file", func(t *testing.T) {
tempDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
t.Fatal(err)
}
if err := WriteFile(tempDir, filename, data); err != nil {
t.Fatal(err)
}
destDir := filepath.Join(tempDir, "b")
if err := CopyToDir(filename, destDir); err != nil {
t.Fatal(err)
}
})
t.Run("source directory permission error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
tempDir := t.TempDir()
destDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
t.Fatal(err)
}
if err := WriteFile(tempDir, filename, data); err != nil {
t.Fatal(err)
}
if err := os.Chmod(tempDir, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(tempDir, 0700)
if err := CopyToDir(filename, destDir); err == nil {
t.Fatal("should have error")
}
})
t.Run("not a regular file", func(t *testing.T) {
tempDir := t.TempDir()
destDir := t.TempDir()
if err := CopyToDir(tempDir, destDir); err == nil {
t.Fatal("should have error")
}
})
t.Run("source file permission error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
tempDir := t.TempDir()
destDir := t.TempDir()
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
t.Fatal(err)
}
if err := WriteFile(tempDir, filename, data); err != nil {
t.Fatal(err)
}
// forbid reading
if err := os.Chmod(filename, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(filename, 0600)
if err := CopyToDir(filename, destDir); err == nil {
t.Fatal("should have error")
}
})
t.Run("dest directory permission error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
tempDir := t.TempDir()
destTempDir := t.TempDir()
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
t.Fatal(err)
}
if err := WriteFile(tempDir, filename, data); err != nil {
t.Fatal(err)
}
// forbid dest directory operation
if err := os.Chmod(destTempDir, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(destTempDir, 0700)
if err := CopyToDir(filename, filepath.Join(destTempDir, "a")); err == nil {
t.Fatal("should have error")
}
})
t.Run("dest directory permission error 2", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
tempDir := t.TempDir()
destTempDir := t.TempDir()
data := []byte("data")
// prepare file
filename := filepath.Join(tempDir, "a", "file.txt")
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
t.Fatal(err)
}
if err := WriteFile(tempDir, filename, data); err != nil {
t.Fatal(err)
}
// forbid writing to destTempDir
if err := os.Chmod(destTempDir, 0000); err != nil {
t.Fatal(err)
}
defer os.Chmod(destTempDir, 0700)
if err := CopyToDir(filename, destTempDir); err == nil {
t.Fatal("should have error")
}
})
t.Run("copy file and check content", func(t *testing.T) {
tempDir := t.TempDir()
data := []byte("data")
filename := filepath.Join(tempDir, "a", "file.txt")
if err := os.MkdirAll(filepath.Dir(filename), 0700); err != nil {
t.Fatal(err)
}
if err := WriteFile(tempDir, filename, data); err != nil {
t.Fatal(err)
}
destDir := filepath.Join(tempDir, "b")
if err := CopyToDir(filename, destDir); err != nil {
t.Fatal(err)
}
validFileContent(t, filepath.Join(destDir, "file.txt"), data)
})
}
func TestFileNameWithoutExtension(t *testing.T) {
input := "testfile.tar.gz"
expectedOutput := "testfile.tar"
actualOutput := TrimFileExtension(input)
if actualOutput != expectedOutput {
t.Errorf("expected '%s', but got '%s'", expectedOutput, actualOutput)
}
}
func TestWriteFile(t *testing.T) {
tempDir := t.TempDir()
content := []byte("test WriteFile")
t.Run("permission denied", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
err := os.Chmod(tempDir, 0)
if err != nil {
t.Fatal(err)
}
err = WriteFile(tempDir, filepath.Join(tempDir, "testFile"), content)
if err == nil || !strings.Contains(err.Error(), "permission denied") {
t.Fatalf("expected permission denied error, but got %s", err)
}
err = os.Chmod(tempDir, 0700)
if err != nil {
t.Fatal(err)
}
})
}
func validFileContent(t *testing.T, filename string, content []byte) {
b, err := os.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(content, b) {
t.Fatal("file content is not correct")
}
}

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

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mockfs
import (

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package mock
import (
@ -5,8 +18,7 @@ import (
_ "embed"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-go/plugin"
"github.com/notaryproject/notation-go/plugin/proto"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
@ -113,6 +125,7 @@ type Repository struct {
FetchSignatureBlobError error
MissMatchDigest bool
ExceededNumOfSignatures bool
PushSignatureError error
}
func NewRepository() Repository {
@ -151,40 +164,44 @@ func (t Repository) FetchSignatureBlob(ctx context.Context, desc ocispec.Descrip
}
func (t Repository) PushSignature(ctx context.Context, mediaType string, blob []byte, subject ocispec.Descriptor, annotations map[string]string) (blobDesc, manifestDesc ocispec.Descriptor, err error) {
if t.PushSignatureError != nil {
return ocispec.Descriptor{}, ocispec.Descriptor{}, t.PushSignatureError
}
return ocispec.Descriptor{}, ocispec.Descriptor{}, nil
}
type PluginMock struct {
Metadata proto.GetMetadataResponse
Metadata plugin.GetMetadataResponse
ExecuteResponse interface{}
ExecuteError error
}
func (p *PluginMock) GetMetadata(ctx context.Context, req *proto.GetMetadataRequest) (*proto.GetMetadataResponse, error) {
func (p *PluginMock) GetMetadata(ctx context.Context, req *plugin.GetMetadataRequest) (*plugin.GetMetadataResponse, error) {
return &p.Metadata, nil
}
func (p *PluginMock) VerifySignature(ctx context.Context, req *proto.VerifySignatureRequest) (*proto.VerifySignatureResponse, error) {
if resp, ok := p.ExecuteResponse.(*proto.VerifySignatureResponse); ok {
func (p *PluginMock) VerifySignature(ctx context.Context, req *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) {
if resp, ok := p.ExecuteResponse.(*plugin.VerifySignatureResponse); ok {
return resp, nil
}
return nil, p.ExecuteError
}
func (p *PluginMock) DescribeKey(ctx context.Context, req *proto.DescribeKeyRequest) (*proto.DescribeKeyResponse, error) {
func (p *PluginMock) DescribeKey(ctx context.Context, req *plugin.DescribeKeyRequest) (*plugin.DescribeKeyResponse, error) {
panic("not implemented") // TODO: Implement
}
func (p *PluginMock) GenerateSignature(ctx context.Context, req *proto.GenerateSignatureRequest) (*proto.GenerateSignatureResponse, error) {
func (p *PluginMock) GenerateSignature(ctx context.Context, req *plugin.GenerateSignatureRequest) (*plugin.GenerateSignatureResponse, error) {
panic("not implemented") // TODO: Implement
}
func (p *PluginMock) GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error) {
func (p *PluginMock) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) {
panic("not implemented") // TODO: Implement
}
type PluginManager struct {
PluginCapabilities []proto.Capability
PluginCapabilities []plugin.Capability
GetPluginError error
PluginRunnerLoadError error
PluginRunnerExecuteResponse interface{}
@ -193,7 +210,7 @@ type PluginManager struct {
func (pm PluginManager) Get(ctx context.Context, name string) (plugin.Plugin, error) {
return &PluginMock{
Metadata: proto.GetMetadataResponse{
Metadata: plugin.GetMetadataResponse{
Name: "plugin-name",
Description: "for mocking in unit tests",
Version: "1.0.0",

View File

@ -0,0 +1,47 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ocilayout
import (
"context"
"os"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/content/oci"
)
// Copy creates a temporary OCI layout for testing
// and returns the path to the layout.
func Copy(sourcePath, destPath, tag string) error {
ctx := context.Background()
srcStore, err := oci.NewFromFS(ctx, os.DirFS(sourcePath))
if err != nil {
return err
}
// create a dest store for store the generated oci layout.
destStore, err := oci.New(destPath)
if err != nil {
return err
}
// copy data
_, err = oras.ExtendedCopy(ctx, srcStore, tag, destStore, "", oras.DefaultExtendedCopyOptions)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,64 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ocilayout
import (
"os"
"runtime"
"testing"
)
func TestCopy(t *testing.T) {
t.Run("empty oci layout", func(t *testing.T) {
err := Copy("", "", "v2")
if err == nil {
t.Errorf("expected error, got nil")
}
})
t.Run("invalid target path permission", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
tempDir := t.TempDir()
// change the permission of the tempDir to make it invalid
if err := os.Chmod(tempDir, 0); err != nil {
t.Fatalf("failed to change the permission of the tempDir: %v", err)
}
err := Copy("../../testdata/oci-layout", tempDir, "v2")
if err == nil {
t.Errorf("expected error, got nil")
}
if err := os.Chmod(tempDir, 0755); err != nil {
t.Fatalf("failed to change the permission of the tempDir: %v", err)
}
})
t.Run("copy failed", func(t *testing.T) {
tempDir := t.TempDir()
err := Copy("../../testdata/oci-layout", tempDir, "v3")
if err == nil {
t.Errorf("expected error, got nil")
}
})
t.Run("copy success", func(t *testing.T) {
tempDir := t.TempDir()
err := Copy("../../testdata/oci-layout", tempDir, "v2")
if err != nil {
t.Errorf("expected nil, got %v", err)
}
})
}

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

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package pkix
import (
@ -13,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 {
@ -26,6 +38,10 @@ func ParseDistinguishedName(name string) (map[string]string, error) {
return nil, fmt.Errorf("distinguished name (DN) %q has multi-valued RDN attributes, remove multi-valued RDN attributes as they are not supported", name)
}
for _, attribute := range rdn.Attributes {
// stateOrProvince name 'S' is an alias for 'ST'
if attribute.Type == "S" {
attribute.Type = "ST"
}
if attrKeyValue[attribute.Type] == "" {
attrKeyValue[attribute.Type] = attribute.Value
} else {
@ -35,11 +51,13 @@ func ParseDistinguishedName(name string) (map[string]string, error) {
}
// Verify mandatory fields are present
mandatoryFields := []string{"C", "ST", "O"}
for _, field := range mandatoryFields {
if attrKeyValue[field] == "" {
return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST', and 'O' RDN attributes at a minimum", name, field)
return nil, fmt.Errorf("distinguished name (DN) %q has no mandatory RDN attribute for %q, it must contain 'C', 'ST' or 'S', and 'O' RDN attributes at a minimum", name, field)
}
}
// No errors
return attrKeyValue, nil
}

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

49
internal/semver/semver.go Normal file
View File

@ -0,0 +1,49 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package semver provides functions related to semanic version.
// This package is based on "golang.org/x/mod/semver"
package semver
import (
"fmt"
"regexp"
"golang.org/x/mod/semver"
)
// semVerRegEx is taken from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
var semVerRegEx = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
// IsValid returns true if version is a valid semantic version
func IsValid(version string) bool {
return semVerRegEx.MatchString(version)
}
// ComparePluginVersion validates and compares two plugin semantic versions.
//
// The result will be 0 if v == w, -1 if v < w, or +1 if v > w.
func ComparePluginVersion(v, w string) (int, error) {
// sanity check
if !IsValid(v) {
return 0, fmt.Errorf("%s is not a valid semantic version", v)
}
if !IsValid(w) {
return 0, fmt.Errorf("%s is not a valid semantic version", w)
}
// golang.org/x/mod/semver requires semantic version strings must begin
// with a leading "v". Adding prefix "v" to the inputs.
// Reference: https://pkg.go.dev/golang.org/x/mod/semver#pkg-overview
return semver.Compare("v"+v, "v"+w), nil
}

View File

@ -0,0 +1,40 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package semver
import "testing"
func TestComparePluginVersion(t *testing.T) {
t.Run("compare with lower version", func(t *testing.T) {
comp, err := ComparePluginVersion("1.0.0", "1.0.1")
if err != nil || comp >= 0 {
t.Fatal("expected nil err and negative comp")
}
})
t.Run("compare with equal version", func(t *testing.T) {
comp, err := ComparePluginVersion("1.0.1", "1.0.1")
if err != nil || comp != 0 {
t.Fatal("expected nil err and comp equal to 0")
}
})
t.Run("failed due to invalid semantic version", func(t *testing.T) {
expectedErrMsg := "v1.0.0 is not a valid semantic version"
_, err := ComparePluginVersion("v1.0.0", "1.0.1")
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected err %s, but got %s", expectedErrMsg, err)
}
})
}

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package slices
// Contains reports whether v is present in s.

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package trustpolicy
const (

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package log provides logging functionality to notation.
// Users who want to enable logging option in notation should implement the
// log.Logger interface and include it in context by calling log.WithLogger.

41
log/log_test.go Normal file
View File

@ -0,0 +1,41 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package log provides logging functionality to notation.
// Users who want to enable logging option in notation should implement the
// log.Logger interface and include it in context by calling log.WithLogger.
// 3rd party loggers that implement log.Logger: github.com/uber-go/zap.SugaredLogger
// and github.com/sirupsen/logrus.Logger.
package log
import (
"context"
"testing"
)
func TestWithLoggerAndGetLogger(t *testing.T) {
tl := &discardLogger{}
ctx := WithLogger(context.Background(), tl)
if got := GetLogger(ctx); got != tl {
t.Errorf("GetLogger() = %v, want %v", got, tl)
}
}
func TestGetLoggerWithNoLogger(t *testing.T) {
ctx := context.Background()
if got := GetLogger(ctx); got != Discard {
t.Errorf("GetLogger() = %v, want Discard", got)
}
}

View File

@ -1,34 +1,57 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package notation provides signer and verifier for notation Sign
// and Verification.
// and Verification. It supports both OCI artifact and arbitrary blob.
package notation
import (
"context"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"strings"
"time"
orasRegistry "oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-core-go/signature/cose"
"github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go/internal/envelope"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/tspclient-go"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
orasRegistry "oras.land/oras-go/v2/registry"
)
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
// Currently, both `application/jose+json` and `application/cose` are
// supported.
SignatureMediaType string
@ -41,17 +64,60 @@ 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 artifact.
// Signer is a generic interface for signing an OCI artifact.
// The interface allows signing with local or remote keys,
// and packing in various signature formats.
type Signer interface {
// Sign signs the artifact described by its descriptor,
// Sign signs the OCI artifact described by its descriptor,
// and returns the signature and SignerInfo.
Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error)
}
// SignBlobOptions contains parameters for [notation.SignBlob].
type SignBlobOptions struct {
SignerSignOptions
// ContentMediaType is the media-type of the blob being signed.
ContentMediaType string
// UserMetadata contains key-value pairs that are added to the signature
// payload
UserMetadata map[string]string
}
// BlobDescriptorGenerator creates descriptor using the digest Algorithm.
// Below is the example of minimal descriptor, it must contain mediatype,
// digest and size of the artifact.
//
// {
// "mediaType": "application/octet-stream",
// "digest": "sha256:2f3a23b6373afb134ddcd864be8e037e34a662d090d33ee849471ff73c873345",
// "size": 1024
// }
type BlobDescriptorGenerator func(digest.Algorithm) (ocispec.Descriptor, error)
// BlobSigner is a generic interface for signing arbitrary data.
// The interface allows signing with local or remote keys,
// and packing in various signature formats.
type BlobSigner interface {
// SignBlob signs the descriptor returned by genDesc, and returns the
// signature and SignerInfo.
SignBlob(ctx context.Context, genDesc BlobDescriptorGenerator, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error)
}
// signerAnnotation facilitates return of manifest annotations by signers
type signerAnnotation interface {
// PluginAnnotations returns signature manifest annotations returned from
@ -59,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
@ -72,21 +138,32 @@ type SignOptions struct {
UserMetadata map[string]string
}
// Sign signs the artifact and push the signature to the Repository.
// The descriptor of the sign content is returned upon sucessful signing.
// 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 signer == nil {
return ocispec.Descriptor{}, errors.New("signer cannot be nil")
if err := validateSignArguments(signer, signOpts.SignerSignOptions); err != nil {
return ocispec.Descriptor{}, ocispec.Descriptor{}, err
}
if repo == nil {
return ocispec.Descriptor{}, errors.New("repo cannot be nil")
}
if signOpts.ExpiryDuration < 0 {
return ocispec.Descriptor{}, fmt.Errorf("expiry duration cannot be a negative value")
}
if signOpts.ExpiryDuration%time.Second != 0 {
return ocispec.Descriptor{}, fmt.Errorf("expiry duration supports minimum granularity of seconds")
return ocispec.Descriptor{}, ocispec.Descriptor{}, errors.New("repo cannot be nil")
}
logger := log.GetLogger(ctx)
@ -95,73 +172,114 @@ 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
if errors.As(err, &referrerError) && referrerError.IsReferrersIndexDelete() {
// return the descriptors for referrersIndexDelete error as
// the signature is successfully pushed to the repository
return artifactManifestDesc, sigManifestDesc, err
}
logger.Error("Failed to push the signature")
return ocispec.Descriptor{}, ErrorPushSignatureFailed{Msg: err.Error()}
return ocispec.Descriptor{}, ocispec.Descriptor{}, ErrorPushSignatureFailed{Msg: err.Error()}
}
return artifactManifestDesc, sigManifestDesc, nil
}
// SignBlob signs the arbitrary data from blobReader and returns
// the signature and SignerInfo.
func SignBlob(ctx context.Context, signer BlobSigner, blobReader io.Reader, signBlobOpts SignBlobOptions) ([]byte, *signature.SignerInfo, error) {
// sanity checks
if err := validateSignArguments(signer, signBlobOpts.SignerSignOptions); err != nil {
return nil, nil, err
}
if blobReader == nil {
return nil, nil, errors.New("blobReader cannot be nil")
}
if signBlobOpts.ContentMediaType == "" {
return nil, nil, errors.New("content media-type cannot be empty")
}
if err := validateContentMediaType(signBlobOpts.ContentMediaType); err != nil {
return nil, nil, err
}
return targetDesc, nil
getDescFunc := getDescriptorFunc(ctx, blobReader, signBlobOpts.ContentMediaType, signBlobOpts.UserMetadata)
return signer.SignBlob(ctx, getDescFunc, signBlobOpts.SignerSignOptions)
}
func validateSignArguments(signer any, signOpts SignerSignOptions) error {
if signer == nil {
return errors.New("signer cannot be nil")
}
if signOpts.ExpiryDuration < 0 {
return errors.New("expiry duration cannot be a negative value")
}
if signOpts.ExpiryDuration%time.Second != 0 {
return errors.New("expiry duration supports minimum granularity of seconds")
}
if signOpts.SignatureMediaType == "" {
return errors.New("signature media-type cannot be empty")
}
if err := validateSigMediaType(signOpts.SignatureMediaType); err != nil {
return err
}
return nil
}
func addUserMetadataToDescriptor(ctx context.Context, desc ocispec.Descriptor, userMetadata map[string]string) (ocispec.Descriptor, error) {
logger := log.GetLogger(ctx)
if desc.Annotations == nil && len(userMetadata) > 0 {
desc.Annotations = map[string]string{}
}
for k, v := range userMetadata {
logger.Debugf("Adding metadata %v=%v to annotations", k, v)
for _, reservedPrefix := range reservedAnnotationPrefixes {
if strings.HasPrefix(k, reservedPrefix) {
return desc, fmt.Errorf("error adding user metadata: metadata key %v has reserved prefix %v", k, reservedPrefix)
}
}
if _, ok := desc.Annotations[k]; ok {
return desc, fmt.Errorf("error adding user metadata: metadata key %v is already present in the target artifact", k)
}
desc.Annotations[k] = v
}
return desc, nil
}
@ -180,7 +298,7 @@ type ValidationResult struct {
Error error
}
// VerificationOutcome encapsulates a signature blob's descriptor, its content,
// VerificationOutcome encapsulates a signature envelope blob, its content,
// the verification level and results for each verification type that was
// performed.
type VerificationOutcome struct {
@ -203,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")
@ -213,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 been
// 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
@ -240,24 +358,51 @@ 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 been
// ArtifactReference is the reference of the artifact that is being
// verified against to.
ArtifactReference string
@ -274,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
@ -299,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)
@ -307,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
@ -328,18 +515,18 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
}
if ref.ValidateReferenceAsDigest() != nil {
// artifactRef is not a digest reference
logger.Infof("Resolved artifact tag `%s` to digest `%s` before verification", ref.Reference, artifactDescriptor.Digest.String())
logger.Infof("Resolved artifact tag `%s` to digest `%v` before verification", ref.Reference, artifactDescriptor.Digest)
logger.Warn("The resolved digest may not point to the same signed artifact, since tags are mutable")
} else if ref.Reference != artifactDescriptor.Digest.String() {
return ocispec.Descriptor{}, nil, ErrorSignatureRetrievalFailed{Msg: fmt.Sprintf("user input digest %s does not match the resolved digest %s", ref.Reference, artifactDescriptor.Digest.String())}
}
var verificationSucceeded bool
var verificationOutcomes []*VerificationOutcome
var verificationFailedErrorArray = []error{ErrorVerificationFailed{}}
errExceededMaxVerificationLimit := ErrorVerificationFailed{Msg: fmt.Sprintf("signature evaluation stopped. The configured limit of %d signatures to verify per artifact exceeded", verifyOpts.MaxSignatureAttempts)}
numOfSignatureProcessed := 0
var verificationFailedErr error = ErrorVerificationFailed{}
// get signature manifests
logger.Debug("Fetching signature manifests")
err = repo.ListSignatures(ctx, artifactDescriptor, func(signatureManifests []ocispec.Descriptor) error {
@ -367,29 +554,26 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
logger.Error("Got nil outcome. Expecting non-nil outcome on verification failure")
return err
}
if _, ok := outcome.Error.(ErrorUserMetadataVerificationFailed); ok {
verificationFailedErr = outcome.Error
}
outcome.Error = fmt.Errorf("failed to verify signature with digest %v, %w", sigManifestDesc.Digest, outcome.Error)
verificationFailedErrorArray = append(verificationFailedErrorArray, outcome.Error)
continue
}
// at this point, the signature is verified successfully. Add
// it to the verificationOutcomes.
verificationOutcomes = append(verificationOutcomes, outcome)
// at this point, the signature is verified successfully
verificationSucceeded = true
// on success, verificationOutcomes only contains the
// succeeded outcome
verificationOutcomes = []*VerificationOutcome{outcome}
logger.Debugf("Signature verification succeeded for artifact %v with signature digest %v", artifactDescriptor.Digest, sigManifestDesc.Digest)
// early break on success
return errDoneVerification
}
if numOfSignatureProcessed >= verifyOpts.MaxSignatureAttempts {
return errExceededMaxVerificationLimit
}
return nil
})
if err != nil && !errors.Is(err, errDoneVerification) {
if errors.Is(err, errExceededMaxVerificationLimit) {
return ocispec.Descriptor{}, verificationOutcomes, err
@ -403,9 +587,9 @@ func Verify(ctx context.Context, verifier Verifier, repo registry.Repository, ve
}
// Verification Failed
if len(verificationOutcomes) == 0 {
if !verificationSucceeded {
logger.Debugf("Signature verification failed for all the signatures associated with artifact %v", artifactDescriptor.Digest)
return ocispec.Descriptor{}, verificationOutcomes, verificationFailedErr
return ocispec.Descriptor{}, verificationOutcomes, errors.Join(verificationFailedErrorArray...)
}
// Verification Succeeded
@ -437,3 +621,35 @@ func generateAnnotations(signerInfo *signature.SignerInfo, annotations map[strin
annotations[ocispec.AnnotationCreated] = signingTime.Format(time.RFC3339)
return annotations, nil
}
func getDescriptorFunc(ctx context.Context, reader io.Reader, contentMediaType string, userMetadata map[string]string) BlobDescriptorGenerator {
return func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) {
digester := hashAlgo.Digester()
bytes, err := io.Copy(digester.Hash(), reader)
if err != nil {
return ocispec.Descriptor{}, err
}
targetDesc := ocispec.Descriptor{
MediaType: contentMediaType,
Digest: digester.Digest(),
Size: bytes,
}
return addUserMetadataToDescriptor(ctx, targetDesc, userMetadata)
}
}
func validateContentMediaType(contentMediaType string) error {
if contentMediaType != "" {
if _, _, err := mime.ParseMediaType(contentMediaType); err != nil {
return fmt.Errorf("invalid content media-type %q: %v", contentMediaType, err)
}
}
return nil
}
func validateSigMediaType(sigMediaType string) error {
if !(sigMediaType == jws.MediaTypeEnvelope || sigMediaType == cose.MediaTypeEnvelope) {
return fmt.Errorf("invalid signature media-type %q", sigMediaType)
}
return nil
}

View File

@ -1,21 +1,43 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package notation
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"os"
"path/filepath"
"strings"
"testing"
"time"
"oras.land/oras-go/v2/registry/remote"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-core-go/signature/cose"
"github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-go/internal/envelope"
"github.com/notaryproject/notation-go/internal/mock"
"github.com/notaryproject/notation-go/internal/mock/ocilayout"
"github.com/notaryproject/notation-go/plugin"
"github.com/notaryproject/notation-go/registry"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
@ -34,6 +56,7 @@ func TestSignSuccess(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(b *testing.T) {
opts := SignOptions{}
opts.SignatureMediaType = jws.MediaTypeEnvelope
opts.ExpiryDuration = tc.dur
opts.ArtifactReference = mock.SampleArtifactUri
@ -45,11 +68,91 @@ func TestSignSuccess(t *testing.T) {
}
}
func TestSignBlobSuccess(t *testing.T) {
reader := strings.NewReader("some content")
testCases := []struct {
name string
dur time.Duration
mtype string
agent string
pConfig map[string]string
metadata map[string]string
}{
{"expiryInHours", 24 * time.Hour, "video/mp4", "", nil, nil},
{"oneSecondExpiry", 1 * time.Second, "video/mp4", "", nil, nil},
{"zeroExpiry", 0, "video/mp4", "", nil, nil},
{"validContentType", 1 * time.Second, "video/mp4", "", nil, nil},
{"emptyContentType", 1 * time.Second, "video/mp4", "someDummyAgent", map[string]string{"hi": "hello"}, map[string]string{"bye": "tata"}},
}
for _, tc := range testCases {
t.Run(tc.name, func(b *testing.T) {
opts := SignBlobOptions{
SignerSignOptions: SignerSignOptions{
SignatureMediaType: jws.MediaTypeEnvelope,
ExpiryDuration: tc.dur,
PluginConfig: tc.pConfig,
SigningAgent: tc.agent,
},
UserMetadata: expectedMetadata,
ContentMediaType: tc.mtype,
}
_, _, err := SignBlob(context.Background(), &dummySigner{}, reader, opts)
if err != nil {
b.Fatalf("Sign failed with error: %v", err)
}
})
}
}
func TestSignBlobError(t *testing.T) {
reader := strings.NewReader("some content")
testCases := []struct {
name string
signer BlobSigner
dur time.Duration
rdr io.Reader
sigMType string
ctMType string
errMsg string
}{
{"negativeExpiry", &dummySigner{}, -1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration cannot be a negative value"},
{"milliSecExpiry", &dummySigner{}, 1 * time.Millisecond, nil, "video/mp4", jws.MediaTypeEnvelope, "expiry duration supports minimum granularity of seconds"},
{"invalidContentMediaType", &dummySigner{}, 1 * time.Second, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type \"video/mp4/zoping\": mime: unexpected content after media subtype"},
{"emptyContentMediaType", &dummySigner{}, 1 * time.Second, reader, "", jws.MediaTypeEnvelope, "content media-type cannot be empty"},
{"invalidSignatureMediaType", &dummySigner{}, 1 * time.Second, reader, "", "", "content media-type cannot be empty"},
{"nilReader", &dummySigner{}, 1 * time.Second, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"},
{"nilSigner", nil, 1 * time.Second, reader, "video/mp4", jws.MediaTypeEnvelope, "signer cannot be nil"},
{"signerError", &dummySigner{fail: true}, 1 * time.Second, reader, "video/mp4", jws.MediaTypeEnvelope, "expected SignBlob failure"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := SignBlobOptions{
SignerSignOptions: SignerSignOptions{
SignatureMediaType: jws.MediaTypeEnvelope,
ExpiryDuration: tc.dur,
PluginConfig: nil,
},
ContentMediaType: tc.sigMType,
}
_, _, err := SignBlob(context.Background(), tc.signer, tc.rdr, opts)
if err == nil {
t.Fatalf("expected error but didnt found")
}
if err.Error() != tc.errMsg {
t.Fatalf("expected err message to be '%s' but found '%s'", tc.errMsg, err.Error())
}
})
}
}
func TestSignSuccessWithUserMetadata(t *testing.T) {
repo := mock.NewRepository()
opts := SignOptions{}
opts.ArtifactReference = mock.SampleArtifactUri
opts.UserMetadata = expectedMetadata
opts.SignatureMediaType = jws.MediaTypeEnvelope
_, err := Sign(context.Background(), &verifyMetadataSigner{}, repo, opts)
if err != nil {
@ -57,6 +160,71 @@ func TestSignSuccessWithUserMetadata(t *testing.T) {
}
}
func TestSignWithDanglingReferrersIndex(t *testing.T) {
repo := mock.NewRepository()
repo.PushSignatureError = &remote.ReferrersError{
Op: "DeleteReferrersIndex",
Err: errors.New("error"),
}
opts := SignOptions{}
opts.ArtifactReference = mock.SampleArtifactUri
opts.SignatureMediaType = jws.MediaTypeEnvelope
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
if err == nil {
t.Fatalf("no error occurred, expected error")
}
}
func TestSignWithNilRepo(t *testing.T) {
opts := SignOptions{}
opts.ArtifactReference = mock.SampleArtifactUri
opts.SignatureMediaType = jws.MediaTypeEnvelope
_, err := Sign(context.Background(), &dummySigner{}, nil, opts)
if err == nil {
t.Fatalf("no error occurred, expected error: repo cannot be nil")
}
}
func TestSignResolveFailed(t *testing.T) {
repo := mock.NewRepository()
repo.ResolveError = errors.New("resolve error")
opts := SignOptions{}
opts.ArtifactReference = mock.SampleArtifactUri
opts.SignatureMediaType = jws.MediaTypeEnvelope
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
if err == nil {
t.Fatalf("no error occurred, expected resolve error")
}
}
func TestSignArtifactRefIsTag(t *testing.T) {
repo := mock.NewRepository()
opts := SignOptions{}
opts.ArtifactReference = "registry.acme-rockets.io/software/net-monitor:v1"
opts.SignatureMediaType = jws.MediaTypeEnvelope
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
if err != nil {
t.Fatalf("expect no error, got %s", err)
}
}
func TestSignWithPushSignatureError(t *testing.T) {
repo := mock.NewRepository()
repo.PushSignatureError = errors.New("error")
opts := SignOptions{}
opts.ArtifactReference = mock.SampleArtifactUri
opts.SignatureMediaType = jws.MediaTypeEnvelope
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
if err == nil {
t.Fatalf("no error occurred, expected error: failed to delete dangling referrers index")
}
}
func TestSignWithInvalidExpiry(t *testing.T) {
repo := mock.NewRepository()
testCases := []struct {
@ -90,7 +258,14 @@ func TestSignWithInvalidUserMetadata(t *testing.T) {
}
for _, tc := range testCases {
t.Run(tc.name, func(b *testing.T) {
_, err := Sign(context.Background(), &dummySigner{}, repo, SignOptions{UserMetadata: tc.metadata})
opts := SignOptions{
UserMetadata: tc.metadata,
SignerSignOptions: SignerSignOptions{
SignatureMediaType: jws.MediaTypeEnvelope,
},
}
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
if err == nil {
b.Fatalf("Expected error but not found")
}
@ -98,10 +273,41 @@ func TestSignWithInvalidUserMetadata(t *testing.T) {
}
}
func TestRegistryResolveError(t *testing.T) {
policyDocument := dummyPolicyDocument()
func TestSignOptsMissingSignatureMediaType(t *testing.T) {
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
opts := SignOptions{
SignerSignOptions: SignerSignOptions{
SignatureMediaType: "",
},
ArtifactReference: mock.SampleArtifactUri,
}
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
if err == nil {
t.Fatalf("expected error but not found")
}
}
func TestSignOptsUnknownMediaType(t *testing.T) {
repo := mock.NewRepository()
opts := SignOptions{
SignerSignOptions: SignerSignOptions{
SignatureMediaType: "unknown",
},
ArtifactReference: mock.SampleArtifactUri,
}
_, err := Sign(context.Background(), &dummySigner{}, repo, opts)
if err == nil {
t.Fatalf("expected error but not found")
}
}
func TestRegistryResolveError(t *testing.T) {
repo := mock.NewRepository()
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
errorMessage := "network error"
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
@ -117,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}
@ -133,11 +339,11 @@ 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"
errorMessage := "invalid reference: invalid repository \"UPPERCASE/test\""
expectedErr := ErrorSignatureRetrievalFailed{Msg: errorMessage}
// mock the repository
@ -149,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}
@ -169,11 +375,14 @@ func TestSignDigestNotMatchResolve(t *testing.T) {
repo := mock.NewRepository()
repo.MissMatchDigest = true
signOpts := SignOptions{
SignerSignOptions: SignerSignOptions{
SignatureMediaType: jws.MediaTypeEnvelope,
},
ArtifactReference: mock.SampleArtifactUri,
}
errorMessage := fmt.Sprintf("user input digest %s does not match the resolved digest %s", mock.SampleDigest, mock.ZeroDigest)
expectedErr := fmt.Errorf(errorMessage)
expectedErr := errors.New(errorMessage)
_, err := Sign(context.Background(), &dummySigner{}, repo, signOpts)
if err == nil || err.Error() != errorMessage {
@ -182,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)
@ -195,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}
@ -212,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}
@ -229,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
@ -258,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
@ -274,30 +498,116 @@ func TestExceededMaxSignatureAttempts(t *testing.T) {
}
func TestVerifyFailed(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict}
expectedErr := ErrorVerificationFailed{}
t.Run("verification error", func(t *testing.T) {
policyDocument := dummyPolicyDocument()
repo := mock.NewRepository()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, true, *trustpolicy.LevelStrict, false}
expectedErr := ErrorVerificationFailed{}
// mock the repository
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), &verifier, repo, opts)
// mock the repository
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), &verifier, repo, opts)
if err == nil || !errors.Is(err, expectedErr) {
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
if err == nil || !errors.Is(err, expectedErr) {
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
}
})
t.Run("verifier is nil", func(t *testing.T) {
repo := mock.NewRepository()
expectedErr := errors.New("verifier cannot be nil")
// mock the repository
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), nil, repo, opts)
if err == nil || err.Error() != expectedErr.Error() {
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
}
})
t.Run("repo is nil", func(t *testing.T) {
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
expectedErr := errors.New("repo cannot be nil")
// mock the repository
opts := VerifyOptions{ArtifactReference: mock.SampleArtifactUri, MaxSignatureAttempts: 50}
_, _, err := Verify(context.Background(), &verifier, nil, opts)
if err == nil || err.Error() != expectedErr.Error() {
t.Fatalf("VerificationFailed expected: %v got: %v", expectedErr, err)
}
})
}
func TestVerifyBlobError(t *testing.T) {
reader := strings.NewReader("some content")
sig := []byte("signature")
testCases := []struct {
name string
verifier BlobVerifier
sig []byte
rdr io.Reader
ctMType string
sigMType string
errMsg string
}{
{"nilVerifier", nil, sig, reader, "video/mp4", jws.MediaTypeEnvelope, "blobVerifier cannot be nil"},
{"verifierError", &dummyVerifier{FailVerify: true}, sig, reader, "video/mp4", jws.MediaTypeEnvelope, "failed verify"},
{"nilSignature", &dummyVerifier{}, nil, reader, "video/mp4", jws.MediaTypeEnvelope, "signature cannot be nil or empty"},
{"emptySignature", &dummyVerifier{}, []byte{}, reader, "video/mp4", jws.MediaTypeEnvelope, "signature cannot be nil or empty"},
{"nilReader", &dummyVerifier{}, sig, nil, "video/mp4", jws.MediaTypeEnvelope, "blobReader cannot be nil"},
{"invalidContentType", &dummyVerifier{}, sig, reader, "video/mp4/zoping", jws.MediaTypeEnvelope, "invalid content media-type \"video/mp4/zoping\": mime: unexpected content after media subtype"},
{"invalidSigType", &dummyVerifier{}, sig, reader, "video/mp4", "hola!", "invalid signature media-type \"hola!\""},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
opts := VerifyBlobOptions{
BlobVerifierVerifyOptions: BlobVerifierVerifyOptions{
SignatureMediaType: tc.sigMType,
UserMetadata: nil,
TrustPolicyName: "",
},
ContentMediaType: tc.ctMType,
}
_, _, err := VerifyBlob(context.Background(), tc.verifier, tc.rdr, tc.sig, opts)
if err == nil {
t.Fatalf("expected error but didnt found")
}
if err.Error() != tc.errMsg {
t.Fatalf("expected err message to be '%s' but found '%s'", tc.errMsg, err.Error())
}
})
}
}
func dummyPolicyDocument() (policyDoc trustpolicy.Document) {
policyDoc = trustpolicy.Document{
func TestVerifyBlobValid(t *testing.T) {
opts := VerifyBlobOptions{
BlobVerifierVerifyOptions: BlobVerifierVerifyOptions{
SignatureMediaType: jws.MediaTypeEnvelope,
UserMetadata: nil,
TrustPolicyName: "",
},
}
_, _, err := VerifyBlob(context.Background(), &dummyVerifier{}, strings.NewReader("some content"), []byte("signature"), opts)
if err != nil {
t.Fatalf("expected nil error, but got: %v", err)
}
}
func dummyPolicyDocument() (policyDoc trustpolicy.OCIDocument) {
policyDoc = trustpolicy.OCIDocument{
Version: "1.0",
TrustPolicies: []trustpolicy.TrustPolicy{dummyPolicyStatement()},
TrustPolicies: []trustpolicy.OCITrustPolicy{dummyPolicyStatement()},
}
return
}
func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
policyStatement = trustpolicy.TrustPolicy{
func dummyPolicyStatement() (policyStatement trustpolicy.OCITrustPolicy) {
policyStatement = trustpolicy.OCITrustPolicy{
Name: "test-statement-name",
RegistryScopes: []string{"registry.acme-rockets.io/software/net-monitor"},
SignatureVerification: trustpolicy.SignatureVerification{VerificationLevel: "strict"},
@ -307,9 +617,28 @@ func dummyPolicyStatement() (policyStatement trustpolicy.TrustPolicy) {
return
}
type dummySigner struct{}
type dummySigner struct {
fail bool
}
func (s *dummySigner) Sign(_ context.Context, _ ocispec.Descriptor, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
return []byte("ABC"), &signature.SignerInfo{
SignedAttributes: signature.SignedAttributes{
SigningTime: time.Now(),
},
}, nil
}
func (s *dummySigner) SignBlob(_ context.Context, descGenFunc BlobDescriptorGenerator, _ SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
if s.fail {
return nil, nil, errors.New("expected SignBlob failure")
}
_, err := descGenFunc(digest.SHA384)
if err != nil {
return nil, nil, err
}
func (s *dummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
return []byte("ABC"), &signature.SignerInfo{
SignedAttributes: signature.SignedAttributes{
SigningTime: time.Now(),
@ -319,7 +648,7 @@ func (s *dummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts Si
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")
@ -333,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,
@ -350,8 +680,30 @@ 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 (
ociLayoutPath = filepath.FromSlash("./internal/testdata/oci-layout")
reference = "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
artifactReference = "local/oci-layout@sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
signaturePath = filepath.FromSlash("./internal/testdata/cose_signature.sig")
@ -359,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
@ -375,37 +727,114 @@ func (s *ociDummySigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
return sigBlob, &content.SignerInfo, nil
}
func TestSignLocalContent(t *testing.T) {
repo, err := registry.NewOCIRepository(ociLayoutPath, registry.RepositoryOptions{})
func TestLocalContent(t *testing.T) {
// create a temp OCI layout
ociLayoutTestDataPath, err := filepath.Abs(filepath.Join("internal", "testdata", "oci-layout"))
if err != nil {
t.Fatalf("failed to get oci layout path: %v", err)
}
newOCILayoutPath := t.TempDir()
if err := ocilayout.Copy(ociLayoutTestDataPath, newOCILayoutPath, "v2"); err != nil {
t.Fatalf("failed to create temp oci layout: %v", err)
}
repo, err := registry.NewOCIRepository(newOCILayoutPath, registry.RepositoryOptions{})
if err != nil {
t.Fatal(err)
}
signOpts := SignOptions{
SignerSignOptions: SignerSignOptions{
SignatureMediaType: cose.MediaTypeEnvelope,
},
ArtifactReference: reference,
}
_, err = Sign(context.Background(), &ociDummySigner{}, repo, signOpts)
if err != nil {
t.Fatalf("failed to Sign: %v", err)
}
t.Run("sign the local content", func(t *testing.T) {
// sign the artifact
signOpts := SignOptions{
SignerSignOptions: SignerSignOptions{
SignatureMediaType: cose.MediaTypeEnvelope,
},
ArtifactReference: reference,
}
_, err = Sign(context.Background(), &ociDummySigner{}, repo, signOpts)
if err != nil {
t.Fatalf("failed to Sign: %v", err)
}
})
t.Run("verify local content", func(t *testing.T) {
// verify the artifact
verifyOpts := VerifyOptions{
ArtifactReference: artifactReference,
MaxSignatureAttempts: math.MaxInt64,
}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict, false}
// verify signatures inside the OCI layout folder
_, _, err = Verify(context.Background(), &verifier, repo, verifyOpts)
if err != nil {
t.Fatalf("failed to verify local content: %v", err)
}
})
}
func TestVerifyLocalContent(t *testing.T) {
repo, err := registry.NewOCIRepository(ociLayoutPath, registry.RepositoryOptions{})
if err != nil {
t.Fatalf("failed to create oci.Store as registry.Repository: %v", err)
}
verifyOpts := VerifyOptions{
ArtifactReference: artifactReference,
MaxSignatureAttempts: math.MaxInt64,
}
policyDocument := dummyPolicyDocument()
verifier := dummyVerifier{&policyDocument, mock.PluginManager{}, false, *trustpolicy.LevelStrict}
// verify signatures inside the OCI layout folder
_, _, err = Verify(context.Background(), &verifier, repo, verifyOpts)
if err != nil {
t.Fatalf("failed to verify local content: %v", err)
}
func TestUserMetadata(t *testing.T) {
t.Run("EnvelopeContent is nil", func(t *testing.T) {
outcome := &VerificationOutcome{}
_, err := outcome.UserMetadata()
if err == nil {
t.Fatal("expected an error, got nil")
}
if err.Error() != "unable to find envelope content for verification outcome" {
t.Fatalf("expected error message 'unable to find envelope content for verification outcome', got '%s'", err.Error())
}
})
t.Run("EnvelopeContent is valid", func(t *testing.T) {
payload := envelope.Payload{
TargetArtifact: ocispec.Descriptor{
Annotations: map[string]string{
"key": "value",
},
},
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("unexpected error marshaling payload: %v", err)
}
outcome := &VerificationOutcome{
EnvelopeContent: &signature.EnvelopeContent{
Payload: signature.Payload{
Content: payloadBytes,
},
},
}
metadata, err := outcome.UserMetadata()
if err != nil {
t.Fatalf("unexpected error getting user metadata: %v", err)
}
if len(metadata) != 1 || metadata["key"] != "value" {
t.Fatalf("expected metadata map[key]=value, got %v", metadata)
}
})
t.Run("Annotation is nil", func(t *testing.T) {
payload := envelope.Payload{
TargetArtifact: ocispec.Descriptor{},
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
t.Fatalf("unexpected error marshaling payload: %v", err)
}
outcome := &VerificationOutcome{
EnvelopeContent: &signature.EnvelopeContent{
Payload: signature.Payload{
Content: payloadBytes,
},
},
}
metadata, err := outcome.UserMetadata()
if err != nil {
t.Fatalf("unexpected error getting user metadata: %v", err)
}
if len(metadata) != 0 {
t.Fatalf("expected empty metadata, got %v", metadata)
}
})
}

96
plugin/errors.go Normal file
View File

@ -0,0 +1,96 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import "errors"
// ErrNotCompliant is returned by plugin methods when the response is not
// compliant.
var ErrNotCompliant = errors.New("plugin not compliant")
// ErrNotRegularFile is returned when the plugin file is not an regular file.
var ErrNotRegularFile = errors.New("plugin executable file is not a regular file")
// PluginDowngradeError is returned when installing a plugin with version
// lower than the exisiting plugin version.
type PluginDowngradeError struct {
Msg string
}
// Error returns the error message.
func (e PluginDowngradeError) Error() string {
if e.Msg != "" {
return e.Msg
}
return "installing plugin with version lower than the existing plugin version"
}
// InstallEqualVersionError is returned when installing a plugin with version
// equal to the exisiting plugin version.
type InstallEqualVersionError struct {
Msg string
}
// Error returns the error message.
func (e InstallEqualVersionError) Error() string {
if e.Msg != "" {
return e.Msg
}
return "installing plugin with version equal to the existing plugin version"
}
// PluginMalformedError is used when there is an issue with plugin and
// should be fixed by plugin developers.
type PluginMalformedError struct {
Msg string
InnerError error
}
// Error returns the error message.
func (e PluginMalformedError) Error() string {
if e.Msg != "" {
return e.Msg
}
return e.InnerError.Error()
}
// Unwrap returns the inner error.
func (e PluginMalformedError) Unwrap() error {
return e.InnerError
}
// PluginDirectoryWalkError is used when there is an issue with plugins directory
// and should suggest user to check the permission of plugin directory.
type PluginDirectoryWalkError error
// PluginExecutableFileError is used when there is an issue with plugin
// executable file and should suggest user to check the existence, permission
// and platform/arch compatibility of plugin.
type PluginExecutableFileError struct {
Msg string
InnerError error
}
// Error returns the error message.
func (e PluginExecutableFileError) Error() string {
if e.Msg != "" {
return e.Msg
}
return e.InnerError.Error()
}
// Unwrap returns the inner error.
func (e PluginExecutableFileError) Unwrap() error {
return e.InnerError
}

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
@ -10,16 +23,16 @@ import (
"testing"
"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/plugin/proto"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
)
var exampleMetadata = proto.GetMetadataResponse{
var exampleMetadata = plugin.GetMetadataResponse{
Name: "foo",
Description: "friendly",
Version: "1",
URL: "example.com",
SupportedContractVersions: []string{"1.0"},
Capabilities: []proto.Capability{"cap"}}
Capabilities: []plugin.Capability{"cap"}}
func preparePlugin(t *testing.T) string {
root := t.TempDir()
@ -74,11 +87,11 @@ func TestIntegration(t *testing.T) {
}
// validate and create
plugin, err := mgr.Get(context.Background(), "foo")
pl, err := mgr.Get(context.Background(), "foo")
if err != nil {
t.Fatal(err)
}
metadata, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
metadata, err := pl.GetMetadata(context.Background(), &plugin.GetMetadataRequest{})
if err != nil {
t.Fatal(err)
}

View File

@ -1,28 +1,41 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path"
"path/filepath"
"github.com/notaryproject/notation-go/dir"
"github.com/notaryproject/notation-go/internal/file"
"github.com/notaryproject/notation-go/internal/semver"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
)
// ErrNotCompliant is returned by plugin methods when the response is not
// compliant.
var ErrNotCompliant = errors.New("plugin not compliant")
// ErrNotRegularFile is returned when the plugin file is not an regular file.
var ErrNotRegularFile = errors.New("not regular file")
// Manager manages plugins installed on the system.
type Manager interface {
Get(ctx context.Context, name string) (Plugin, error)
Get(ctx context.Context, name string) (plugin.Plugin, error)
List(ctx context.Context) ([]string, error)
}
// CLIManager implements Manager
// CLIManager implements [Manager]
type CLIManager struct {
pluginFS dir.SysFS
}
@ -35,7 +48,7 @@ func NewCLIManager(pluginFS dir.SysFS) *CLIManager {
// Get returns a plugin on the system by its name.
//
// If the plugin is not found, the error is of type os.ErrNotExist.
func (m *CLIManager) Get(ctx context.Context, name string) (Plugin, error) {
func (m *CLIManager) Get(ctx context.Context, name string) (plugin.Plugin, error) {
pluginPath := path.Join(name, binName(name))
path, err := m.pluginFS.SysPath(pluginPath)
if err != nil {
@ -49,8 +62,11 @@ func (m *CLIManager) Get(ctx context.Context, name string) (Plugin, error) {
// List produces a list of the plugin names on the system.
func (m *CLIManager) List(ctx context.Context) ([]string, error) {
var plugins []string
fs.WalkDir(m.pluginFS, ".", func(dir string, d fs.DirEntry, err error) error {
if err := fs.WalkDir(m.pluginFS, ".", func(dir string, d fs.DirEntry, err error) error {
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
if dir == "." {
@ -66,6 +82,215 @@ func (m *CLIManager) List(ctx context.Context) ([]string, error) {
// add plugin name
plugins = append(plugins, d.Name())
return fs.SkipDir
})
}); err != nil {
return nil, PluginDirectoryWalkError(fmt.Errorf("failed to list plugin: %w", err))
}
return plugins, nil
}
// CLIInstallOptions provides user customized options for plugin installation
type CLIInstallOptions struct {
// PluginPath can be path of:
//
// 1. A directory which contains plugin related files. Sub-directories are
// ignored. It MUST contain one and only one valid plugin executable file
// following spec: https://github.com/notaryproject/specifications/blob/v1.0.0/specs/plugin-extensibility.md#installation
// It may contain extra lib files and LICENSE files.
// On success, these files will be installed as well.
//
// 2. A single plugin executable file following the spec.
PluginPath string
// Overwrite is a boolean flag. When set, always install the new plugin.
Overwrite bool
}
// Install installs a plugin to the system. It returns existing
// plugin metadata, new plugin metadata, and error. It returns nil error
// if and only if the installation succeeded.
//
// If plugin does not exist, directly install the new plugin.
//
// If plugin already exists:
//
// If overwrite is not set, then the new plugin
// version MUST be higher than the existing plugin version.
//
// If overwrite is set, version check is skipped. If existing
// plugin is malfunctioning, it will be overwritten.
func (m *CLIManager) Install(ctx context.Context, installOpts CLIInstallOptions) (*plugin.GetMetadataResponse, *plugin.GetMetadataResponse, error) {
// initialization
logger := log.GetLogger(ctx)
overwrite := installOpts.Overwrite
if installOpts.PluginPath == "" {
return nil, nil, errors.New("plugin source path cannot be empty")
}
logger.Debugf("Installing plugin from path %s", installOpts.PluginPath)
var installFromNonDir bool
pluginExecutableFile, pluginName, err := parsePluginFromDir(ctx, installOpts.PluginPath)
if err != nil {
if !errors.Is(err, file.ErrNotDirectory) {
return nil, nil, fmt.Errorf("failed to read plugin from input directory: %w", err)
}
// input is not a dir, check if it's a single plugin executable file
installFromNonDir = true
pluginExecutableFile = installOpts.PluginPath
pluginExecutableFileName := filepath.Base(pluginExecutableFile)
pluginName, err = parsePluginName(pluginExecutableFileName)
if err != nil {
return nil, nil, fmt.Errorf("failed to read plugin name from input file %s: %w", pluginExecutableFileName, err)
}
isExec, err := isExecutableFile(pluginExecutableFile)
if err != nil {
return nil, nil, fmt.Errorf("failed to check if input file %s is executable: %w", pluginExecutableFileName, err)
}
if !isExec {
return nil, nil, fmt.Errorf("input file %s is not executable", pluginExecutableFileName)
}
}
// validate and get new plugin metadata
newPlugin, err := NewCLIPlugin(ctx, pluginName, pluginExecutableFile)
if err != nil {
return nil, nil, err
}
newPluginMetadata, err := newPlugin.GetMetadata(ctx, &plugin.GetMetadataRequest{})
if err != nil {
return nil, nil, fmt.Errorf("failed to get metadata of new plugin: %w", err)
}
// check plugin existence and get existing plugin metadata
var existingPluginMetadata *plugin.GetMetadataResponse
existingPlugin, err := m.Get(ctx, pluginName)
if err != nil {
// fail only if overwrite is not set
if !errors.Is(err, os.ErrNotExist) && !overwrite {
return nil, nil, fmt.Errorf("failed to check plugin existence: %w", err)
}
} else { // plugin already exists
existingPluginMetadata, err = existingPlugin.GetMetadata(ctx, &plugin.GetMetadataRequest{})
if err != nil && !overwrite { // fail only if overwrite is not set
return nil, nil, fmt.Errorf("failed to get metadata of existing plugin: %w", err)
}
// existing plugin is valid, and overwrite is not set, check version
if !overwrite {
comp, err := semver.ComparePluginVersion(newPluginMetadata.Version, existingPluginMetadata.Version)
if err != nil {
return nil, nil, fmt.Errorf("failed to compare plugin versions: %w", err)
}
switch {
case comp < 0:
return nil, nil, PluginDowngradeError{Msg: fmt.Sprintf("failed to install plugin %s. The installing plugin version %s is lower than the existing plugin version %s", pluginName, newPluginMetadata.Version, existingPluginMetadata.Version)}
case comp == 0:
return nil, nil, InstallEqualVersionError{Msg: fmt.Sprintf("plugin %s with version %s already exists", pluginName, existingPluginMetadata.Version)}
}
}
}
// clean up before installation, this guarantees idempotent for install
if err := m.Uninstall(ctx, pluginName); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return nil, nil, fmt.Errorf("failed to clean up plugin %s before installation: %w", pluginName, err)
}
}
// core process
pluginDirPath, err := m.pluginFS.SysPath(pluginName)
if err != nil {
return nil, nil, fmt.Errorf("failed to get the system path of plugin %s: %w", pluginName, err)
}
if installFromNonDir {
if err := file.CopyToDir(pluginExecutableFile, pluginDirPath); err != nil {
return nil, nil, fmt.Errorf("failed to copy plugin executable file from %s to %s: %w", pluginExecutableFile, pluginDirPath, err)
}
} else {
if err := file.CopyDirToDir(installOpts.PluginPath, pluginDirPath); err != nil {
return nil, nil, fmt.Errorf("failed to copy plugin files from %s to %s: %w", installOpts.PluginPath, pluginDirPath, err)
}
}
return existingPluginMetadata, newPluginMetadata, nil
}
// Uninstall uninstalls a plugin on the system by its name.
// If the plugin dir does not exist, os.ErrNotExist is returned.
func (m *CLIManager) Uninstall(ctx context.Context, name string) error {
pluginDirPath, err := m.pluginFS.SysPath(name)
if err != nil {
return err
}
if _, err := os.Stat(pluginDirPath); err != nil {
return err
}
return os.RemoveAll(pluginDirPath)
}
// parsePluginFromDir checks if a dir is a valid plugin dir which contains
// one and only one plugin executable file candidate.
// The dir may contain extra lib files and LICENSE files.
// Sub-directories are ignored.
//
// On success, the plugin executable file path, plugin name and
// nil error are returned.
func parsePluginFromDir(ctx context.Context, path string) (string, string, error) {
// sanity check
fi, err := os.Stat(path)
if err != nil {
return "", "", err
}
if !fi.Mode().IsDir() {
return "", "", file.ErrNotDirectory
}
logger := log.GetLogger(ctx)
// walk the path
var pluginExecutableFile, pluginName, candidatePluginName string
var foundPluginExecutableFile bool
var filesWithValidNameFormat []string
if err := filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// skip sub-directories
if d.IsDir() && d.Name() != filepath.Base(path) {
return fs.SkipDir
}
info, err := d.Info()
if err != nil {
return err
}
// only take regular files
if info.Mode().IsRegular() {
if candidatePluginName, err = parsePluginName(d.Name()); err != nil {
// file name does not follow the notation-{plugin-name} format,
// continue
return nil
}
filesWithValidNameFormat = append(filesWithValidNameFormat, p)
isExec, err := isExecutableFile(p)
if err != nil {
return err
}
if !isExec {
return nil
}
if foundPluginExecutableFile {
return errors.New("found more than one plugin executable files")
}
foundPluginExecutableFile = true
pluginExecutableFile = p
pluginName = candidatePluginName
}
return nil
}); err != nil {
return "", "", err
}
if !foundPluginExecutableFile {
// if no executable file was found, but there's one and only one
// potential candidate, try install the candidate
if len(filesWithValidNameFormat) == 1 {
candidate := filesWithValidNameFormat[0]
if err := setExecutable(candidate); err != nil {
return "", "", fmt.Errorf("no plugin executable file was found: %w", err)
}
logger.Warnf("Found candidate plugin executable file %q without executable permission. Setting user executable bit and trying to install.", filepath.Base(candidate))
return candidate, candidatePluginName, nil
}
return "", "", errors.New("no plugin executable file was found")
}
return pluginExecutableFile, pluginName, nil
}

View File

@ -1,10 +1,26 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"context"
"encoding/json"
"io/fs"
"os"
"path/filepath"
"reflect"
"runtime"
"testing"
"testing/fstest"
@ -22,8 +38,50 @@ func (t testCommander) Output(ctx context.Context, path string, command proto.Co
return t.stdout, t.stderr, t.err
}
type testInstallCommander struct {
existedPluginFilePath string
existedPluginStdout []byte
existedPluginStderr []byte
existedPluginErr error
newPluginFilePath string
newPluginStdout []byte
newPluginStderr []byte
newPluginErr error
err error
}
func (t testInstallCommander) Output(ctx context.Context, path string, command proto.Command, req []byte) ([]byte, []byte, error) {
if path == t.existedPluginFilePath {
return t.existedPluginStdout, t.existedPluginStderr, t.existedPluginErr
}
if path == t.newPluginFilePath {
return t.newPluginStdout, t.newPluginStderr, t.newPluginErr
}
return nil, nil, t.err
}
var validMetadata = proto.GetMetadataResponse{
Name: "foo", Description: "friendly", Version: "1", URL: "example.com",
Name: "foo", Description: "friendly", Version: "1.0.0", URL: "example.com",
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
}
var validMetadataHigherVersion = proto.GetMetadataResponse{
Name: "foo", Description: "friendly", Version: "1.1.0", URL: "example.com",
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
}
var validMetadataLowerVersion = proto.GetMetadataResponse{
Name: "foo", Description: "friendly", Version: "0.1.0", URL: "example.com",
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
}
var validMetadataBar = proto.GetMetadataResponse{
Name: "bar", Description: "friendly", Version: "1.0.0", URL: "example.com",
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
}
var validMetadataBarExample = proto.GetMetadataResponse{
Name: "bar.example.plugin", Description: "friendly", Version: "1.0.0", URL: "example.com",
SupportedContractVersions: []string{"1.0"}, Capabilities: []proto.Capability{proto.CapabilitySignatureGenerator},
}
@ -38,6 +96,9 @@ var invalidContractVersionMetadata = proto.GetMetadataResponse{
}
func TestManager_Get(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
executor = testCommander{stdout: metadataJSON(validMetadata)}
mgr := NewCLIManager(mockfs.NewSysFSWithRootMock(fstest.MapFS{}, "./testdata/plugins"))
_, err := mgr.Get(context.Background(), "foo")
@ -74,6 +135,533 @@ func TestManager_List(t *testing.T) {
})
}
func TestManager_Install(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
newPluginFilePath := "testdata/foo/notation-foo"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
t.Fatal(err)
}
mgr := NewCLIManager(mockfs.NewSysFSWithRootMock(fstest.MapFS{}, "testdata/plugins"))
t.Run("success install with higher version", func(t *testing.T) {
executor = testInstallCommander{
existedPluginFilePath: existedPluginFilePath,
newPluginFilePath: newPluginFilePath,
existedPluginStdout: metadataJSON(validMetadata),
newPluginStdout: metadataJSON(validMetadataHigherVersion),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
if err != nil {
t.Fatalf("expecting error to be nil, but got %v", err)
}
if existingPluginMetadata.Version != validMetadata.Version {
t.Fatalf("existing plugin version mismatch, existing plugin version: %s, but got: %s", validMetadata.Version, existingPluginMetadata.Version)
}
if newPluginMetadata.Version != validMetadataHigherVersion.Version {
t.Fatalf("new plugin version mismatch, new plugin version: %s, but got: %s", validMetadataHigherVersion.Version, newPluginMetadata.Version)
}
})
t.Run("success install with lower version and overwrite", func(t *testing.T) {
executor = testInstallCommander{
existedPluginFilePath: existedPluginFilePath,
newPluginFilePath: newPluginFilePath,
existedPluginStdout: metadataJSON(validMetadata),
newPluginStdout: metadataJSON(validMetadataLowerVersion),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
Overwrite: true,
}
if _, _, err := mgr.Install(context.Background(), installOpts); err != nil {
t.Fatalf("expecting error to be nil, but got %v", err)
}
})
t.Run("success install without existing plugin", func(t *testing.T) {
newPluginFilePath := "testdata/bar/notation-bar"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
t.Fatal(err)
}
executor = testInstallCommander{
newPluginFilePath: newPluginFilePath,
newPluginStdout: metadataJSON(validMetadataBar),
}
defer mgr.Uninstall(context.Background(), "bar")
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
if err != nil {
t.Fatalf("expecting error to be nil, but got %v", err)
}
if existingPluginMetadata != nil {
t.Fatalf("expecting existingPluginMetadata to be nil, but got %v", existingPluginMetadata)
}
if newPluginMetadata.Version != validMetadataBar.Version {
t.Fatalf("new plugin version mismatch, new plugin version: %s, but got: %s", validMetadataBar.Version, newPluginMetadata.Version)
}
})
t.Run("success install with file extension", func(t *testing.T) {
newPluginFilePath := "testdata/bar/notation-bar.example.plugin"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
t.Fatal(err)
}
executor = testInstallCommander{
newPluginFilePath: newPluginFilePath,
newPluginStdout: metadataJSON(validMetadataBarExample),
}
defer mgr.Uninstall(context.Background(), "bar.example.plugin")
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
if err != nil {
t.Fatalf("expecting error to be nil, but got %v", err)
}
if existingPluginMetadata != nil {
t.Fatalf("expecting existingPluginMetadata to be nil, but got %v", existingPluginMetadata)
}
if newPluginMetadata.Version != validMetadataBar.Version {
t.Fatalf("new plugin version mismatch, new plugin version: %s, but got: %s", validMetadataBar.Version, newPluginMetadata.Version)
}
})
t.Run("fail to install due to equal version", func(t *testing.T) {
executor = testInstallCommander{
existedPluginFilePath: existedPluginFilePath,
newPluginFilePath: newPluginFilePath,
existedPluginStdout: metadataJSON(validMetadata),
newPluginStdout: metadataJSON(validMetadata),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
expectedErrorMsg := "plugin foo with version 1.0.0 already exists"
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
}
})
t.Run("fail to install due to lower version", func(t *testing.T) {
executor = testInstallCommander{
existedPluginFilePath: existedPluginFilePath,
newPluginFilePath: newPluginFilePath,
existedPluginStdout: metadataJSON(validMetadata),
newPluginStdout: metadataJSON(validMetadataLowerVersion),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
expectedErrorMsg := "failed to install plugin foo. The installing plugin version 0.1.0 is lower than the existing plugin version 1.0.0"
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
}
})
t.Run("fail to install due to wrong plugin executable file name format", func(t *testing.T) {
newPluginFilePath := "testdata/bar/bar"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
t.Fatal(err)
}
executor = testInstallCommander{
newPluginFilePath: newPluginFilePath,
newPluginStdout: metadataJSON(validMetadataBar),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
expectedErrorMsg := "failed to read plugin name from input file bar: invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got bar"
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
}
})
t.Run("fail to install due to plugin executable file name missing plugin name", func(t *testing.T) {
newPluginFilePath := "testdata/bar/notation-"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
t.Fatal(err)
}
executor = testInstallCommander{
newPluginFilePath: newPluginFilePath,
newPluginStdout: metadataJSON(validMetadataBar),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
expectedErrorMsg := "failed to read plugin name from input file notation-: invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got notation-"
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
}
})
t.Run("fail to install due to wrong plugin file permission", func(t *testing.T) {
newPluginFilePath := "testdata/bar/notation-bar"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0600); err != nil {
t.Fatal(err)
}
executor = testInstallCommander{
newPluginFilePath: newPluginFilePath,
newPluginStdout: metadataJSON(validMetadataBar),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
expectedErrorMsg := "input file notation-bar is not executable"
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
}
})
t.Run("fail to install due to new plugin executable file does not exist", func(t *testing.T) {
newPluginFilePath := "testdata/bar/notation-bar"
executor = testInstallCommander{
newPluginFilePath: newPluginFilePath,
newPluginStdout: metadataJSON(validMetadataBar),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
expectedErrorMsg := "failed to read plugin from input directory: stat testdata/bar/notation-bar: no such file or directory"
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
}
})
t.Run("fail to install due to invalid new plugin metadata", func(t *testing.T) {
newPluginFilePath := "testdata/bar/notation-bar"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
t.Fatal(err)
}
executor = testInstallCommander{
newPluginFilePath: newPluginFilePath,
newPluginStdout: metadataJSON(invalidMetadataName),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
expectedErrorMsg := "failed to get metadata of new plugin: plugin executable file name must be \"notation-foobar\" instead of \"notation-bar\""
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
}
})
t.Run("fail to install due to invalid existing plugin metadata", func(t *testing.T) {
executor = testInstallCommander{
existedPluginFilePath: existedPluginFilePath,
newPluginFilePath: newPluginFilePath,
existedPluginStdout: metadataJSON(validMetadataBar),
newPluginStdout: metadataJSON(validMetadata),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
}
expectedErrorMsg := "failed to get metadata of existing plugin: plugin executable file name must be \"notation-bar\" instead of \"notation-foo\""
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
}
})
t.Run("success to install with overwrite and invalid existing plugin metadata", func(t *testing.T) {
executor = testInstallCommander{
existedPluginFilePath: existedPluginFilePath,
newPluginFilePath: newPluginFilePath,
existedPluginStdout: metadataJSON(validMetadataBar),
newPluginStdout: metadataJSON(validMetadata),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginFilePath,
Overwrite: true,
}
_, _, err := mgr.Install(context.Background(), installOpts)
if err != nil {
t.Fatalf("expecting error to be nil, but got %v", err)
}
})
t.Run("success to install from plugin dir", func(t *testing.T) {
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
newPluginFilePath := "testdata/foo/notation-foo"
newPluginLibPath := "testdata/foo/notation-libfoo"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
t.Fatal(err)
}
if err := createFileAndChmod(newPluginLibPath, 0600); err != nil {
t.Fatal(err)
}
executor = testInstallCommander{
existedPluginFilePath: existedPluginFilePath,
newPluginFilePath: newPluginFilePath,
existedPluginStdout: metadataJSON(validMetadata),
newPluginStdout: metadataJSON(validMetadataHigherVersion),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginDir,
}
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
if err != nil {
t.Fatalf("expecting nil error, but got %v", err)
}
if existingPluginMetadata.Version != "1.0.0" {
t.Fatalf("expecting existing plugin metadata to be 1.0.0, but got %s", existingPluginMetadata.Version)
}
if newPluginMetadata.Version != "1.1.0" {
t.Fatalf("expecting new plugin metadata to be 1.1.0, but got %s", newPluginMetadata.Version)
}
})
t.Run("success to install from plugin dir with no executable file and one valid candidate file", func(t *testing.T) {
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
newPluginFilePath := "testdata/foo/notation-foo"
newPluginLibPath := "testdata/foo/libfoo"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0600); err != nil {
t.Fatal(err)
}
if err := createFileAndChmod(newPluginLibPath, 0600); err != nil {
t.Fatal(err)
}
executor = testInstallCommander{
existedPluginFilePath: existedPluginFilePath,
newPluginFilePath: newPluginFilePath,
existedPluginStdout: metadataJSON(validMetadata),
newPluginStdout: metadataJSON(validMetadataHigherVersion),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginDir,
}
existingPluginMetadata, newPluginMetadata, err := mgr.Install(context.Background(), installOpts)
if err != nil {
t.Fatalf("expecting nil error, but got %v", err)
}
if existingPluginMetadata.Version != "1.0.0" {
t.Fatalf("expecting existing plugin metadata to be 1.0.0, but got %s", existingPluginMetadata.Version)
}
if newPluginMetadata.Version != "1.1.0" {
t.Fatalf("expecting new plugin metadata to be 1.1.0, but got %s", newPluginMetadata.Version)
}
})
t.Run("fail to install from plugin dir due to more than one candidate plugin executable files", func(t *testing.T) {
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
newPluginFilePath := "testdata/foo/notation-foo1"
newPluginFilePath2 := "testdata/foo/notation-foo2"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0600); err != nil {
t.Fatal(err)
}
if err := createFileAndChmod(newPluginFilePath2, 0600); err != nil {
t.Fatal(err)
}
executor = testInstallCommander{
existedPluginFilePath: existedPluginFilePath,
newPluginFilePath: newPluginFilePath,
existedPluginStdout: metadataJSON(validMetadata),
newPluginStdout: metadataJSON(validMetadataHigherVersion),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginDir,
}
expectedErrorMsg := "failed to read plugin from input directory: no plugin executable file was found"
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
}
})
t.Run("fail to install from plugin dir due to more than one plugin executable files", func(t *testing.T) {
existedPluginFilePath := "testdata/plugins/foo/notation-foo"
newPluginFilePath := "testdata/foo/notation-foo1"
newPluginFilePath2 := "testdata/foo/notation-foo2"
newPluginDir := filepath.Dir(newPluginFilePath)
if err := os.MkdirAll(newPluginDir, 0777); err != nil {
t.Fatalf("failed to create %s: %v", newPluginDir, err)
}
defer os.RemoveAll(newPluginDir)
if err := createFileAndChmod(newPluginFilePath, 0700); err != nil {
t.Fatal(err)
}
if err := createFileAndChmod(newPluginFilePath2, 0700); err != nil {
t.Fatal(err)
}
executor = testInstallCommander{
existedPluginFilePath: existedPluginFilePath,
newPluginFilePath: newPluginFilePath,
existedPluginStdout: metadataJSON(validMetadata),
newPluginStdout: metadataJSON(validMetadataHigherVersion),
}
installOpts := CLIInstallOptions{
PluginPath: newPluginDir,
}
expectedErrorMsg := "failed to read plugin from input directory: found more than one plugin executable files"
_, _, err := mgr.Install(context.Background(), installOpts)
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expecting error %s, but got %v", expectedErrorMsg, err)
}
})
}
func TestManager_Uninstall(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
executor = testCommander{stdout: metadataJSON(validMetadata)}
mgr := NewCLIManager(mockfs.NewSysFSWithRootMock(fstest.MapFS{}, "./testdata/plugins"))
if err := os.MkdirAll("./testdata/plugins/toUninstall", 0777); err != nil {
t.Fatalf("failed to create toUninstall dir: %v", err)
}
defer os.RemoveAll("./testdata/plugins/toUninstall")
pluginFile, err := os.Create("./testdata/plugins/toUninstall/toUninstall")
if err != nil {
t.Fatalf("failed to create toUninstall file: %v", err)
}
if err := pluginFile.Close(); err != nil {
t.Fatalf("failed to close toUninstall file: %v", err)
}
// test uninstall valid plugin
if err := mgr.Uninstall(context.Background(), "toUninstall"); err != nil {
t.Fatalf("Manager.Uninstall() err %v, want nil", err)
}
// test uninstall non-exist plugin
expectedErrorMsg := "stat testdata/plugins/non-exist: no such file or directory"
if err := mgr.Uninstall(context.Background(), "non-exist"); err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("Manager.Uninstall() err %v, want %s", err, expectedErrorMsg)
}
}
func TestParsePluginName(t *testing.T) {
if runtime.GOOS == "windows" {
pluginName, err := parsePluginName("notation-my-plugin.exe")
if err != nil {
t.Fatalf("expected nil err, but got %v", err)
}
if pluginName != "my-plugin" {
t.Fatalf("expected plugin name my-plugin, but got %s", pluginName)
}
expectedErrorMsg := "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got notation-com.plugin"
_, err = parsePluginName("notation-com.plugin")
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
}
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got my-plugin.exe"
_, err = parsePluginName("my-plugin.exe")
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
}
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got notation-.exe"
_, err = parsePluginName("notation-.exe")
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
}
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got my-plugin"
_, err = parsePluginName("my-plugin")
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
}
} else {
pluginName, err := parsePluginName("notation-my-plugin")
if err != nil {
t.Fatalf("expected nil err, but got %v", err)
}
if pluginName != "my-plugin" {
t.Fatalf("expected plugin name my-plugin, but got %s", pluginName)
}
pluginName, err = parsePluginName("notation-com.example.plugin")
if err != nil {
t.Fatalf("expected nil err, but got %v", err)
}
if pluginName != "com.example.plugin" {
t.Fatalf("expected plugin name com.example.plugin, but got %s", pluginName)
}
expectedErrorMsg := "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got myPlugin"
_, err = parsePluginName("myPlugin")
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
}
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got my-plugin"
_, err = parsePluginName("my-plugin")
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
}
expectedErrorMsg = "invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got notation-"
_, err = parsePluginName("notation-")
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected %s, but got %v", expectedErrorMsg, err)
}
}
}
func metadataJSON(m proto.GetMetadataResponse) []byte {
d, err := json.Marshal(m)
if err != nil {
@ -81,3 +669,14 @@ func metadataJSON(m proto.GetMetadataResponse) []byte {
}
return d
}
func createFileAndChmod(path string, mode fs.FileMode) error {
f, err := os.Create(path)
if err != nil {
return err
}
if err := f.Chmod(mode); err != nil {
return err
}
return f.Close()
}

View File

@ -1,10 +1,61 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !windows
// +build !windows
package plugin
import "github.com/notaryproject/notation-go/plugin/proto"
import (
"fmt"
"os"
"strings"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
)
func binName(name string) string {
return proto.Prefix + name
return plugin.BinaryPrefix + name
}
// isExecutableFile checks if a file at filePath is user executable
func isExecutableFile(filePath string) (bool, error) {
fi, err := os.Stat(filePath)
if err != nil {
return false, err
}
mode := fi.Mode()
if !mode.IsRegular() {
return false, ErrNotRegularFile
}
return mode.Perm()&0100 != 0, nil
}
// parsePluginName checks if fileName is a valid plugin file name
// and gets plugin name from it based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation
func parsePluginName(fileName string) (string, error) {
pluginName, found := strings.CutPrefix(fileName, plugin.BinaryPrefix)
if !found || pluginName == "" {
return "", fmt.Errorf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}, but got %s", fileName)
}
return pluginName, nil
}
// setExecutable sets file to be user executable
func setExecutable(filePath string) error {
fileInfo, err := os.Stat(filePath)
if err != nil {
return err
}
return os.Chmod(filePath, fileInfo.Mode()|os.FileMode(0100))
}

View File

@ -1,7 +1,60 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import "github.com/notaryproject/notation-go/plugin/proto"
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/notaryproject/notation-go/internal/file"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
)
func binName(name string) string {
return proto.Prefix + name + ".exe"
return plugin.BinaryPrefix + name + ".exe"
}
// isExecutableFile checks if a file at filePath is executable
func isExecutableFile(filePath string) (bool, error) {
fi, err := os.Stat(filePath)
if err != nil {
return false, err
}
if !fi.Mode().IsRegular() {
return false, ErrNotRegularFile
}
return strings.EqualFold(filepath.Ext(filepath.Base(filePath)), ".exe"), nil
}
// parsePluginName checks if fileName is a valid plugin file name
// and gets plugin name from it based on spec: https://github.com/notaryproject/specifications/blob/main/specs/plugin-extensibility.md#installation
func parsePluginName(fileName string) (string, error) {
if !strings.EqualFold(filepath.Ext(fileName), ".exe") {
return "", fmt.Errorf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got %s", fileName)
}
fname := file.TrimFileExtension(fileName)
pluginName, found := strings.CutPrefix(fname, plugin.BinaryPrefix)
if !found || pluginName == "" {
return "", fmt.Errorf("invalid plugin executable file name. Plugin file name requires format notation-{plugin-name}.exe, but got %s", fileName)
}
return pluginName, nil
}
// setExecutable returns error on Windows. User needs to install the correct
// plugin file.
func setExecutable(filePath string) error {
return fmt.Errorf(`plugin executable file must have file extension ".exe", but got %q`, filepath.Base(filePath))
}

View File

@ -1,4 +1,17 @@
// Package plugin provides the toolings to use the notation plugin.
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package plugin provides the tooling to use the notation plugin.
//
// includes a CLIManager and a CLIPlugin implementation.
package plugin
@ -12,63 +25,58 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/notaryproject/notation-go/internal/io"
"github.com/notaryproject/notation-go/internal/slices"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/plugin/proto"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
)
// maxPluginOutputSize is the maximum size of the plugin output.
const maxPluginOutputSize = 64 * 1024 * 1024 // 64 MiB
var executor commander = &execCommander{} // for unit test
// GenericPlugin is the base requirement to be an plugin.
type GenericPlugin interface {
// GetMetadata returns the metadata information of the plugin.
GetMetadata(ctx context.Context, req *proto.GetMetadataRequest) (*proto.GetMetadataResponse, error)
}
// GenericPlugin is the base requirement to be a plugin.
//
// Deprecated: GenericPlugin exists for historical compatibility and should not be used.
// To access GenericPlugin, use the notation-plugin-framework-go's plugin.GenericPlugin type.
type GenericPlugin = plugin.GenericPlugin
// SignPlugin defines the required methods to be a SignPlugin.
type SignPlugin interface {
GenericPlugin
// DescribeKey returns the KeySpec of a key.
DescribeKey(ctx context.Context, req *proto.DescribeKeyRequest) (*proto.DescribeKeyResponse, error)
// GenerateSignature generates the raw signature based on the request.
GenerateSignature(ctx context.Context, req *proto.GenerateSignatureRequest) (*proto.GenerateSignatureResponse, error)
// GenerateEnvelope generates the Envelope with signature based on the
// request.
GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error)
}
//
// Deprecated: SignPlugin exists for historical compatibility and should not be used.
// To access SignPlugin, use the notation-plugin-framework-go's plugin.SignPlugin type.
type SignPlugin = plugin.SignPlugin
// VerifyPlugin defines the required method to be a VerifyPlugin.
type VerifyPlugin interface {
GenericPlugin
//
// Deprecated: VerifyPlugin exists for historical compatibility and should not be used.
// To access VerifyPlugin, use the notation-plugin-framework-go's plugin.VerifyPlugin type.
type VerifyPlugin = plugin.VerifyPlugin
// VerifySignature validates the signature based on the request.
VerifySignature(ctx context.Context, req *proto.VerifySignatureRequest) (*proto.VerifySignatureResponse, error)
}
// Plugin defines required methods to be a Plugin.
//
// Deprecated: Plugin exists for historical compatibility and should not be used.
// To access Plugin, use the notation-plugin-framework-go's plugin.Plugin type.
type Plugin = plugin.Plugin
// Plugin defines required methods to be an Plugin.
type Plugin interface {
SignPlugin
VerifyPlugin
}
// CLIPlugin implements Plugin interface to CLI plugins.
// CLIPlugin implements [Plugin] interface to CLI plugins.
type CLIPlugin struct {
name string
path string
}
// NewCLIPlugin validate the metadata of the plugin and return a *CLIPlugin.
// NewCLIPlugin returns a *CLIPlugin.
func NewCLIPlugin(ctx context.Context, name, path string) (*CLIPlugin, error) {
// validate file existence
fi, err := os.Stat(path)
if err != nil {
// Ignore any file which we cannot Stat
// (e.g. due to permissions or anything else).
return nil, err
return nil, fmt.Errorf("plugin executable file is either not found or inaccessible: %w", err)
}
if !fi.Mode().IsRegular() {
// Ignore non-regular files.
@ -76,26 +84,28 @@ func NewCLIPlugin(ctx context.Context, name, path string) (*CLIPlugin, error) {
}
// generate plugin
plugin := CLIPlugin{
return &CLIPlugin{
name: name,
path: path,
}
return &plugin, nil
}, nil
}
// GetMetadata returns the metadata information of the plugin.
func (p *CLIPlugin) GetMetadata(ctx context.Context, req *proto.GetMetadataRequest) (*proto.GetMetadataResponse, error) {
var metadata proto.GetMetadataResponse
func (p *CLIPlugin) GetMetadata(ctx context.Context, req *plugin.GetMetadataRequest) (*plugin.GetMetadataResponse, error) {
var metadata plugin.GetMetadataResponse
err := run(ctx, p.name, p.path, req, &metadata)
if err != nil {
return nil, err
}
// validate metadata
if err = validate(&metadata); err != nil {
return nil, fmt.Errorf("invalid metadata: %w", err)
return nil, &PluginMalformedError{
Msg: fmt.Sprintf("metadata validation failed for plugin %s: %s", p.name, err),
InnerError: err,
}
}
if metadata.Name != p.name {
return nil, fmt.Errorf("executable name must be %q instead of %q", binName(metadata.Name), filepath.Base(p.path))
return nil, fmt.Errorf("plugin executable file name must be %q instead of %q", binName(metadata.Name), filepath.Base(p.path))
}
return &metadata, nil
}
@ -103,12 +113,12 @@ func (p *CLIPlugin) GetMetadata(ctx context.Context, req *proto.GetMetadataReque
// DescribeKey returns the KeySpec of a key.
//
// if ContractVersion is not set, it will be set by the function.
func (p *CLIPlugin) DescribeKey(ctx context.Context, req *proto.DescribeKeyRequest) (*proto.DescribeKeyResponse, error) {
func (p *CLIPlugin) DescribeKey(ctx context.Context, req *plugin.DescribeKeyRequest) (*plugin.DescribeKeyResponse, error) {
if req.ContractVersion == "" {
req.ContractVersion = proto.ContractVersion
req.ContractVersion = plugin.ContractVersion
}
var resp proto.DescribeKeyResponse
var resp plugin.DescribeKeyResponse
err := run(ctx, p.name, p.path, req, &resp)
return &resp, err
}
@ -116,12 +126,12 @@ func (p *CLIPlugin) DescribeKey(ctx context.Context, req *proto.DescribeKeyReque
// GenerateSignature generates the raw signature based on the request.
//
// if ContractVersion is not set, it will be set by the function.
func (p *CLIPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateSignatureRequest) (*proto.GenerateSignatureResponse, error) {
func (p *CLIPlugin) GenerateSignature(ctx context.Context, req *plugin.GenerateSignatureRequest) (*plugin.GenerateSignatureResponse, error) {
if req.ContractVersion == "" {
req.ContractVersion = proto.ContractVersion
req.ContractVersion = plugin.ContractVersion
}
var resp proto.GenerateSignatureResponse
var resp plugin.GenerateSignatureResponse
err := run(ctx, p.name, p.path, req, &resp)
return &resp, err
}
@ -129,12 +139,12 @@ func (p *CLIPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateSi
// GenerateEnvelope generates the Envelope with signature based on the request.
//
// if ContractVersion is not set, it will be set by the function.
func (p *CLIPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error) {
func (p *CLIPlugin) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) {
if req.ContractVersion == "" {
req.ContractVersion = proto.ContractVersion
req.ContractVersion = plugin.ContractVersion
}
var resp proto.GenerateEnvelopeResponse
var resp plugin.GenerateEnvelopeResponse
err := run(ctx, p.name, p.path, req, &resp)
return &resp, err
}
@ -142,76 +152,96 @@ func (p *CLIPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEnv
// VerifySignature validates the signature based on the request.
//
// if ContractVersion is not set, it will be set by the function.
func (p *CLIPlugin) VerifySignature(ctx context.Context, req *proto.VerifySignatureRequest) (*proto.VerifySignatureResponse, error) {
func (p *CLIPlugin) VerifySignature(ctx context.Context, req *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) {
if req.ContractVersion == "" {
req.ContractVersion = proto.ContractVersion
req.ContractVersion = plugin.ContractVersion
}
var resp proto.VerifySignatureResponse
var resp plugin.VerifySignatureResponse
err := run(ctx, p.name, p.path, req, &resp)
return &resp, err
}
func run(ctx context.Context, pluginName string, pluginPath string, req proto.Request, resp interface{}) error {
func run(ctx context.Context, pluginName string, pluginPath string, req plugin.Request, resp interface{}) error {
logger := log.GetLogger(ctx)
// serialize request
data, err := json.Marshal(req)
if err != nil {
return fmt.Errorf("%s: failed to marshal request object: %w", pluginName, err)
logger.Errorf("Failed to marshal request object: %+v", req)
return fmt.Errorf("failed to marshal request object: %w", err)
}
logger.Debugf("Plugin %s request: %s", req.Command(), string(data))
// execute request
stdout, stderr, err := executor.Output(ctx, pluginPath, req.Command(), data)
if err != nil {
logger.Debugf("plugin %s execution status: %v", req.Command(), err)
logger.Debugf("Plugin %s returned error: %s", req.Command(), string(stderr))
var re proto.RequestError
jsonErr := json.Unmarshal(stderr, &re)
if jsonErr != nil {
return proto.RequestError{
Code: proto.ErrorCodeGeneric,
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", err, string(stderr))}
logger.Errorf("plugin %s execution status: %v", req.Command(), err)
if len(stderr) == 0 {
// if stderr is empty, it is possible that the plugin is not
// running properly.
logger.Errorf("failed to execute the %s command for plugin %s: %s", req.Command(), pluginName, err)
return &PluginExecutableFileError{
InnerError: err,
}
} else {
var re proto.RequestError
jsonErr := json.Unmarshal(stderr, &re)
if jsonErr != nil {
logger.Errorf("failed to execute the %s command for plugin %s: %s", req.Command(), pluginName, strings.TrimSuffix(string(stderr), "\n"))
return &PluginMalformedError{
InnerError: jsonErr,
}
}
logger.Errorf("failed to execute the %s command for plugin %s: %s: %w", req.Command(), pluginName, re.Code, re)
return re
}
return re
}
logger.Debugf("Plugin %s response: %s", req.Command(), string(stdout))
// deserialize response
err = json.Unmarshal(stdout, resp)
if err != nil {
return fmt.Errorf("failed to decode json response: %w", ErrNotCompliant)
if err = json.Unmarshal(stdout, resp); err != nil {
logger.Errorf("failed to unmarshal plugin %s response: %w", req.Command(), err)
return &PluginMalformedError{
Msg: fmt.Sprintf("failed to unmarshal the response of %s command for plugin %s", req.Command(), pluginName),
InnerError: err,
}
}
return nil
}
// commander is defined for mocking purposes.
type commander interface {
// Output runs the command, passing req to the its stdin.
// Output runs the command, passing req to the stdin.
// It only returns an error if the binary can't be executed.
// Returns stdout if err is nil, stderr if err is not nil.
Output(ctx context.Context, path string, command proto.Command, req []byte) (stdout []byte, stderr []byte, err error)
Output(ctx context.Context, path string, command plugin.Command, req []byte) (stdout []byte, stderr []byte, err error)
}
// execCommander implements the commander interface using exec.Command().
type execCommander struct{}
func (c execCommander) Output(ctx context.Context, name string, command proto.Command, req []byte) ([]byte, []byte, error) {
func (c execCommander) Output(ctx context.Context, name string, command plugin.Command, req []byte) ([]byte, []byte, error) {
var stdout, stderr bytes.Buffer
cmd := exec.CommandContext(ctx, name, string(command))
cmd.Stdin = bytes.NewReader(req)
cmd.Stderr = &stderr
cmd.Stdout = &stdout
// The limit writer will be handled by the caller in run() by comparing the
// bytes written with the expected length of the bytes.
cmd.Stderr = io.LimitWriter(&stderr, maxPluginOutputSize)
cmd.Stdout = io.LimitWriter(&stdout, maxPluginOutputSize)
err := cmd.Run()
if err != nil {
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, stderr.Bytes(), fmt.Errorf("'%s %s' command execution timeout: %w", name, string(command), err)
}
return nil, stderr.Bytes(), err
}
return stdout.Bytes(), nil, nil
}
// validate checks if the metadata is correctly populated.
func validate(metadata *proto.GetMetadataResponse) error {
func validate(metadata *plugin.GetMetadataResponse) error {
if metadata.Name == "" {
return errors.New("empty name")
}
@ -230,10 +260,10 @@ func validate(metadata *proto.GetMetadataResponse) error {
if len(metadata.SupportedContractVersions) == 0 {
return errors.New("supported contract versions not specified")
}
if !slices.Contains(metadata.SupportedContractVersions, proto.ContractVersion) {
if !slices.Contains(metadata.SupportedContractVersions, plugin.ContractVersion) {
return fmt.Errorf(
"contract version %q is not in the list of the plugin supported versions %v",
proto.ContractVersion, metadata.SupportedContractVersions,
plugin.ContractVersion, metadata.SupportedContractVersions,
)
}
return nil

View File

@ -1,15 +1,29 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package plugin
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"testing"
"time"
"github.com/notaryproject/notation-go/plugin/proto"
)
@ -18,14 +32,12 @@ func TestGetMetadata(t *testing.T) {
t.Run("plugin error is in invalid json format", func(t *testing.T) {
exitErr := errors.New("unknown error")
stderr := []byte("sad")
wantErr := proto.RequestError{
Code: proto.ErrorCodeGeneric,
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", exitErr, string(stderr))}
plugin := CLIPlugin{}
expectedErrMsg := "invalid character 's' looking for beginning of value"
plugin := CLIPlugin{name: "test-plugin"}
executor = testCommander{stdout: nil, stderr: stderr, err: exitErr}
_, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
if !errors.Is(err, wantErr) {
t.Fatalf("should error. got err = %v, want %v", err, wantErr)
if err.Error() != expectedErrMsg {
t.Fatalf("should error. got err = %v, want %v", err, expectedErrMsg)
}
})
@ -45,14 +57,12 @@ func TestGetMetadata(t *testing.T) {
t.Run("plugin cause system error", func(t *testing.T) {
exitErr := errors.New("system error")
stderr := []byte("")
wantErr := proto.RequestError{
Code: proto.ErrorCodeGeneric,
Err: fmt.Errorf("response is not in JSON format. error: %v, stderr: %s", exitErr, string(stderr))}
plugin := CLIPlugin{}
expectedErrMsg := "system error"
plugin := CLIPlugin{name: "test-plugin"}
executor = testCommander{stdout: nil, stderr: stderr, err: exitErr}
_, err := plugin.GetMetadata(context.Background(), &proto.GetMetadataRequest{})
if !errors.Is(err, wantErr) {
t.Fatalf("should error. got err = %v, want %v", err, wantErr)
if err.Error() != expectedErrMsg {
t.Fatalf("should error. got err = %v, want %v", err, expectedErrMsg)
}
})
@ -173,7 +183,7 @@ func TestValidateMetadata(t *testing.T) {
}
}
func TestNewCLIPlugin_PathError(t *testing.T) {
func TestNewCLIPlugin_Error(t *testing.T) {
ctx := context.Background()
t.Run("plugin directory exists without executable.", func(t *testing.T) {
p, err := NewCLIPlugin(ctx, "emptyplugin", "./testdata/plugins/emptyplugin/notation-emptyplugin")
@ -186,14 +196,34 @@ func TestNewCLIPlugin_PathError(t *testing.T) {
})
t.Run("plugin is not a regular file", func(t *testing.T) {
expectedErrMsg := "plugin executable file is not a regular file"
p, err := NewCLIPlugin(ctx, "badplugin", "./testdata/plugins/badplugin/notation-badplugin")
if !errors.Is(err, ErrNotRegularFile) {
t.Errorf("NewCLIPlugin() error = %v, want %v", err, ErrNotRegularFile)
if err.Error() != expectedErrMsg {
t.Errorf("NewCLIPlugin() error = %v, want %v", err, expectedErrMsg)
}
if p != nil {
t.Errorf("NewCLIPlugin() plugin = %v, want nil", p)
}
})
t.Run("plugin timeout error", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
expectedErrMsg := "'sleep 2' command execution timeout: signal: killed"
ctxWithTimout, cancel := context.WithTimeout(ctx, 10 * time.Millisecond)
defer cancel()
var twoSeconds proto.Command
twoSeconds = "2"
_, _, err := execCommander{}.Output(ctxWithTimout, "sleep", twoSeconds, nil);
if err == nil {
t.Errorf("execCommander{}.Output() expected error = %v, got nil", expectedErrMsg)
}
if err.Error() != expectedErrMsg {
t.Errorf("execCommander{}.Output() error = %v, want %v", err, expectedErrMsg)
}
})
}
func TestNewCLIPlugin_ValidError(t *testing.T) {
@ -205,23 +235,23 @@ func TestNewCLIPlugin_ValidError(t *testing.T) {
t.Run("command no response", func(t *testing.T) {
executor = testCommander{}
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
if !strings.Contains(err.Error(), ErrNotCompliant.Error()) {
t.Fatal("should fail the operation.")
if _, ok := err.(*PluginMalformedError); !ok {
t.Fatal("should return plugin validity error")
}
})
t.Run("invalid json", func(t *testing.T) {
executor = testCommander{stdout: []byte("content")}
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
if !strings.Contains(err.Error(), ErrNotCompliant.Error()) {
t.Fatal("should fail the operation.")
if _, ok := err.(*PluginMalformedError); !ok {
t.Fatal("should return plugin validity error")
}
})
t.Run("invalid metadata name", func(t *testing.T) {
executor = testCommander{stdout: metadataJSON(invalidMetadataName)}
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
if !strings.Contains(err.Error(), "executable name must be") {
if !strings.Contains(err.Error(), "executable file name must be") {
t.Fatal("should fail the operation.")
}
})
@ -229,8 +259,8 @@ func TestNewCLIPlugin_ValidError(t *testing.T) {
t.Run("invalid metadata content", func(t *testing.T) {
executor = testCommander{stdout: metadataJSON(proto.GetMetadataResponse{Name: "foo"})}
_, err := p.GetMetadata(ctx, &proto.GetMetadataRequest{})
if !strings.Contains(err.Error(), "invalid metadata") {
t.Fatal("should fail the operation.")
if _, ok := err.(*PluginMalformedError); !ok {
t.Fatal("should be plugin validity error.")
}
})

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proto
import (
@ -5,67 +18,74 @@ import (
"fmt"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
)
// KeySpec is type of the signing algorithm, including algorithm and size.
type KeySpec string
//
// Deprecated: KeySpec exists for historical compatibility and should not be used.
// To access KeySpec, use the notation-plugin-framework-go's [plugin.KeySpec] type.
type KeySpec = plugin.KeySpec
// one of the following supported key spec names.
//
// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
// Deprecated: KeySpec exists for historical compatibility and should not be used.
// To access KeySpec, use the notation-plugin-framework-go's [plugin.KeySpec].
//
// [keys spec]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
const (
KeySpecRSA2048 KeySpec = "RSA-2048"
KeySpecRSA3072 KeySpec = "RSA-3072"
KeySpecRSA4096 KeySpec = "RSA-4096"
KeySpecEC256 KeySpec = "EC-256"
KeySpecEC384 KeySpec = "EC-384"
KeySpecEC521 KeySpec = "EC-521"
KeySpecRSA2048 = plugin.KeySpecRSA2048
KeySpecRSA3072 = plugin.KeySpecRSA3072
KeySpecRSA4096 = plugin.KeySpecRSA4096
KeySpecEC256 = plugin.KeySpecEC256
KeySpecEC384 = plugin.KeySpecEC384
KeySpecEC521 = plugin.KeySpecEC521
)
// EncodeKeySpec returns the name of a keySpec according to the spec.
func EncodeKeySpec(k signature.KeySpec) (KeySpec, error) {
func EncodeKeySpec(k signature.KeySpec) (plugin.KeySpec, error) {
switch k.Type {
case signature.KeyTypeEC:
switch k.Size {
case 256:
return KeySpecEC256, nil
return plugin.KeySpecEC256, nil
case 384:
return KeySpecEC384, nil
return plugin.KeySpecEC384, nil
case 521:
return KeySpecEC521, nil
return plugin.KeySpecEC521, nil
}
case signature.KeyTypeRSA:
switch k.Size {
case 2048:
return KeySpecRSA2048, nil
return plugin.KeySpecRSA2048, nil
case 3072:
return KeySpecRSA3072, nil
return plugin.KeySpecRSA3072, nil
case 4096:
return KeySpecRSA4096, nil
return plugin.KeySpecRSA4096, nil
}
}
return "", fmt.Errorf("invalid KeySpec %q", k)
}
// DecodeKeySpec parses keySpec name to a signature.keySpec type.
func DecodeKeySpec(k KeySpec) (keySpec signature.KeySpec, err error) {
func DecodeKeySpec(k plugin.KeySpec) (keySpec signature.KeySpec, err error) {
switch k {
case KeySpecRSA2048:
case plugin.KeySpecRSA2048:
keySpec.Size = 2048
keySpec.Type = signature.KeyTypeRSA
case KeySpecRSA3072:
case plugin.KeySpecRSA3072:
keySpec.Size = 3072
keySpec.Type = signature.KeyTypeRSA
case KeySpecRSA4096:
case plugin.KeySpecRSA4096:
keySpec.Size = 4096
keySpec.Type = signature.KeyTypeRSA
case KeySpecEC256:
case plugin.KeySpecEC256:
keySpec.Size = 256
keySpec.Type = signature.KeyTypeEC
case KeySpecEC384:
case plugin.KeySpecEC384:
keySpec.Size = 384
keySpec.Type = signature.KeyTypeEC
case KeySpecEC521:
case plugin.KeySpecEC521:
keySpec.Size = 521
keySpec.Type = signature.KeyTypeEC
default:
@ -75,92 +95,104 @@ func DecodeKeySpec(k KeySpec) (keySpec signature.KeySpec, err error) {
return
}
// HashAlgorithm is the type of a hash algorithm.
type HashAlgorithm string
// HashAlgorithm is the type of hash algorithm.
//
// Deprecated: HashAlgorithm exists for historical compatibility and should not be used.
// To access HashAlgorithm, use the notation-plugin-framework-go's [plugin.HashAlgorithm] type.
type HashAlgorithm = plugin.HashAlgorithm
// one of the following supported hash algorithm names.
//
// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
// Deprecated: HashAlgorithm exists for historical compatibility and should not be used.
// To access HashAlgorithm, use the notation-plugin-framework-go's [plugin.HashAlgorithm] type.
//
// [hash algorithm]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
const (
HashAlgorithmSHA256 HashAlgorithm = "SHA-256"
HashAlgorithmSHA384 HashAlgorithm = "SHA-384"
HashAlgorithmSHA512 HashAlgorithm = "SHA-512"
HashAlgorithmSHA256 = plugin.HashAlgorithmSHA256
HashAlgorithmSHA384 = plugin.HashAlgorithmSHA384
HashAlgorithmSHA512 = plugin.HashAlgorithmSHA512
)
// HashAlgorithmFromKeySpec returns the name of hash function according to the spec.
func HashAlgorithmFromKeySpec(k signature.KeySpec) (HashAlgorithm, error) {
func HashAlgorithmFromKeySpec(k signature.KeySpec) (plugin.HashAlgorithm, error) {
switch k.Type {
case signature.KeyTypeEC:
switch k.Size {
case 256:
return HashAlgorithmSHA256, nil
return plugin.HashAlgorithmSHA256, nil
case 384:
return HashAlgorithmSHA384, nil
return plugin.HashAlgorithmSHA384, nil
case 521:
return HashAlgorithmSHA512, nil
return plugin.HashAlgorithmSHA512, nil
}
case signature.KeyTypeRSA:
switch k.Size {
case 2048:
return HashAlgorithmSHA256, nil
return plugin.HashAlgorithmSHA256, nil
case 3072:
return HashAlgorithmSHA384, nil
return plugin.HashAlgorithmSHA384, nil
case 4096:
return HashAlgorithmSHA512, nil
return plugin.HashAlgorithmSHA512, nil
}
}
return "", fmt.Errorf("invalid KeySpec %q", k)
}
// SignatureAlgorithm is the type of signature algorithm
type SignatureAlgorithm string
// one of the following supported signing algorithm names.
//
// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
// Deprecated: SignatureAlgorithm exists for historical compatibility and should not be used.
// To access SignatureAlgorithm, use the notation-plugin-framework-go's [plugin.SignatureAlgorithm] type.
type SignatureAlgorithm = plugin.SignatureAlgorithm
// one of the following supported [signing algorithm] names.
//
// Deprecated: SignatureAlgorithm exists for historical compatibility and should not be used.
// To access SignatureAlgorithm, use the notation-plugin-framework-go's [plugin.SignatureAlgorithm] type.
//
// [signing algorithm]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
const (
SignatureAlgorithmECDSA_SHA256 SignatureAlgorithm = "ECDSA-SHA-256"
SignatureAlgorithmECDSA_SHA384 SignatureAlgorithm = "ECDSA-SHA-384"
SignatureAlgorithmECDSA_SHA512 SignatureAlgorithm = "ECDSA-SHA-512"
SignatureAlgorithmRSASSA_PSS_SHA256 SignatureAlgorithm = "RSASSA-PSS-SHA-256"
SignatureAlgorithmRSASSA_PSS_SHA384 SignatureAlgorithm = "RSASSA-PSS-SHA-384"
SignatureAlgorithmRSASSA_PSS_SHA512 SignatureAlgorithm = "RSASSA-PSS-SHA-512"
SignatureAlgorithmECDSA_SHA256 = plugin.SignatureAlgorithmECDSA_SHA256
SignatureAlgorithmECDSA_SHA384 = plugin.SignatureAlgorithmECDSA_SHA384
SignatureAlgorithmECDSA_SHA512 = plugin.SignatureAlgorithmECDSA_SHA512
SignatureAlgorithmRSASSA_PSS_SHA256 = plugin.SignatureAlgorithmRSASSA_PSS_SHA256
SignatureAlgorithmRSASSA_PSS_SHA384 = plugin.SignatureAlgorithmRSASSA_PSS_SHA384
SignatureAlgorithmRSASSA_PSS_SHA512 = plugin.SignatureAlgorithmRSASSA_PSS_SHA512
)
// EncodeSigningAlgorithm returns the signing algorithm name of an algorithm
// according to the spec.
func EncodeSigningAlgorithm(alg signature.Algorithm) (SignatureAlgorithm, error) {
func EncodeSigningAlgorithm(alg signature.Algorithm) (plugin.SignatureAlgorithm, error) {
switch alg {
case signature.AlgorithmES256:
return SignatureAlgorithmECDSA_SHA256, nil
return plugin.SignatureAlgorithmECDSA_SHA256, nil
case signature.AlgorithmES384:
return SignatureAlgorithmECDSA_SHA384, nil
return plugin.SignatureAlgorithmECDSA_SHA384, nil
case signature.AlgorithmES512:
return SignatureAlgorithmECDSA_SHA512, nil
return plugin.SignatureAlgorithmECDSA_SHA512, nil
case signature.AlgorithmPS256:
return SignatureAlgorithmRSASSA_PSS_SHA256, nil
return plugin.SignatureAlgorithmRSASSA_PSS_SHA256, nil
case signature.AlgorithmPS384:
return SignatureAlgorithmRSASSA_PSS_SHA384, nil
return plugin.SignatureAlgorithmRSASSA_PSS_SHA384, nil
case signature.AlgorithmPS512:
return SignatureAlgorithmRSASSA_PSS_SHA512, nil
return plugin.SignatureAlgorithmRSASSA_PSS_SHA512, nil
}
return "", fmt.Errorf("invalid algorithm %q", alg)
}
// DecodeSigningAlgorithm parses the signing algorithm name from a given string.
func DecodeSigningAlgorithm(raw SignatureAlgorithm) (signature.Algorithm, error) {
func DecodeSigningAlgorithm(raw plugin.SignatureAlgorithm) (signature.Algorithm, error) {
switch raw {
case SignatureAlgorithmECDSA_SHA256:
case plugin.SignatureAlgorithmECDSA_SHA256:
return signature.AlgorithmES256, nil
case SignatureAlgorithmECDSA_SHA384:
case plugin.SignatureAlgorithmECDSA_SHA384:
return signature.AlgorithmES384, nil
case SignatureAlgorithmECDSA_SHA512:
case plugin.SignatureAlgorithmECDSA_SHA512:
return signature.AlgorithmES512, nil
case SignatureAlgorithmRSASSA_PSS_SHA256:
case plugin.SignatureAlgorithmRSASSA_PSS_SHA256:
return signature.AlgorithmPS256, nil
case SignatureAlgorithmRSASSA_PSS_SHA384:
case plugin.SignatureAlgorithmRSASSA_PSS_SHA384:
return signature.AlgorithmPS384, nil
case SignatureAlgorithmRSASSA_PSS_SHA512:
case plugin.SignatureAlgorithmRSASSA_PSS_SHA512:
return signature.AlgorithmPS512, nil
}
return 0, errors.New("unknown signing algorithm")

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proto
import (
@ -215,7 +228,7 @@ func TestDecodeKeySpec(t *testing.T) {
},
{
name: "Unsupported key spec",
raw: "unsuppored",
raw: "unsupported",
expected: signature.KeySpec{},
expectErr: true,
},

View File

@ -1,51 +1,78 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proto
import (
"encoding/json"
"errors"
"fmt"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
)
type ErrorCode string
// Deprecated: ErrorCode exists for historical compatibility and should not be used.
// To access ErrorCode, use the notation-plugin-framework-go's plugin.ErrorCode type.
type ErrorCode = plugin.ErrorCode
const (
// Any of the required request fields was empty,
// or a value was malformed/invalid.
ErrorCodeValidation ErrorCode = "VALIDATION_ERROR"
// ErrorCodeValidation is used when any of the required request fields is empty ormalformed/invalid.
//
// Deprecated: ErrorCodeValidation exists for historical compatibility and should not be used.
// To access ErrorCodeValidation, use the notation-plugin-framework-go's [plugin.ErrorCodeValidation].
ErrorCodeValidation = plugin.ErrorCodeValidation
// The contract version used in the request is unsupported.
ErrorCodeUnsupportedContractVersion ErrorCode = "UNSUPPORTED_CONTRACT_VERSION"
// ErrorCodeUnsupportedContractVersion is used when when the contract version used in the request is unsupported.
//
// Deprecated: ErrorCodeUnsupportedContractVersion exists for historical compatibility and should not be used.
// To access ErrorCodeUnsupportedContractVersion, use the notation-plugin-framework-go's [plugin.ErrorCodeUnsupportedContractVersion].
ErrorCodeUnsupportedContractVersion = plugin.ErrorCodeUnsupportedContractVersion
// Authentication/authorization error to use given key.
ErrorCodeAccessDenied ErrorCode = "ACCESS_DENIED"
// ErrorCodeAccessDenied is used when user doesn't have required permission to access the key.
//
// Deprecated: ErrorCodeAccessDenied exists for historical compatibility and should not be used.
// To access ErrorCodeAccessDenied, use the notation-plugin-framework-go's [plugin.ErrorCodeAccessDenied].
ErrorCodeAccessDenied = plugin.ErrorCodeAccessDenied
// The operation to generate signature timed out
// ErrorCodeTimeout is used when an operation to generate signature timed out and can be retried by Notation.
//
// Deprecated: ErrorCodeTimeout exists for historical compatibility and should not be used.
// To access ErrorCodeTimeout, use the notation-plugin-framework-go's [plugin.ErrorCodeTimeout].
ErrorCodeTimeout = plugin.ErrorCodeTimeout
// ErrorCodeThrottled is used when an operation to generate signature was throttles
// and can be retried by Notation.
ErrorCodeTimeout ErrorCode = "TIMEOUT"
//
// Deprecated: ErrorCodeThrottled exists for historical compatibility and should not be used.
// To access ErrorCodeThrottled, use the notation-plugin-framework-go's [plugin.ErrorCodeThrottled].
ErrorCodeThrottled = plugin.ErrorCodeThrottled
// The operation to generate signature was throttles
// and can be retried by Notation.
ErrorCodeThrottled ErrorCode = "THROTTLED"
// Any general error that does not fall into any categories.
ErrorCodeGeneric ErrorCode = "ERROR"
// ErrorCodeGeneric is used when an general error occurred that does not fall into any categories.
//
// Deprecated: ErrorCodeGeneric exists for historical compatibility and should not be used.
// To access ErrorCodeGeneric, use the notation-plugin-framework-go's [plugin.ErrorCodeGeneric].
ErrorCodeGeneric = plugin.ErrorCodeGeneric
)
type jsonErr struct {
Code ErrorCode `json:"errorCode"`
Message string `json:"errorMessage,omitempty"`
Metadata map[string]string `json:"errorMetadata,omitempty"`
}
// RequestError is the common error response for any request.
type RequestError struct {
Code ErrorCode
Code plugin.ErrorCode
Err error
Metadata map[string]string
}
func (e RequestError) Error() string {
return fmt.Sprintf("%s: %v", e.Code, e.Err)
return fmt.Sprintf("%v", e.Err)
}
func (e RequestError) Unwrap() error {
@ -70,19 +97,19 @@ func (e RequestError) MarshalJSON() ([]byte, error) {
if e.Err != nil {
msg = e.Err.Error()
}
return json.Marshal(jsonErr{e.Code, msg, e.Metadata})
return json.Marshal(plugin.Error{ErrCode: e.Code, Message: msg, Metadata: e.Metadata})
}
func (e *RequestError) UnmarshalJSON(data []byte) error {
var tmp jsonErr
var tmp plugin.Error
err := json.Unmarshal(data, &tmp)
if err != nil {
return err
}
if tmp.Code == "" && tmp.Message == "" && tmp.Metadata == nil {
if tmp.ErrCode == "" && tmp.Message == "" && tmp.Metadata == nil {
return errors.New("incomplete json")
}
*e = RequestError{Code: tmp.Code, Metadata: tmp.Metadata}
*e = RequestError{Code: tmp.ErrCode, Metadata: tmp.Metadata}
if tmp.Message != "" {
e.Err = errors.New(tmp.Message)
}

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proto
import (
@ -9,7 +22,7 @@ import (
func TestRequestError_Error(t *testing.T) {
err := RequestError{Code: ErrorCodeAccessDenied, Err: errors.New("an error")}
want := string(ErrorCodeAccessDenied) + ": an error"
want := "an error"
if got := err.Error(); got != want {
t.Errorf("RequestError.Error() = %v, want %v", got, want)
}

View File

@ -1,36 +1,28 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proto
// GetMetadataRequest contains the parameters passed in a get-plugin-metadata
// request.
type GetMetadataRequest struct {
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
}
import "github.com/notaryproject/notation-plugin-framework-go/plugin"
func (GetMetadataRequest) Command() Command {
return CommandGetMetadata
}
// GetMetadataRequest contains the parameters passed in a get-plugin-metadata request.
//
// Deprecated: GetMetadataRequest exists for historical compatibility and should not be used.
// To access GetMetadataRequest, use the notation-plugin-framework-go's [plugin.GetMetadataRequest] type.
type GetMetadataRequest = plugin.GetMetadataRequest
// GetMetadataResponse provided by the plugin.
type GetMetadataResponse struct {
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
URL string `json:"url"`
SupportedContractVersions []string `json:"supportedContractVersions"`
Capabilities []Capability `json:"capabilities"`
}
// HasCapability return true if the metadata states that the
// capability is supported.
// Returns true if capability is empty.
func (resp *GetMetadataResponse) HasCapability(capability Capability) bool {
if capability == "" {
return true
}
for _, c := range resp.Capabilities {
if c == capability {
return true
}
}
return false
}
//
// Deprecated: GetMetadataResponse exists for historical compatibility and should not be used.
// To access GetMetadataResponse, use the notation-plugin-framework-go's [plugin.GetMetadataResponse] type.
type GetMetadataResponse = plugin.GetMetadataResponse

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proto
import (

View File

@ -1,65 +1,120 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package proto defines the protocol layer for communication between notation
// and notation external plugin.
package proto
import "github.com/notaryproject/notation-plugin-framework-go/plugin"
// Prefix is the prefix required on all plugin binary names.
const Prefix = "notation-"
//
// Deprecated: Prefix exists for historical compatibility and should not be used.
// To access Prefix, use the notation-plugin-framework-go's [plugin.BinaryPrefix] type.
const Prefix = plugin.BinaryPrefix
// ContractVersion is the <major>.<minor> version of the plugin contract.
const ContractVersion = "1.0"
//
// Deprecated: ContractVersion exists for historical compatibility and should not be used.
// To access ContractVersion, use the notation-plugin-framework-go's [plugin.ContractVersion] type.
const ContractVersion = plugin.ContractVersion
// Command is a CLI command available in the plugin contract.
type Command string
//
// Deprecated: Command exists for historical compatibility and should not be used.
// To access Command, use the notation-plugin-framework-go's [plugin.Command] type.
type Command = plugin.Command
// Request defines a plugin request, which is always associated to a command.
type Request interface {
Command() Command
}
//
// Deprecated: Request exists for historical compatibility and should not be used.
// To access Request, use the notation-plugin-framework-go's [plugin.Request] type.
type Request = plugin.Request
const (
// CommandGetMetadata is the name of the plugin command
// which must be supported by every plugin and returns the
// plugin metadata.
CommandGetMetadata Command = "get-plugin-metadata"
//
// Deprecated: CommandGetMetadata exists for historical compatibility and should not be used.
// To access CommandGetMetadata, use the notation-plugin-framework-go's [plugin.CommandGetMetadata].
CommandGetMetadata = plugin.CommandGetMetadata
// CommandDescribeKey is the name of the plugin command
// which must be supported by every plugin that has the
// SIGNATURE_GENERATOR.RAW capability.
CommandDescribeKey Command = "describe-key"
//
// Deprecated: CommandDescribeKey exists for historical compatibility and should not be used.
// To access CommandDescribeKey, use the notation-plugin-framework-go's [plugin.CommandDescribeKey].
CommandDescribeKey = plugin.CommandDescribeKey
// CommandGenerateSignature is the name of the plugin command
// which must be supported by every plugin that has the
// SIGNATURE_GENERATOR.RAW capability.
CommandGenerateSignature Command = "generate-signature"
//
// Deprecated: CommandGenerateSignature exists for historical compatibility and should not be used.
// To access CommandGenerateSignature, use the notation-plugin-framework-go's [plugin.CommandGenerateSignature].
CommandGenerateSignature = plugin.CommandGenerateSignature
// CommandGenerateEnvelope is the name of the plugin command
// which must be supported by every plugin that has the
// SIGNATURE_GENERATOR.ENVELOPE capability.
CommandGenerateEnvelope Command = "generate-envelope"
//
// Deprecated: CommandGenerateEnvelope exists for historical compatibility and should not be used.
// To access CommandGenerateEnvelope, use the notation-plugin-framework-go's [plugin.CommandGenerateEnvelope].
CommandGenerateEnvelope = plugin.CommandGenerateEnvelope
// CommandVerifySignature is the name of the plugin command
// which must be supported by every plugin that has
// any SIGNATURE_VERIFIER.* capability
CommandVerifySignature Command = "verify-signature"
//
// Deprecated: CommandVerifySignature exists for historical compatibility and should not be used.
// To access CommandVerifySignature, use the notation-plugin-framework-go's [plugin.CommandVerifySignature].
CommandVerifySignature = plugin.CommandVerifySignature
)
// Capability is a feature available in the plugin contract.
type Capability string
//
// Deprecated: Capability exists for historical compatibility and should not be used.
// To access Capability, use the notation-plugin-framework-go's [plugin.Capability] type.
type Capability = plugin.Capability
const (
// CapabilitySignatureGenerator is the name of the capability
// for a plugin to support generating raw signatures.
CapabilitySignatureGenerator Capability = "SIGNATURE_GENERATOR.RAW"
//
// Deprecated: CapabilitySignatureGenerator exists for historical compatibility and should not be used.
// To access CapabilitySignatureGenerator, use the notation-plugin-framework-go's [plugin.CapabilitySignatureGenerator].
CapabilitySignatureGenerator = plugin.CapabilitySignatureGenerator
// CapabilityEnvelopeGenerator is the name of the capability
// for a plugin to support generating envelope signatures.
CapabilityEnvelopeGenerator Capability = "SIGNATURE_GENERATOR.ENVELOPE"
//
// Deprecated: CapabilityEnvelopeGenerator exists for historical compatibility and should not be used.
// To access CapabilityEnvelopeGenerator, use the notation-plugin-framework-go's [plugin.CapabilityEnvelopeGenerator].
CapabilityEnvelopeGenerator = plugin.CapabilityEnvelopeGenerator
// CapabilityTrustedIdentityVerifier is the name of the
// capability for a plugin to support verifying trusted identities.
CapabilityTrustedIdentityVerifier Capability = "SIGNATURE_VERIFIER.TRUSTED_IDENTITY"
//
// Deprecated: CapabilityTrustedIdentityVerifier exists for historical compatibility and should not be used.
// To access CapabilityTrustedIdentityVerifier, use the notation-plugin-framework-go's [plugin.CapabilityTrustedIdentityVerifier].
CapabilityTrustedIdentityVerifier = plugin.CapabilityTrustedIdentityVerifier
// CapabilityRevocationCheckVerifier is the name of the
// capability for a plugin to support verifying revocation checks.
CapabilityRevocationCheckVerifier Capability = "SIGNATURE_VERIFIER.REVOCATION_CHECK"
//
// Deprecated: CapabilityRevocationCheckVerifier exists for historical compatibility and should not be used.
// To access CapabilityRevocationCheckVerifier, use the notation-plugin-framework-go's [plugin.CapabilityRevocationCheckVerifier].
CapabilityRevocationCheckVerifier = plugin.CapabilityRevocationCheckVerifier
)

View File

@ -1,71 +1,54 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proto
// DescribeKeyRequest contains the parameters passed in a describe-key request.
type DescribeKeyRequest struct {
ContractVersion string `json:"contractVersion"`
KeyID string `json:"keyId"`
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
}
import "github.com/notaryproject/notation-plugin-framework-go/plugin"
func (DescribeKeyRequest) Command() Command {
return CommandDescribeKey
}
// DescribeKeyRequest contains the parameters passed in a describe-key request.
//
// Deprecated: DescribeKeyRequest exists for historical compatibility and should not be used.
// To access DescribeKeyRequest, use the notation-plugin-framework-go's [plugin.DescribeKeyRequest] type.
type DescribeKeyRequest = plugin.DescribeKeyRequest
// DescribeKeyResponse is the response of a describe-key request.
type DescribeKeyResponse struct {
// The same key id as passed in the request.
KeyID string `json:"keyId"`
// One of following supported key types:
// https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
KeySpec KeySpec `json:"keySpec"`
}
//
// Deprecated: DescribeKeyResponse exists for historical compatibility and should not be used.
// To access DescribeKeyResponse, use the notation-plugin-framework-go's [plugin.DescribeKeyResponse] type.
type DescribeKeyResponse = plugin.DescribeKeyResponse
// GenerateSignatureRequest contains the parameters passed in a
// generate-signature request.
type GenerateSignatureRequest struct {
ContractVersion string `json:"contractVersion"`
KeyID string `json:"keyId"`
KeySpec KeySpec `json:"keySpec"`
Hash HashAlgorithm `json:"hashAlgorithm"`
Payload []byte `json:"payload"`
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
}
func (GenerateSignatureRequest) Command() Command {
return CommandGenerateSignature
}
//
// Deprecated: GenerateSignatureRequest exists for historical compatibility and should not be used.
// To access GenerateSignatureRequest, use the notation-plugin-framework-go's [plugin.GenerateSignatureRequest] type.
type GenerateSignatureRequest = plugin.GenerateSignatureRequest
// GenerateSignatureResponse is the response of a generate-signature request.
type GenerateSignatureResponse struct {
KeyID string `json:"keyId"`
Signature []byte `json:"signature"`
SigningAlgorithm string `json:"signingAlgorithm"`
// Ordered list of certificates starting with leaf certificate
// and ending with root certificate.
CertificateChain [][]byte `json:"certificateChain"`
}
//
// Deprecated: GenerateSignatureResponse exists for historical compatibility and should not be used.
// To access GenerateSignatureResponse, use the notation-plugin-framework-go's [plugin.GenerateSignatureResponse] type.
type GenerateSignatureResponse = plugin.GenerateSignatureResponse
// GenerateEnvelopeRequest contains the parameters passed in a generate-envelope
// request.
type GenerateEnvelopeRequest struct {
ContractVersion string `json:"contractVersion"`
KeyID string `json:"keyId"`
PayloadType string `json:"payloadType"`
SignatureEnvelopeType string `json:"signatureEnvelopeType"`
Payload []byte `json:"payload"`
ExpiryDurationInSeconds uint64 `json:"expiryDurationInSeconds,omitempty"`
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
}
func (GenerateEnvelopeRequest) Command() Command {
return CommandGenerateEnvelope
}
//
// Deprecated: GenerateEnvelopeRequest exists for historical compatibility and should not be used.
// To access GenerateEnvelopeRequest, use the notation-plugin-framework-go's [plugin.GenerateEnvelopeRequest] type.
type GenerateEnvelopeRequest = plugin.GenerateEnvelopeRequest
// GenerateEnvelopeResponse is the response of a generate-envelope request.
type GenerateEnvelopeResponse struct {
SignatureEnvelope []byte `json:"signatureEnvelope"`
SignatureEnvelopeType string `json:"signatureEnvelopeType"`
Annotations map[string]string `json:"annotations,omitempty"`
}
//
// Deprecated: GenerateEnvelopeResponse exists for historical compatibility and should not be used.
// To access GenerateEnvelopeResponse, use the notation-plugin-framework-go's [plugin.GenerateEnvelopeResponse] type.
type GenerateEnvelopeResponse = plugin.GenerateEnvelopeResponse

View File

@ -1,51 +1,56 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package proto
import "time"
import (
"github.com/notaryproject/notation-plugin-framework-go/plugin"
)
// VerifySignatureRequest contains the parameters passed in a verify-signature
// request.
type VerifySignatureRequest struct {
ContractVersion string `json:"contractVersion"`
Signature Signature `json:"signature"`
TrustPolicy TrustPolicy `json:"trustPolicy"`
PluginConfig map[string]string `json:"pluginConfig,omitempty"`
}
func (VerifySignatureRequest) Command() Command {
return CommandVerifySignature
}
//
// Deprecated: VerifySignatureRequest exists for historical compatibility and should not be used.
// To access VerifySignatureRequest, use the notation-plugin-framework-go'[s plugin.VerifySignatureRequest] type.
type VerifySignatureRequest = plugin.VerifySignatureRequest
// Signature represents a signature pulled from the envelope
type Signature struct {
CriticalAttributes CriticalAttributes `json:"criticalAttributes"`
UnprocessedAttributes []string `json:"unprocessedAttributes"`
CertificateChain [][]byte `json:"certificateChain"`
}
//
// Deprecated: Signature exists for historical compatibility and should not be used.
// To access Signature, use the notation-plugin-framework-go's [plugin.Signature] type.
type Signature = plugin.Signature
// CriticalAttributes contains all Notary Project defined critical
// attributes and their values in the signature envelope
type CriticalAttributes struct {
ContentType string `json:"contentType"`
SigningScheme string `json:"signingScheme"`
Expiry *time.Time `json:"expiry,omitempty"`
AuthenticSigningTime *time.Time `json:"authenticSigningTime,omitempty"`
ExtendedAttributes map[string]interface{} `json:"extendedAttributes,omitempty"`
}
//
// Deprecated: CriticalAttributes exists for historical compatibility and should not be used.
// To access CriticalAttributes, use the notation-plugin-framework-go's [plugin.CriticalAttributes] type.
type CriticalAttributes = plugin.CriticalAttributes
// TrustPolicy represents trusted identities that sign the artifacts
type TrustPolicy struct {
TrustedIdentities []string `json:"trustedIdentities"`
SignatureVerification []Capability `json:"signatureVerification"`
}
//
// Deprecated: TrustPolicy exists for historical compatibility and should not be used.
// To access TrustPolicy, use the notation-plugin-framework-go's [plugin.TrustPolicy] type.
type TrustPolicy = plugin.TrustPolicy
// VerifySignatureResponse is the response of a verify-signature request.
type VerifySignatureResponse struct {
VerificationResults map[Capability]*VerificationResult `json:"verificationResults"`
ProcessedAttributes []interface{} `json:"processedAttributes"`
}
//
// Deprecated: VerifySignatureResponse exists for historical compatibility and should not be used.
// To access VerifySignatureResponse, use the notation-plugin-framework-go's [plugin.VerifySignatureResponse] type.
type VerifySignatureResponse = plugin.VerifySignatureResponse
// VerificationResult is the result of a verification performed by the plugin
type VerificationResult struct {
Success bool `json:"success"`
Reason string `json:"reason,omitempty"`
}
// VerificationResult is the result of a verification performed by the plugin.
//
// Deprecated: VerificationResult exists for historical compatibility and should not be used.
// To access VerificationResult, use the notation-plugin-framework-go's [plugin.VerificationResult] type.
type VerificationResult = plugin.VerificationResult

0
plugin/testdata/plugins/foo/libfoo vendored Normal file
View File

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package registry provides access to signatures in a registry
package registry

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package artifactspec
import ocispec "github.com/opencontainers/image-spec/specs-go/v1"

View File

@ -1,5 +1,18 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
// ArtifactTypeNotation specifies the artifact type for a notation object.
// spec: https://github.com/notaryproject/notaryproject/blob/efc828223710f99ab9639d2d0f72d59036a8e80c/specs/signature-specification.md#storage
// spec: https://github.com/notaryproject/specifications/blob/v1.1.0/specs/signature-specification.md#signature
const ArtifactTypeNotation = "application/vnd.cncf.notary.signature"

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
@ -6,6 +19,7 @@ import (
"fmt"
"os"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/registry/internal/artifactspec"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2"
@ -19,17 +33,17 @@ const (
maxManifestSizeLimit = 4 * 1024 * 1024 // 4 MiB
)
// RepositoryOptions provides user options when creating a Repository
// RepositoryOptions provides user options when creating a [Repository]
// it is kept for future extensibility
type RepositoryOptions struct{}
// repositoryClient implements Repository
// repositoryClient implements [Repository]
type repositoryClient struct {
oras.GraphTarget
RepositoryOptions
}
// NewRepository returns a new Repository.
// NewRepository returns a new [Repository].
// Known implementations of oras.GraphTarget:
// - [remote.Repository](https://pkg.go.dev/oras.land/oras-go/v2/registry/remote#Repository)
// - [oci.Store](https://pkg.go.dev/oras.land/oras-go/v2/content/oci#Store)
@ -39,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{
@ -48,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)
@ -79,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)
@ -97,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()
@ -150,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
@ -164,28 +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) {
opts := oras.PackOptions{
opts := oras.PackManifestOptions{
Subject: &subject,
ManifestAnnotations: annotations,
PackImageManifest: true,
Layers: []ocispec.Descriptor{blobDesc},
}
return oras.Pack(ctx, c.GraphTarget, ArtifactTypeNotation, []ocispec.Descriptor{blobDesc}, opts)
return oras.PackManifest(ctx, c.GraphTarget, oras.PackManifestVersion1_1, ArtifactTypeNotation, opts)
}
// signatureReferrers returns referrer nodes of desc in target filtered by
// the "application/vnd.cncf.notary.signature" artifact type
func signatureReferrers(ctx context.Context, target content.ReadOnlyGraphStorage, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
logger := log.GetLogger(ctx)
var results []ocispec.Descriptor
predecessors, err := target.Predecessors(ctx, desc)
if err != nil {
@ -201,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
@ -208,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:
@ -218,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
@ -225,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

@ -1,8 +1,22 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package registry
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"net/http"
@ -10,21 +24,26 @@ import (
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"github.com/notaryproject/notation-go/internal/envelope"
"github.com/notaryproject/notation-go/internal/mock/ocilayout"
"github.com/notaryproject/notation-go/internal/slices"
"github.com/notaryproject/notation-go/registry/internal/artifactspec"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
"oras.land/oras-go/v2/content/memory"
"oras.land/oras-go/v2/content/oci"
"oras.land/oras-go/v2/registry"
"oras.land/oras-go/v2/registry/remote"
)
const (
zeroDigest = "sha256:0000000000000000000000000000000000000000000000000000000000000000"
emptyConfigDigest = "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
validDigest = "6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b"
validDigest2 = "1834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f2"
invalidDigest = "invaliddigest"
@ -48,7 +67,7 @@ const (
{
"Manifests": [
{
"MediaType": "application/vnd.oci.artifact.manifest.v1+json",
"MediaType": "application/vnd.oci.image.manifest.v1+json",
"Digest": "sha256:cf2a0974295fc17b8351ef52abae2f40212e20e0359ea980ec5597bb0315347b",
"Size": 620,
"ArtifactType": "application/vnd.cncf.notary.signature"
@ -116,8 +135,13 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
"Docker-Content-Digest": {validDigestWithAlgo2},
},
}, nil
case "/v2/test/blobs/" + emptyConfigDigest:
return &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(bytes.NewReader([]byte{})),
}, nil
case "/v2/test/manifests/" + invalidDigest:
return &http.Response{}, fmt.Errorf(errMsg)
return &http.Response{}, errors.New(errMsg)
case "v2/test/manifest/" + validDigest2:
return &http.Response{
StatusCode: http.StatusOK,
@ -141,21 +165,39 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
},
}, nil
default:
return &http.Response{}, fmt.Errorf(msg)
return &http.Response{}, errors.New(msg)
}
case "/v2/test/referrers/":
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte(validPage))),
Header: http.Header{
"Content-Type": []string{ocispec.MediaTypeImageIndex},
},
Body: io.NopCloser(bytes.NewReader([]byte(validPage))),
Request: &http.Request{
Method: "GET",
URL: &url.URL{Path: "/v2/test/referrers/"},
},
}, nil
case "/v2/test/referrers/" + validDigestWithAlgo:
return &http.Response{
StatusCode: http.StatusOK,
Header: http.Header{
"Content-Type": []string{ocispec.MediaTypeImageIndex},
},
Body: io.NopCloser(bytes.NewReader([]byte(validPage))),
Request: &http.Request{
Method: "GET",
URL: &url.URL{Path: "/v2/test/referrers/" + validDigestWithAlgo},
},
}, nil
case "/v2/test/referrers/" + zeroDigest:
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader([]byte(validPageImage))),
Header: http.Header{
"Content-Type": []string{ocispec.MediaTypeImageIndex},
},
Body: io.NopCloser(bytes.NewReader([]byte(validPageImage))),
Request: &http.Request{
Method: "GET",
URL: &url.URL{Path: "/v2/test/referrers/" + zeroDigest},
@ -169,15 +211,17 @@ func (c mockRemoteClient) Do(req *http.Request) (*http.Response, error) {
default:
_, digest, found := strings.Cut(req.URL.Path, "/v2/test/manifests/")
if found && !slices.Contains(validDigestWithAlgoSlice, digest) {
return &http.Response{
resp := &http.Response{
StatusCode: http.StatusCreated,
Body: io.NopCloser(bytes.NewReader([]byte(msg))),
Header: map[string][]string{
"Content-Type": {joseTag},
Header: http.Header{
"Content-Type": []string{joseTag},
"Oci-Subject": []string{validDigestWithAlgo},
},
}, nil
}
return resp, nil
}
return &http.Response{}, fmt.Errorf(errMsg)
return &http.Response{}, errors.New(errMsg)
}
}
@ -288,16 +332,24 @@ func TestListSignatures(t *testing.T) {
reference: validReference,
remoteClient: mockRemoteClient{},
plainHttp: false,
artifactManifestDesc: ocispec.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: validDigestWithAlgo,
Size: 481,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args := tt.args
ref, _ := registry.ParseReference(args.reference)
ref, err := registry.ParseReference(args.reference)
if err != nil {
t.Fatal(err)
}
client := newRepositoryClient(args.remoteClient, ref, args.plainHttp)
err := client.ListSignatures(args.ctx, args.artifactManifestDesc, func(signatureManifests []ocispec.Descriptor) error {
err = client.ListSignatures(args.ctx, args.artifactManifestDesc, func(signatureManifests []ocispec.Descriptor) error {
if len(signatureManifests) != 1 {
return fmt.Errorf("length of signatureManifests expected 1, got %d", len(signatureManifests))
}
@ -348,6 +400,11 @@ func TestPushSignature(t *testing.T) {
signature: signature,
ctx: context.Background(),
remoteClient: mockRemoteClient{},
subjectManifest: ocispec.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
Digest: validDigestWithAlgo,
Size: 481,
},
annotations: map[string]string{
envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]",
},
@ -412,7 +469,6 @@ func newRepositoryClientWithImageManifest(client remote.Client, ref registry.Ref
}
var (
ociLayoutPath = filepath.FromSlash("../internal/testdata/oci-layout")
reference = "sha256:19dbd2e48e921426ee8ace4dc892edfb2ecdc1d1a72d5416c83670c30acecef0"
expectedTargetDesc = ocispec.Descriptor{
MediaType: "application/vnd.oci.image.manifest.v1+json",
@ -425,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,
@ -435,11 +491,23 @@ var (
}
)
func TestOciLayoutRepositoryResolveAndPush(t *testing.T) {
repo, err := NewOCIRepository(ociLayoutPath, RepositoryOptions{})
func TestOciLayoutRepositoryPushAndFetch(t *testing.T) {
// create a temp OCI layout
ociLayoutTestdataPath, err := filepath.Abs(filepath.Join("..", "internal", "testdata", "oci-layout"))
if err != nil {
t.Fatalf("failed to get oci layout path: %v", err)
}
newOCILayoutPath := t.TempDir()
if err := ocilayout.Copy(ociLayoutTestdataPath, newOCILayoutPath, "v2"); err != nil {
t.Fatalf("failed to create temp oci layout: %v", err)
}
repo, err := NewOCIRepository(newOCILayoutPath, RepositoryOptions{})
if err != nil {
t.Fatalf("failed to create oci.Store as registry.Repository: %v", err)
}
// test resolve
targetDesc, err := repo.Resolve(context.Background(), reference)
if err != nil {
t.Fatalf("failed to resolve reference: %v", err)
@ -447,59 +515,515 @@ func TestOciLayoutRepositoryResolveAndPush(t *testing.T) {
if !content.Equal(targetDesc, expectedTargetDesc) {
t.Fatalf("failed to resolve reference. expected descriptor: %v, but got: %v", expectedTargetDesc, targetDesc)
}
signature, err := os.ReadFile(signaturePath)
if err != nil {
t.Fatalf("failed to read signature: %v", err)
}
_, signatureManifestDesc, err := repo.PushSignature(context.Background(), joseTag, signature, targetDesc, annotations)
if err != nil {
t.Fatalf("failed to push signature: %v", err)
}
if !content.Equal(expectedSignatureManifestDesc, signatureManifestDesc) {
t.Fatalf("expected desc: %v, got: %v", expectedSignatureManifestDesc, signatureManifestDesc)
}
expectedAnnotations := map[string]string{
envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]",
ocispec.AnnotationCreated: "2023-03-14T08:10:02Z",
}
if !reflect.DeepEqual(expectedAnnotations, signatureManifestDesc.Annotations) {
t.Fatalf("expected annotations: %v, but got: %v", expectedAnnotations, signatureManifestDesc.Annotations)
}
t.Run("oci layout push", func(t *testing.T) {
signature, err := os.ReadFile(signaturePath)
if err != nil {
t.Fatalf("failed to read signature: %v", err)
}
_, signatureManifestDesc, err := repo.PushSignature(context.Background(), joseTag, signature, targetDesc, annotations)
if err != nil {
t.Fatalf("failed to push signature: %v", err)
}
if !content.Equal(expectedSignatureManifestDesc, signatureManifestDesc) {
t.Fatalf("expected desc: %v, got: %v", expectedSignatureManifestDesc, signatureManifestDesc)
}
expectedAnnotations := map[string]string{
envelope.AnnotationX509ChainThumbprint: "[\"9f5f5aecee24b5cfdc7a91f6d5ac5c3a5348feb17c934d403f59ac251549ea0d\"]",
ocispec.AnnotationCreated: "2023-03-14T08:10:02Z",
}
if !reflect.DeepEqual(expectedAnnotations, signatureManifestDesc.Annotations) {
t.Fatalf("expected annotations: %v, but got: %v", expectedAnnotations, signatureManifestDesc.Annotations)
}
})
t.Run("oci layout fetch", func(t *testing.T) {
err = repo.ListSignatures(context.Background(), targetDesc, func(signatureManifests []ocispec.Descriptor) error {
if len(signatureManifests) == 0 {
return fmt.Errorf("expected to find signature in the OCI layout folder, but got none")
}
var found bool
for _, sigManifestDesc := range signatureManifests {
if !content.Equal(sigManifestDesc, expectedSignatureManifestDesc) {
continue
}
_, sigDesc, err := repo.FetchSignatureBlob(context.Background(), sigManifestDesc)
if err != nil {
return fmt.Errorf("failed to fetch blob: %w", err)
}
if !content.Equal(expectedSignatureBlobDesc, sigDesc) {
return fmt.Errorf("expected to get signature blob desc: %v, got: %v", expectedSignatureBlobDesc, sigDesc)
}
found = true
}
if !found {
return fmt.Errorf("expected to find the signature with manifest desc: %v, but failed", expectedSignatureManifestDesc)
}
return nil
})
if err != nil {
t.Fatal(err)
}
})
}
func TestOciLayoutRepositoryListAndFetchBlob(t *testing.T) {
repo, err := NewOCIRepository(ociLayoutPath, RepositoryOptions{})
func TestNewRepository(t *testing.T) {
target, err := oci.New(t.TempDir())
if err != nil {
t.Fatalf("failed to create oci.Store as registry.Repository: %v", err)
}
targetDesc, err := repo.Resolve(context.Background(), reference)
if err != nil {
t.Fatalf("failed to resolve reference: %v", err)
repo := NewRepository(target)
if repo == nil {
t.Fatalf("failed to create repository")
}
err = repo.ListSignatures(context.Background(), targetDesc, func(signatureManifests []ocispec.Descriptor) error {
if len(signatureManifests) == 0 {
return fmt.Errorf("expected to find signature in the OCI layout folder, but got none")
}
var found bool
for _, sigManifestDesc := range signatureManifests {
if !content.Equal(sigManifestDesc, expectedSignatureManifestDesc) {
continue
}
_, sigDesc, err := repo.FetchSignatureBlob(context.Background(), sigManifestDesc)
if err != nil {
return fmt.Errorf("failed to fetch blob: %w", err)
}
if !content.Equal(expectedSignatureBlobDesc, sigDesc) {
return fmt.Errorf("expected to get signature blob desc: %v, got: %v", expectedSignatureBlobDesc, sigDesc)
}
found = true
}
if !found {
return fmt.Errorf("expected to find the signature with manifest desc: %v, but failed", expectedSignatureManifestDesc)
}
return nil
})
if err != nil {
t.Fatal(err)
repoClient, ok := repo.(*repositoryClient)
if !ok {
t.Fatalf("failed to create repositoryClient")
}
if target != repoClient.GraphTarget {
t.Fatalf("expected target: %v, got: %v", target, repoClient.GraphTarget)
}
}
func TestNewOCIRepositoryFailed(t *testing.T) {
t.Run("os stat failed", func(t *testing.T) {
_, err := NewOCIRepository("invalid-path", RepositoryOptions{})
if err == nil {
t.Fatalf("expected to fail with invalid path")
}
})
t.Run("path is regular file", func(t *testing.T) {
// create a regular file in the temp dir
filePath := filepath.Join(t.TempDir(), "file")
file, err := os.Create(filePath)
if err != nil {
t.Fatalf("failed to create file: %v", err)
}
file.Close()
_, err = NewOCIRepository(filePath, RepositoryOptions{})
if err == nil {
t.Fatalf("expected to fail with regular file")
}
})
t.Run("no permission to create new path", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on Windows")
}
// create a directory in the temp dir
dirPath := filepath.Join(t.TempDir(), "dir")
err := os.Mkdir(dirPath, 0000)
if err != nil {
t.Fatalf("failed to create dir: %v", err)
}
_, err = NewOCIRepository(dirPath, RepositoryOptions{})
if err == nil {
t.Fatalf("expected to fail with no permission to create new path")
}
})
}
// testStorage implements content.ReadOnlyGraphStorage
type testStorage struct {
store *memory.Store
FetchError error
FetchContent []byte
PredecessorsError error
PredecessorsDesc []ocispec.Descriptor
}
func (s *testStorage) Push(ctx context.Context, expected ocispec.Descriptor, reader io.Reader) error {
return s.store.Push(ctx, expected, reader)
}
func (s *testStorage) Fetch(ctx context.Context, target ocispec.Descriptor) (io.ReadCloser, error) {
if s.FetchError != nil {
return nil, s.FetchError
}
return io.NopCloser(bytes.NewReader(s.FetchContent)), nil
}
func (s *testStorage) Exists(ctx context.Context, target ocispec.Descriptor) (bool, error) {
return s.store.Exists(ctx, target)
}
func (s *testStorage) Predecessors(ctx context.Context, node ocispec.Descriptor) ([]ocispec.Descriptor, error) {
if s.PredecessorsError != nil {
return nil, s.PredecessorsError
}
return s.PredecessorsDesc, nil
}
func TestSignatureReferrers(t *testing.T) {
t.Run("get predecessors failed", func(t *testing.T) {
store := &testStorage{
store: &memory.Store{},
PredecessorsError: fmt.Errorf("failed to get predecessors"),
}
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{})
if err == nil {
t.Fatalf("expected to fail with getting predecessors")
}
})
t.Run("artifact manifest exceds max blob size", func(t *testing.T) {
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{
{
Digest: validDigestWithAlgo2,
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Size: 4*1024*1024 + 1,
},
},
}
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: validDigestWithAlgo2,
})
if err == nil {
t.Fatalf("expected to fail with artifact manifest exceds max blob size")
}
})
t.Run("image manifest exceds max blob size", func(t *testing.T) {
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{
{
Digest: validDigestWithAlgo2,
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 4*1024*1024 + 1,
},
},
}
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: validDigestWithAlgo2,
})
if err == nil {
t.Fatalf("expected to fail with image manifest exceds max blob size")
}
})
t.Run("artifact manifest fetchAll failed", func(t *testing.T) {
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{
{
Digest: validDigestWithAlgo,
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Size: 481,
},
},
FetchError: fmt.Errorf("failed to fetch all"),
}
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: validDigestWithAlgo,
})
if err == nil {
t.Fatalf("expected to fail with fetchAll failed")
}
})
t.Run("image manifest fetchAll failed", func(t *testing.T) {
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{
{
Digest: validDigestWithAlgo,
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 481,
},
},
FetchError: fmt.Errorf("failed to fetch all"),
}
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: validDigestWithAlgo,
})
if err == nil {
t.Fatalf("expected to fail with fetchAll failed")
}
})
t.Run("artifact manifest marshal failed", func(t *testing.T) {
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{
{
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Size: 15,
},
},
FetchContent: []byte("invalid content"),
}
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
})
if err == nil {
t.Fatalf("expected to fail with marshal failed")
}
})
t.Run("image manifest marshal failed", func(t *testing.T) {
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{
{
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 15,
},
},
FetchContent: []byte("invalid content"),
}
_, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:24aafc739daae02bcd33471a1b28bcfaaef0bb5e530ef44cd4e5d2445e606690",
})
if err == nil {
t.Fatalf("expected to fail with marshal failed")
}
})
t.Run("no valid artifact manifest", func(t *testing.T) {
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{
{
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Size: 2,
},
},
FetchContent: []byte("{}"),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
}
})
t.Run("artifact manifest with invalid artifactType", func(t *testing.T) {
sigManifest := `{"artifactType":"invalid", "subject":{"mediaType":"application/vnd.oci.artifact.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:835c3386406350fbddf5ee376b358bd20c6c423d6becbec166f83c533e4df5d6",
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Size: 198,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.artifact.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
}
})
t.Run("no valid image manifest", func(t *testing.T) {
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{
{
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
},
},
FetchContent: []byte("{}"),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatalf("expected to get no referrers, but got: %v", descriptors)
}
})
t.Run("image manifest with invalid mediaType", func(t *testing.T) {
sigManifest := `{}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "invalid",
Size: 2,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatal("expected length of descriptors to be 0")
}
})
t.Run("image manifest with valid artifactType and config.MediaType", func(t *testing.T) {
sigManifest := `{"artifactType":"application/vnd.cncf.notary.signature","config":{"mediaType":"application/vnd.oci.empty.v1+json"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:ad3ab7874c72d7bf5db0e55ce839b37ee71320bf7c18ac1a512600963f03c54d",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 283,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 1 {
t.Fatal("expected length of descriptors to be 1")
}
if !content.Equal(sigManifestDesc, descriptors[0]) {
t.Fatalf("expected %v, got: %v", sigManifestDesc, descriptors[0])
}
})
t.Run("image manifest with valid artifactType but invalid config.MediaType", func(t *testing.T) {
sigManifest := `{"artifactType":"application/vnd.cncf.notary.signature","config":{"mediaType":"invalid"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:becfe1975b40352d0c7bd1337707a4c471fdcfa1ac380f2875fe8076a3bc3581",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 257,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatal("expected length of descriptors to be 0")
}
})
t.Run("image manifest with no artifactType and valid config.MediaType", func(t *testing.T) {
sigManifest := `{"config":{"mediaType":"application/vnd.cncf.notary.signature"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:0e0be61f687ba634dd772f6d3048101f78f22fabda64cc9600671cee41ab2d47",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 232,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 1 {
t.Fatal("expected length of descriptors to be 1")
}
if !content.Equal(sigManifestDesc, descriptors[0]) {
t.Fatalf("expected %v, got: %v", sigManifestDesc, descriptors[0])
}
})
t.Run("image manifest with no artifactType and invalid config.MediaType", func(t *testing.T) {
sigManifest := `{"config":{"mediaType":"invalid"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:1580e4f590269bd40a33e902888429c9bbb250902f5a7eb50f04fbb8bd4dbab3",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 202,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatal("expected length of descriptors to be 0")
}
})
t.Run("image manifest with invalid artifactType", func(t *testing.T) {
sigManifest := `{"artifactType":"invalid","config":{"mediaType":"application/vnd.oci.empty.v1+json"},"subject":{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2}}`
sigManifestDesc := ocispec.Descriptor{
Digest: "sha256:d8c225cb4eca3e15fa2a44c9d302044e8c8683399939e26f417edb82f8b69cc3",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 253,
}
store := &testStorage{
store: &memory.Store{},
PredecessorsDesc: []ocispec.Descriptor{sigManifestDesc},
FetchContent: []byte(sigManifest),
}
descriptors, err := signatureReferrers(context.Background(), store, ocispec.Descriptor{
Digest: "sha256:sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
MediaType: "application/vnd.oci.image.manifest.v1+json",
Size: 2,
})
if err != nil {
t.Fatalf("failed to get referrers: %v", err)
}
if len(descriptors) != 0 {
t.Fatal("expected length of descriptors to be 0")
}
})
}
func TestUploadSignatureManifest(t *testing.T) {
ref, err := registry.ParseReference(validReference)
if err != nil {
t.Fatalf("failed to parse reference")
}
client := newRepositoryClientWithImageManifest(mockRemoteClient{}, ref, false)
manifest, err := client.uploadSignatureManifest(context.Background(),
ocispec.Descriptor{}, ocispec.Descriptor{}, nil)
if err != nil {
t.Fatalf("failed to upload signature manifest: %v", err)
}
if manifest.ArtifactType != ArtifactTypeNotation {
t.Fatalf("expected artifact type: %s, got: %s", ArtifactTypeNotation, manifest.ArtifactType)
}
}

View File

@ -1,44 +1,76 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package signer
import (
"context"
"crypto"
"crypto/x509"
"encoding/json"
"errors"
"fmt"
"time"
"oras.land/oras-go/v2/content"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/internal/envelope"
"github.com/notaryproject/notation-go/log"
"github.com/notaryproject/notation-go/plugin"
"github.com/notaryproject/notation-go/plugin/proto"
"github.com/notaryproject/notation-plugin-framework-go/plugin"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/v2/content"
)
// pluginSigner signs artifacts and generates signatures.
// It implements notation.Signer
type pluginSigner struct {
// PluginSigner signs artifacts and generates signatures.
//
// It implements [notation.Signer] and [notation.BlobSigner].
type PluginSigner struct {
plugin plugin.SignPlugin
keyID string
pluginConfig map[string]string
manifestAnnotations map[string]string
}
// NewFromPlugin creates a notation.Signer that signs artifacts and generates
var algorithms = map[crypto.Hash]digest.Algorithm{
crypto.SHA256: digest.SHA256,
crypto.SHA384: digest.SHA384,
crypto.SHA512: digest.SHA512,
}
// NewFromPlugin creates a [PluginSigner] that signs artifacts and generates
// signatures by delegating the one or more operations to the named plugin,
// as defined in https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md#signing-interfaces.
func NewFromPlugin(plugin plugin.Plugin, keyID string, pluginConfig map[string]string) (notation.Signer, error) {
//
// Deprecated: NewFromPlugin function exists for historical compatibility and
// should not be used. To create [PluginSigner], use NewPluginSigner() function.
func NewFromPlugin(plugin plugin.SignPlugin, keyID string, pluginConfig map[string]string) (notation.Signer, error) {
return NewPluginSigner(plugin, keyID, pluginConfig)
}
// NewPluginSigner creates a [PluginSigner] that signs artifacts and generates
// signatures by delegating the one or more operations to the named plugin,
// as defined in https://github.com/notaryproject/notaryproject/blob/main/specs/plugin-extensibility.md#signing-interfaces.
func NewPluginSigner(plugin plugin.SignPlugin, keyID string, pluginConfig map[string]string) (*PluginSigner, error) {
if plugin == nil {
return nil, errors.New("nil plugin")
}
if keyID == "" {
return nil, errors.New("keyID not specified")
}
return &pluginSigner{
return &PluginSigner{
plugin: plugin,
keyID: keyID,
pluginConfig: pluginConfig,
@ -46,66 +78,103 @@ func NewFromPlugin(plugin plugin.Plugin, keyID string, pluginConfig map[string]s
}
// PluginAnnotations returns signature manifest annotations returned from plugin
func (s *pluginSigner) PluginAnnotations() map[string]string {
func (s *PluginSigner) PluginAnnotations() map[string]string {
return s.manifestAnnotations
}
// Sign signs the artifact described by its descriptor and returns the
// marshalled envelope.
func (s *pluginSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
// signature and SignerInfo.
func (s *PluginSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
logger := log.GetLogger(ctx)
mergedConfig := s.mergeConfig(opts.PluginConfig)
logger.Debug("Invoking plugin's get-plugin-metadata command")
req := &proto.GetMetadataRequest{
PluginConfig: s.mergeConfig(opts.PluginConfig),
}
metadata, err := s.plugin.GetMetadata(ctx, req)
metadata, err := s.plugin.GetMetadata(ctx, &plugin.GetMetadataRequest{PluginConfig: mergedConfig})
if err != nil {
return nil, nil, err
}
logger.Debugf("Using plugin %v with capabilities %v to sign artifact %v in signature media type %v", metadata.Name, metadata.Capabilities, desc.Digest, opts.SignatureMediaType)
if metadata.HasCapability(proto.CapabilitySignatureGenerator) {
return s.generateSignature(ctx, desc, opts, metadata)
} else if metadata.HasCapability(proto.CapabilityEnvelopeGenerator) {
return s.generateSignatureEnvelope(ctx, desc, opts)
logger.Debugf("Using plugin %v with capabilities %v to sign oci artifact %v in signature media type %v", metadata.Name, metadata.Capabilities, desc.Digest, opts.SignatureMediaType)
if metadata.HasCapability(plugin.CapabilitySignatureGenerator) {
ks, err := s.getKeySpec(ctx, mergedConfig)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
}
sig, signerInfo, err := s.generateSignature(ctx, desc, opts, ks, metadata, mergedConfig)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
}
return sig, signerInfo, nil
} else if metadata.HasCapability(plugin.CapabilityEnvelopeGenerator) {
sig, signerInfo, err := s.generateSignatureEnvelope(ctx, desc, opts)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign with the plugin %s: %w", metadata.Name, err)
}
return sig, signerInfo, nil
}
return nil, nil, fmt.Errorf("plugin does not have signing capabilities")
}
func (s *pluginSigner) generateSignature(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions, metadata *proto.GetMetadataResponse) ([]byte, *signature.SignerInfo, error) {
// SignBlob signs the descriptor returned by genDesc, and returns the
// signature and SignerInfo.
func (s *PluginSigner) SignBlob(ctx context.Context, descGenFunc notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
logger := log.GetLogger(ctx)
mergedConfig := s.mergeConfig(opts.PluginConfig)
logger.Debug("Invoking plugin's get-plugin-metadata command")
metadata, err := s.plugin.GetMetadata(ctx, &plugin.GetMetadataRequest{PluginConfig: mergedConfig})
if err != nil {
return nil, nil, err
}
// only support blob signing with the signature generator capability because
// the envelope generator capability is designed for OCI signing.
// A new capability may be added in the future for blob signing.
if !metadata.HasCapability(plugin.CapabilitySignatureGenerator) {
return nil, nil, fmt.Errorf("the plugin %q lacks the signature generator capability required for blob signing", metadata.Name)
}
logger.Debug("Invoking plugin's describe-key command")
ks, err := s.getKeySpec(ctx, mergedConfig)
if err != nil {
return nil, nil, err
}
// get descriptor to sign
desc, err := getDescriptor(ks, descGenFunc)
if err != nil {
return nil, nil, err
}
logger.Debugf("Using plugin %v with capabilities %v to sign blob using descriptor %+v", metadata.Name, metadata.Capabilities, desc)
return s.generateSignature(ctx, desc, opts, ks, metadata, mergedConfig)
}
func (s *PluginSigner) getKeySpec(ctx context.Context, config map[string]string) (signature.KeySpec, error) {
logger := log.GetLogger(ctx)
logger.Debug("Invoking plugin's describe-key command")
descKeyResp, err := s.describeKey(ctx, config)
if err != nil {
return signature.KeySpec{}, err
}
if s.keyID != descKeyResp.KeyID {
return signature.KeySpec{}, fmt.Errorf("keyID in describeKey response %q does not match request %q", descKeyResp.KeyID, s.keyID)
}
return proto.DecodeKeySpec(descKeyResp.KeySpec)
}
func (s *PluginSigner) generateSignature(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions, ks signature.KeySpec, metadata *plugin.GetMetadataResponse, pluginConfig map[string]string) ([]byte, *signature.SignerInfo, error) {
logger := log.GetLogger(ctx)
logger.Debug("Generating signature by plugin")
config := s.mergeConfig(opts.PluginConfig)
// Get key info.
key, err := s.describeKey(ctx, config)
if err != nil {
return nil, nil, err
}
// Check keyID is honored.
if s.keyID != key.KeyID {
return nil, nil, fmt.Errorf("keyID in describeKey response %q does not match request %q", key.KeyID, s.keyID)
}
ks, err := proto.DecodeKeySpec(key.KeySpec)
if err != nil {
return nil, nil, err
}
genericSigner := genericSigner{
Signer: &pluginPrimitiveSigner{
genericSigner := GenericSigner{
signer: &pluginPrimitiveSigner{
ctx: ctx,
plugin: s.plugin,
keyID: s.keyID,
pluginConfig: config,
pluginConfig: pluginConfig,
keySpec: ks,
},
}
opts.SigningAgent = fmt.Sprintf("%s %s/%s", signingAgent, metadata.Name, metadata.Version)
return genericSigner.Sign(ctx, desc, opts)
}
func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
func (s *PluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
logger := log.GetLogger(ctx)
logger.Debug("Generating signature envelope by plugin")
payload := envelope.Payload{TargetArtifact: envelope.SanitizeTargetArtifact(desc)}
@ -113,8 +182,10 @@ func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
if err != nil {
return nil, nil, fmt.Errorf("envelope payload can't be marshalled: %w", err)
}
// Execute plugin sign command.
req := &proto.GenerateEnvelopeRequest{
req := &plugin.GenerateEnvelopeRequest{
ContractVersion: plugin.ContractVersion,
KeyID: s.keyID,
Payload: payloadBytes,
SignatureEnvelopeType: opts.SignatureMediaType,
@ -134,13 +205,11 @@ func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
resp.SignatureEnvelopeType, req.SignatureEnvelopeType,
)
}
logger.Debug("Verifying signature envelope generated by the plugin")
sigEnv, err := signature.ParseEnvelope(opts.SignatureMediaType, resp.SignatureEnvelope)
if err != nil {
return nil, nil, err
}
envContent, err := sigEnv.Verify()
if err != nil {
return nil, nil, fmt.Errorf("generated signature failed verification: %w", err)
@ -148,31 +217,29 @@ func (s *pluginSigner) generateSignatureEnvelope(ctx context.Context, desc ocisp
if err := envelope.ValidatePayloadContentType(&envContent.Payload); err != nil {
return nil, nil, err
}
content := envContent.Payload.Content
var signedPayload envelope.Payload
if err = json.Unmarshal(content, &signedPayload); err != nil {
return nil, nil, fmt.Errorf("signed envelope payload can't be unmarshalled: %w", err)
}
if !isPayloadDescriptorValid(desc, signedPayload.TargetArtifact) {
return nil, nil, fmt.Errorf("during signing descriptor subject has changed from %+v to %+v", desc, signedPayload.TargetArtifact)
}
if unknownAttributes := areUnknownAttributesAdded(content); len(unknownAttributes) != 0 {
return nil, nil, fmt.Errorf("during signing, following unknown attributes were added to subject descriptor: %+q", unknownAttributes)
}
s.manifestAnnotations = resp.Annotations
return resp.SignatureEnvelope, &envContent.SignerInfo, nil
}
func (s *pluginSigner) mergeConfig(config map[string]string) map[string]string {
func (s *PluginSigner) mergeConfig(config map[string]string) map[string]string {
c := make(map[string]string, len(s.pluginConfig)+len(config))
// First clone s.PluginConfig.
for k, v := range s.pluginConfig {
c[k] = v
}
// Then set or override entries from config.
for k, v := range config {
c[k] = v
@ -180,16 +247,16 @@ func (s *pluginSigner) mergeConfig(config map[string]string) map[string]string {
return c
}
func (s *pluginSigner) describeKey(ctx context.Context, config map[string]string) (*proto.DescribeKeyResponse, error) {
req := &proto.DescribeKeyRequest{
KeyID: s.keyID,
PluginConfig: config,
func (s *PluginSigner) describeKey(ctx context.Context, config map[string]string) (*plugin.DescribeKeyResponse, error) {
req := &plugin.DescribeKeyRequest{
ContractVersion: plugin.ContractVersion,
KeyID: s.keyID,
PluginConfig: config,
}
resp, err := s.plugin.DescribeKey(ctx, req)
if err != nil {
return nil, fmt.Errorf("describe-key command failed: %w", err)
return nil, err
}
return resp, nil
}
@ -199,6 +266,7 @@ func isDescriptorSubset(original, newDesc ocispec.Descriptor) bool {
if !content.Equal(original, newDesc) {
return false
}
// Plugins may append additional annotations but not replace/override
// existing.
for k, v := range original.Annotations {
@ -216,6 +284,7 @@ func isPayloadDescriptorValid(originalDesc, newDesc ocispec.Descriptor) bool {
func areUnknownAttributesAdded(content []byte) []string {
var targetArtifactMap map[string]interface{}
// Ignoring error because we already successfully unmarshalled before this
// point
_ = json.Unmarshal(content, &targetArtifactMap)
@ -272,23 +341,21 @@ func (s *pluginPrimitiveSigner) Sign(payload []byte) ([]byte, []*x509.Certificat
if err != nil {
return nil, nil, err
}
keySpecHash, err := proto.HashAlgorithmFromKeySpec(s.keySpec)
if err != nil {
return nil, nil, err
}
req := &proto.GenerateSignatureRequest{
KeyID: s.keyID,
KeySpec: keySpec,
Hash: keySpecHash,
Payload: payload,
PluginConfig: s.pluginConfig,
req := &plugin.GenerateSignatureRequest{
ContractVersion: plugin.ContractVersion,
KeyID: s.keyID,
KeySpec: keySpec,
Hash: keySpecHash,
Payload: payload,
PluginConfig: s.pluginConfig,
}
resp, err := s.plugin.GenerateSignature(s.ctx, req)
if err != nil {
return nil, nil, fmt.Errorf("generate-signature command failed: %w", err)
return nil, nil, err
}
// Check keyID is honored.

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package signer
import (
@ -19,6 +32,7 @@ import (
"github.com/notaryproject/notation-go/internal/envelope"
"github.com/notaryproject/notation-go/plugin"
"github.com/notaryproject/notation-go/plugin/proto"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
@ -57,6 +71,16 @@ type mockPlugin struct {
keySpec signature.KeySpec
}
func getDescriptorFunc(throwError bool) func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) {
return func(hashAlgo digest.Algorithm) (ocispec.Descriptor, error) {
if throwError {
return ocispec.Descriptor{}, errors.New("")
}
return validSignDescriptor, nil
}
}
func newMockPlugin(key crypto.PrivateKey, certs []*x509.Certificate, keySpec signature.KeySpec) *mockPlugin {
return &mockPlugin{
key: key,
@ -102,7 +126,7 @@ func (p *mockPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateS
return &proto.GenerateSignatureResponse{
KeyID: req.KeyID,
Signature: invalidSignatureEnvelope,
SigningAlgorithm: string(sigAlg),
SigningAlgorithm: sigAlg,
CertificateChain: certChain,
}, err
}
@ -123,7 +147,7 @@ func (p *mockPlugin) GenerateSignature(ctx context.Context, req *proto.GenerateS
// GenerateEnvelope generates the Envelope with signature based on the request.
func (p *mockPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEnvelopeRequest) (*proto.GenerateEnvelopeResponse, error) {
internalPluginSigner := pluginSigner{
internalPluginSigner := PluginSigner{
plugin: newMockPlugin(p.key, p.certs, p.keySpec),
}
@ -192,16 +216,46 @@ func (p *mockPlugin) GenerateEnvelope(ctx context.Context, req *proto.GenerateEn
return &proto.GenerateEnvelopeResponse{}, nil
}
func TestPluginSignerImpl(t *testing.T) {
p := &PluginSigner{}
if _, ok := interface{}(p).(notation.Signer); !ok {
t.Fatal("PluginSigner does not implement notation.Signer")
}
if _, ok := interface{}(p).(notation.BlobSigner); !ok {
t.Fatal("PluginSigner does not implement notation.BlobSigner")
}
}
func TestNewFromPluginFailed(t *testing.T) {
wantErr := "keyID not specified"
_, err := NewFromPlugin(&plugin.CLIPlugin{}, "", make(map[string]string))
if err == nil || err.Error() != wantErr {
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", wantErr, err.Error())
tests := map[string]struct {
pl plugin.SignPlugin
keyID string
errMsg string
}{
"Invalid KeyID": {
pl: &plugin.CLIPlugin{},
keyID: "",
errMsg: "keyID not specified",
},
"nilPlugin": {
pl: nil,
keyID: "someKeyId",
errMsg: "nil plugin",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
_, err := NewFromPlugin(tc.pl, tc.keyID, make(map[string]string))
if err == nil || err.Error() != tc.errMsg {
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", tc.errMsg, err.Error())
}
})
}
}
func TestSigner_Sign_EnvelopeNotSupported(t *testing.T) {
signer := pluginSigner{
signer := PluginSigner{
plugin: newMockPlugin(nil, nil, signature.KeySpec{Type: signature.KeyTypeRSA, Size: 2048}),
}
opts := notation.SignerSignOptions{SignatureMediaType: "unsupported"}
@ -212,7 +266,7 @@ func TestSigner_Sign_DescribeKeyIDMismatch(t *testing.T) {
respKeyId := ""
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
signer := pluginSigner{
signer := PluginSigner{
plugin: newMockPlugin(nil, nil, signature.KeySpec{}),
keyID: "1",
}
@ -225,7 +279,7 @@ func TestSigner_Sign_ExpiryInValid(t *testing.T) {
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
ks, _ := signature.ExtractKeySpec(keyCertPairCollections[0].certs[0])
signer := pluginSigner{
signer := PluginSigner{
plugin: newMockPlugin(keyCertPairCollections[0].key, keyCertPairCollections[0].certs, ks),
}
_, _, err := signer.Sign(context.Background(), ocispec.Descriptor{}, notation.SignerSignOptions{ExpiryDuration: -24 * time.Hour, SignatureMediaType: envelopeType})
@ -242,7 +296,7 @@ func TestSigner_Sign_InvalidCertChain(t *testing.T) {
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
mockPlugin := newMockPlugin(defaultKeyCert.key, defaultKeyCert.certs, defaultKeySpec)
mockPlugin.invalidCertChain = true
signer := pluginSigner{
signer := PluginSigner{
plugin: mockPlugin,
}
testSignerError(t, signer, "x509: malformed certificate", notation.SignerSignOptions{SignatureMediaType: envelopeType})
@ -256,7 +310,7 @@ func TestSigner_Sign_InvalidDescriptor(t *testing.T) {
mockPlugin := newMockPlugin(defaultKeyCert.key, defaultKeyCert.certs, defaultKeySpec)
mockPlugin.wantEnvelope = true
mockPlugin.invalidDescriptor = true
signer := pluginSigner{
signer := PluginSigner{
plugin: mockPlugin,
}
testSignerError(t, signer, "during signing, following unknown attributes were added to subject descriptor: [\"additional_field\"]", notation.SignerSignOptions{SignatureMediaType: envelopeType})
@ -269,7 +323,7 @@ func TestPluginSigner_Sign_SignatureVerifyError(t *testing.T) {
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
mockPlugin := newMockPlugin(defaultKeyCert.key, defaultKeyCert.certs, defaultKeySpec)
mockPlugin.invalidSig = true
signer := pluginSigner{
signer := PluginSigner{
plugin: mockPlugin,
}
testSignerError(t, signer, "signature is invalid", notation.SignerSignOptions{SignatureMediaType: envelopeType})
@ -282,15 +336,48 @@ func TestPluginSigner_Sign_Valid(t *testing.T) {
for _, keyCert := range keyCertPairCollections {
t.Run(fmt.Sprintf("external plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
pluginSigner := pluginSigner{
pluginSigner := PluginSigner{
plugin: newMockPlugin(keyCert.key, keyCert.certs, keySpec),
}
basicSignTest(t, &pluginSigner, envelopeType, &validMetadata)
validSignOpts.SignatureMediaType = envelopeType
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
})
}
}
}
func TestPluginSigner_SignBlob_Valid(t *testing.T) {
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
for _, keyCert := range keyCertPairCollections {
t.Run(fmt.Sprintf("external plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
pluginSigner := PluginSigner{
plugin: newMockPlugin(keyCert.key, keyCert.certs, keySpec),
}
validSignOpts.SignatureMediaType = envelopeType
data, signerInfo, err := pluginSigner.SignBlob(context.Background(), getDescriptorFunc(false), validSignOpts)
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
})
}
}
}
func TestPluginSigner_SignBlob_Invalid(t *testing.T) {
t.Run("blob signing with generate envelope plugin should fail", func(t *testing.T) {
plugin := &mockPlugin{}
plugin.wantEnvelope = true
pluginSigner := PluginSigner{
plugin: plugin,
}
_, _, err := pluginSigner.SignBlob(context.Background(), getDescriptorFunc(false), validSignOpts)
expectedErrMsg := "the plugin \"testPlugin\" lacks the signature generator capability required for blob signing"
if err == nil || !strings.Contains(err.Error(), expectedErrMsg) {
t.Fatalf("expected error %q, got %v", expectedErrMsg, err)
}
})
}
func TestPluginSigner_SignEnvelope_RunFailed(t *testing.T) {
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
t.Run(fmt.Sprintf("envelopeType=%v", envelopeType), func(t *testing.T) {
@ -298,7 +385,7 @@ func TestPluginSigner_SignEnvelope_RunFailed(t *testing.T) {
wantEnvelope: true,
failEnvelope: true,
}
signer := pluginSigner{
signer := PluginSigner{
plugin: p,
}
testSignerError(t, signer, "failed GenerateEnvelope", notation.SignerSignOptions{SignatureMediaType: envelopeType})
@ -313,10 +400,12 @@ func TestPluginSigner_SignEnvelope_Valid(t *testing.T) {
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
mockPlugin := newMockPlugin(keyCert.key, keyCert.certs, keySpec)
mockPlugin.wantEnvelope = true
pluginSigner := pluginSigner{
pluginSigner := PluginSigner{
plugin: mockPlugin,
}
basicSignTest(t, &pluginSigner, envelopeType, &validMetadata)
validSignOpts.SignatureMediaType = envelopeType
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
})
}
}
@ -328,7 +417,7 @@ func TestPluginSigner_SignWithAnnotations_Valid(t *testing.T) {
t.Run(fmt.Sprintf("external plugin,envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
keySpec, _ := proto.DecodeKeySpec(proto.KeySpec(keyCert.keySpecName))
annts := map[string]string{"key": "value"}
pluginSigner := pluginSigner{
pluginSigner := PluginSigner{
plugin: &mockPlugin{
key: keyCert.key,
certs: keyCert.certs,
@ -337,7 +426,9 @@ func TestPluginSigner_SignWithAnnotations_Valid(t *testing.T) {
wantEnvelope: true,
},
}
basicSignTest(t, &pluginSigner, envelopeType, &validMetadata)
validSignOpts.SignatureMediaType = envelopeType
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
basicSignTest(t, &pluginSigner, envelopeType, data, signerInfo, err)
if !reflect.DeepEqual(pluginSigner.PluginAnnotations(), annts) {
fmt.Println(pluginSigner.PluginAnnotations())
t.Errorf("mismatch in annotations returned from PluginAnnotations()")
@ -347,7 +438,7 @@ func TestPluginSigner_SignWithAnnotations_Valid(t *testing.T) {
}
}
func testSignerError(t *testing.T, signer pluginSigner, wantEr string, opts notation.SignerSignOptions) {
func testSignerError(t *testing.T, signer PluginSigner, wantEr string, opts notation.SignerSignOptions) {
t.Helper()
_, _, err := signer.Sign(context.Background(), ocispec.Descriptor{}, opts)
if err == nil || !strings.Contains(err.Error(), wantEr) {
@ -355,9 +446,7 @@ func testSignerError(t *testing.T, signer pluginSigner, wantEr string, opts nota
}
}
func basicSignTest(t *testing.T, pluginSigner *pluginSigner, envelopeType string, metadata *proto.GetMetadataResponse) {
validSignOpts.SignatureMediaType = envelopeType
data, signerInfo, err := pluginSigner.Sign(context.Background(), validSignDescriptor, validSignOpts)
func basicSignTest(t *testing.T, ps *PluginSigner, envelopeType string, data []byte, signerInfo *signature.SignerInfo, err error) {
if err != nil {
t.Fatalf("Signer.Sign() error = %v, wantErr nil", err)
}
@ -386,12 +475,12 @@ func basicSignTest(t *testing.T, pluginSigner *pluginSigner, envelopeType string
TargetArtifact: validSignDescriptor,
}
if !reflect.DeepEqual(expectedPayload, gotPayload) {
t.Fatalf("Signer.Sign() descriptor subject changed, expect: %v, got: %v", expectedPayload, payload)
t.Fatalf("Signer.Sign() descriptor subject changed, expect: %+v, got: %+v", expectedPayload, payload)
}
if signerInfo.SignedAttributes.SigningScheme != signature.SigningSchemeX509 {
t.Fatalf("Signer.Sign() signing scheme changed, expect: %v, got: %v", signerInfo.SignedAttributes.SigningScheme, signature.SigningSchemeX509)
t.Fatalf("Signer.Sign() signing scheme changed, expect: %+v, got: %+v", signerInfo.SignedAttributes.SigningScheme, signature.SigningSchemeX509)
}
mockPlugin := pluginSigner.plugin.(*mockPlugin)
mockPlugin := ps.plugin.(*mockPlugin)
if mockPlugin.keySpec.SignatureAlgorithm() != signerInfo.SignatureAlgorithm {
t.Fatalf("Signer.Sign() signing algorithm changed")
}
@ -401,5 +490,5 @@ func basicSignTest(t *testing.T, pluginSigner *pluginSigner, envelopeType string
if !reflect.DeepEqual(mockPlugin.certs, signerInfo.CertificateChain) {
t.Fatalf(" Signer.Sign() cert chain changed")
}
basicVerification(t, data, envelopeType, mockPlugin.certs[len(mockPlugin.certs)-1], metadata)
basicVerification(t, data, envelopeType, mockPlugin.certs[len(mockPlugin.certs)-1], &validMetadata)
}

View File

@ -1,6 +1,19 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package signer provides notation signing functionality. It implements the
// notation.Signer interface by providing builtinSigner for local signing and
// pluginSigner for remote signing.
// [notation.Signer] and [notation.BlobSigner] interfaces by providing
// builtinSigner for local signing and [PluginSigner] for remote signing.
package signer
import (
@ -21,26 +34,42 @@ import (
)
// signingAgent is the unprotected header field used by signature.
const signingAgent = "Notation/1.0.0"
const signingAgent = "notation-go/1.3.0+unreleased"
// genericSigner implements notation.Signer and embeds signature.Signer
type genericSigner struct {
signature.Signer
// GenericSigner implements [notation.Signer] and [notation.BlobSigner].
// It embeds signature.Signer.
type GenericSigner struct {
signer signature.Signer
}
// New returns a builtinSigner given key and cert chain
// New returns a [notation.Signer] given key and cert chain.
//
// Deprecated: New function exists for historical compatibility and
// should not be used. To create [GenericSigner],
// use NewGenericSigner() function.
func New(key crypto.PrivateKey, certChain []*x509.Certificate) (notation.Signer, error) {
return NewGenericSigner(key, certChain)
}
// NewGenericSigner returns a builtinSigner given key and cert chain.
func NewGenericSigner(key crypto.PrivateKey, certChain []*x509.Certificate) (*GenericSigner, error) {
localSigner, err := signature.NewLocalSigner(certChain, key)
if err != nil {
return nil, err
}
return &genericSigner{
Signer: localSigner,
return &GenericSigner{
signer: localSigner,
}, nil
}
// NewFromFiles returns a builtinSigner given key and certChain paths.
// NewFromFiles returns a [notation.Signer] given key and certChain paths.
func NewFromFiles(keyPath, certChainPath string) (notation.Signer, error) {
return NewGenericSignerFromFiles(keyPath, certChainPath)
}
// NewGenericSignerFromFiles returns a builtinSigner given key and certChain
// paths.
func NewGenericSignerFromFiles(keyPath, certChainPath string) (*GenericSigner, error) {
if keyPath == "" {
return nil, errors.New("key path not specified")
}
@ -67,12 +96,12 @@ func NewFromFiles(keyPath, certChainPath string) (notation.Signer, error) {
}
// create signer
return New(cert.PrivateKey, certs)
return NewGenericSigner(cert.PrivateKey, certs)
}
// Sign signs the artifact described by its descriptor and returns the
// marshalled envelope.
func (s *genericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
// signature and SignerInfo.
func (s *GenericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
logger := log.GetLogger(ctx)
logger.Debugf("Generic signing for %v in signature media type %v", desc.Digest, opts.SignatureMediaType)
// Generate payload to be signed.
@ -81,22 +110,30 @@ func (s *genericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
if err != nil {
return nil, nil, fmt.Errorf("envelope payload can't be marshalled: %w", err)
}
var signingAgentId string
if opts.SigningAgent != "" {
signingAgentId = opts.SigningAgent
} else {
signingAgentId = signingAgent
}
if opts.Timestamper != nil && opts.TSARootCAs == nil {
return nil, nil, errors.New("timestamping: got Timestamper but nil TSARootCAs")
}
if opts.TSARootCAs != nil && opts.Timestamper == nil {
return nil, nil, errors.New("timestamping: got TSARootCAs but nil Timestamper")
}
signReq := &signature.SignRequest{
Payload: signature.Payload{
ContentType: envelope.MediaTypePayloadV1,
Content: payloadBytes,
},
Signer: s.Signer,
SigningTime: time.Now(),
SigningScheme: signature.SigningSchemeX509,
SigningAgent: signingAgentId,
Signer: s.signer,
SigningTime: time.Now(),
SigningScheme: signature.SigningSchemeX509,
SigningAgent: signingAgentId,
Timestamper: opts.Timestamper,
TSARootCAs: opts.TSARootCAs,
TSARevocationValidator: opts.TSARevocationValidator,
}
// Add expiry only if ExpiryDuration is not zero
@ -110,18 +147,25 @@ func (s *genericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
logger.Debugf(" Expiry: %v", signReq.Expiry)
logger.Debugf(" SigningScheme: %v", signReq.SigningScheme)
logger.Debugf(" SigningAgent: %v", signReq.SigningAgent)
if signReq.Timestamper != nil {
logger.Debug("Enabled timestamping")
if signReq.TSARevocationValidator != nil {
logger.Debug("Enabled timestamping certificate chain revocation check")
}
}
// Add ctx to the SignRequest
signReq = signReq.WithContext(ctx)
// perform signing
sigEnv, err := signature.NewEnvelope(opts.SignatureMediaType)
if err != nil {
return nil, nil, err
}
sig, err := sigEnv.Sign(signReq)
if err != nil {
return nil, nil, err
}
envContent, err := sigEnv.Verify()
if err != nil {
return nil, nil, fmt.Errorf("generated signature failed verification: %v", err)
@ -129,7 +173,29 @@ func (s *genericSigner) Sign(ctx context.Context, desc ocispec.Descriptor, opts
if err := envelope.ValidatePayloadContentType(&envContent.Payload); err != nil {
return nil, nil, err
}
// TODO: re-enable timestamping https://github.com/notaryproject/notation-go/issues/78
return sig, &envContent.SignerInfo, nil
}
// SignBlob signs the descriptor returned by genDesc, and returns the
// signature and SignerInfo.
func (s *GenericSigner) SignBlob(ctx context.Context, genDesc notation.BlobDescriptorGenerator, opts notation.SignerSignOptions) ([]byte, *signature.SignerInfo, error) {
logger := log.GetLogger(ctx)
logger.Debugf("Generic blob signing for signature media type %s", opts.SignatureMediaType)
ks, err := s.signer.KeySpec()
if err != nil {
return nil, nil, err
}
desc, err := getDescriptor(ks, genDesc)
if err != nil {
return nil, nil, err
}
return s.Sign(ctx, desc, opts)
}
func getDescriptor(ks signature.KeySpec, genDesc notation.BlobDescriptorGenerator) (ocispec.Descriptor, error) {
digestAlg, ok := algorithms[ks.SignatureAlgorithm().Hash()]
if !ok {
return ocispec.Descriptor{}, fmt.Errorf("unknown hashing algo %v", ks.SignatureAlgorithm().Hash())
}
return genDesc(digestAlg)
}

View File

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package signer
import (
@ -17,17 +30,23 @@ import (
"testing"
"time"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/notation-core-go/revocation/purpose"
"github.com/notaryproject/notation-core-go/signature"
_ "github.com/notaryproject/notation-core-go/signature/cose"
_ "github.com/notaryproject/notation-core-go/signature/jws"
"github.com/notaryproject/notation-core-go/testhelper"
nx509 "github.com/notaryproject/notation-core-go/x509"
"github.com/notaryproject/notation-go"
"github.com/notaryproject/notation-go/internal/envelope"
"github.com/notaryproject/notation-go/plugin/proto"
"github.com/notaryproject/tspclient-go"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
)
const rfc3161URL = "http://timestamp.digicert.com"
type keyCertPair struct {
keySpecName string
key crypto.PrivateKey
@ -36,7 +55,8 @@ type keyCertPair struct {
var keyCertPairCollections []*keyCertPair
// setUpKeyCertPairCollections setups all combinations of private key and certificates.
// setUpKeyCertPairCollections setups all combinations of private key and
// certificates.
func setUpKeyCertPairCollections() []*keyCertPair {
// rsa
var keyCertPairs []*keyCertPair
@ -104,7 +124,7 @@ func generateKeyBytes(key crypto.PrivateKey) (keyBytes []byte, err error) {
return keyBytes, nil
}
func prepareTestKeyCertFile(keyCert *keyCertPair, envelopeType, dir string) (string, string, error) {
func prepareTestKeyCertFile(keyCert *keyCertPair, dir string) (string, string, error) {
keyPath, certPath := filepath.Join(dir, keyCert.keySpecName+".key"), filepath.Join(dir, keyCert.keySpecName+".cert")
keyBytes, err := generateKeyBytes(keyCert.key)
if err != nil {
@ -125,7 +145,7 @@ func prepareTestKeyCertFile(keyCert *keyCertPair, envelopeType, dir string) (str
}
func testSignerFromFile(t *testing.T, keyCert *keyCertPair, envelopeType, dir string) {
keyPath, certPath, err := prepareTestKeyCertFile(keyCert, envelopeType, dir)
keyPath, certPath, err := prepareTestKeyCertFile(keyCert, dir)
if err != nil {
t.Fatalf("prepareTestKeyCertFile() failed: %v", err)
}
@ -143,6 +163,17 @@ func testSignerFromFile(t *testing.T, keyCert *keyCertPair, envelopeType, dir st
basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1], nil)
}
func TestGenericSignerImpl(t *testing.T) {
g := &GenericSigner{}
if _, ok := interface{}(g).(notation.Signer); !ok {
t.Fatal("GenericSigner does not implement notation.Signer")
}
if _, ok := interface{}(g).(notation.BlobSigner); !ok {
t.Fatal("GenericSigner does not implement notation.BlobSigner")
}
}
func TestNewFromFiles(t *testing.T) {
// sign with key
dir := t.TempDir()
@ -155,12 +186,134 @@ func TestNewFromFiles(t *testing.T) {
}
}
func TestNewFromFilesError(t *testing.T) {
tests := map[string]struct {
keyPath string
certPath string
errMsg string
}{
"empty key path": {
keyPath: "",
certPath: "someCert",
errMsg: "key path not specified",
},
"empty cert path": {
keyPath: "someKeyId",
certPath: "",
errMsg: "certificate path not specified",
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
_, err := NewFromFiles(tc.keyPath, tc.certPath)
if err == nil || err.Error() != tc.errMsg {
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", tc.errMsg, err.Error())
}
})
}
}
func TestNewError(t *testing.T) {
wantErr := "\"certs\" param is invalid. Error: empty certs"
_, err := New(nil, nil)
if err == nil || err.Error() != wantErr {
t.Fatalf("TestNewFromPluginFailed expects error %q, got %q", wantErr, err.Error())
}
}
func TestSignWithCertChain(t *testing.T) {
// sign with key
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
for _, keyCert := range keyCertPairCollections {
t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs)
validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs, false)
})
}
}
}
func TestSignWithTimestamping(t *testing.T) {
// sign with key
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
for _, keyCert := range keyCertPairCollections {
t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
validateSignWithCerts(t, envelopeType, keyCert.key, keyCert.certs, true)
})
}
}
// timestamping without timestamper
envelopeType := signature.RegisteredEnvelopeTypes()[0]
keyCert := keyCertPairCollections[0]
s, err := New(keyCert.key, keyCert.certs)
if err != nil {
t.Fatalf("NewSigner() error = %v", err)
}
ctx := context.Background()
desc, sOpts := generateSigningContent()
sOpts.SignatureMediaType = envelopeType
sOpts.TSARootCAs = x509.NewCertPool()
_, _, err = s.Sign(ctx, desc, sOpts)
expectedErrMsg := "timestamping: got TSARootCAs but nil Timestamper"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
// timestamping without TSARootCAs
desc, sOpts = generateSigningContent()
sOpts.SignatureMediaType = envelopeType
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
if err != nil {
t.Fatal(err)
}
_, _, err = s.Sign(ctx, desc, sOpts)
expectedErrMsg = "timestamping: got Timestamper but nil TSARootCAs"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
// timestamping with unknown authority
desc, sOpts = generateSigningContent()
sOpts.SignatureMediaType = envelopeType
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
if err != nil {
t.Fatal(err)
}
sOpts.TSARootCAs = x509.NewCertPool()
tsaRevocationValidator, err := revocation.NewWithOptions(revocation.Options{
CertChainPurpose: purpose.Timestamping,
})
if err != nil {
t.Fatal(err)
}
sOpts.TSARevocationValidator = tsaRevocationValidator
_, _, err = s.Sign(ctx, desc, sOpts)
expectedErrMsg = "timestamp: failed to verify signed token: cms verification failure: x509: certificate signed by unknown authority"
if err == nil || err.Error() != expectedErrMsg {
t.Fatalf("expected %s, but got %s", expectedErrMsg, err)
}
}
func TestSignBlobWithCertChain(t *testing.T) {
// sign with key
for _, envelopeType := range signature.RegisteredEnvelopeTypes() {
for _, keyCert := range keyCertPairCollections {
t.Run(fmt.Sprintf("envelopeType=%v_keySpec=%v", envelopeType, keyCert.keySpecName), func(t *testing.T) {
s, err := NewGenericSigner(keyCert.key, keyCert.certs)
if err != nil {
t.Fatalf("NewSigner() error = %v", err)
}
sOpts := notation.SignerSignOptions{
SignatureMediaType: envelopeType,
}
sig, _, err := s.SignBlob(context.Background(), getDescriptorFunc(false), sOpts)
if err != nil {
t.Fatalf("Sign() error = %v", err)
}
// basic verification
basicVerification(t, sig, envelopeType, keyCert.certs[len(keyCert.certs)-1], nil)
})
}
}
@ -196,7 +349,7 @@ func signRSA(digest []byte, hash crypto.Hash, pk *rsa.PrivateKey) ([]byte, error
return rsa.SignPSS(rand.Reader, pk, hash, digest, &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash})
}
func signECDSA(digest []byte, hash crypto.Hash, pk *ecdsa.PrivateKey) ([]byte, error) {
func signECDSA(digest []byte, pk *ecdsa.PrivateKey) ([]byte, error) {
r, s, err := ecdsa.Sign(rand.Reader, pk, digest)
if err != nil {
return nil, err
@ -216,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")
}
@ -281,7 +434,7 @@ func verifySigningAgent(t *testing.T, signingAgentId string, metadata *proto.Get
}
}
func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.PrivateKey, certs []*x509.Certificate) {
func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.PrivateKey, certs []*x509.Certificate, timestamp bool) {
s, err := New(key, certs)
if err != nil {
t.Fatalf("NewSigner() error = %v", err)
@ -290,6 +443,19 @@ func validateSignWithCerts(t *testing.T, envelopeType string, key crypto.Private
ctx := context.Background()
desc, sOpts := generateSigningContent()
sOpts.SignatureMediaType = envelopeType
if timestamp {
sOpts.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161URL)
if err != nil {
t.Fatal(err)
}
rootCerts, err := nx509.ReadCertificateFile("./testdata/DigiCertTSARootSHA384.cer")
if err != nil {
t.Fatal(err)
}
rootCAs := x509.NewCertPool()
rootCAs.AddCert(rootCerts[0])
sOpts.TSARootCAs = rootCAs
}
sig, _, err := s.Sign(ctx, desc, sOpts)
if err != nil {
t.Fatalf("Sign() error = %v", err)

Binary file not shown.

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

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package verifier
import (
@ -5,12 +18,12 @@ import (
"crypto/x509"
"errors"
"fmt"
"regexp"
"strings"
"github.com/notaryproject/notation-core-go/signature"
"github.com/notaryproject/notation-go"
set "github.com/notaryproject/notation-go/internal/container"
notationsemver "github.com/notaryproject/notation-go/internal/semver"
"github.com/notaryproject/notation-go/internal/slices"
"github.com/notaryproject/notation-go/verifier/trustpolicy"
"github.com/notaryproject/notation-go/verifier/truststore"
@ -34,10 +47,7 @@ var VerificationPluginHeaders = []string{
var errExtendedAttributeNotExist = errors.New("extended attribute not exist")
// semVerRegEx is takenfrom https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
var semVerRegEx = regexp.MustCompile(`^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`)
func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, policy *trustpolicy.TrustPolicy, x509TrustStore truststore.X509TrustStore) ([]*x509.Certificate, error) {
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:
@ -45,39 +55,15 @@ func loadX509TrustStores(ctx context.Context, scheme signature.SigningScheme, po
case signature.SigningSchemeX509SigningAuthority:
typeToLoad = truststore.TypeSigningAuthority
default:
return nil, fmt.Errorf("unrecognized signing scheme %q", scheme)
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, fmt.Errorf("trust policy statement %q is missing separator in trust store value %q. The required format is <TrustStoreType>:<TrustStoreName>", policy.Name, trustStore)
}
if typeToLoad != truststore.Type(storeType) {
continue
}
certs, err := x509TrustStore.GetCertificates(ctx, typeToLoad, name)
if err != nil {
return nil, err
}
certificates = append(certificates, certs...)
processedStoreSet.Add(trustStore)
}
return certificates, nil
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
@ -139,12 +125,61 @@ func getVerificationPluginMinVersion(signerInfo *signature.SignerInfo) (string,
if strings.TrimSpace(version) == "" {
return "", fmt.Errorf("%v from extended attribute is an empty string", HeaderVerificationPluginMinVersion)
}
if !isVersionSemverValid(version) {
if !notationsemver.IsValid(version) {
return "", fmt.Errorf("%v from extended attribute is not a valid SemVer", HeaderVerificationPluginMinVersion)
}
return version, nil
}
func isVersionSemverValid(version string) bool {
return semVerRegEx.MatchString(version)
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

@ -1,3 +1,16 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package verifier
import (
@ -15,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 {
@ -66,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) {
@ -104,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, "@")
@ -118,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.

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