├── .github ├── .codecov.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── codeql.yml ├── .gitignore ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── NOTICE ├── README.md ├── cmd └── main.go ├── examples ├── README.md ├── go.mod ├── go.sum ├── sign │ ├── main.go │ └── sign.go ├── utils │ ├── ecr.go │ └── utils.go └── verify │ ├── main.go │ └── verify.go ├── go.mod ├── go.sum ├── internal ├── client │ ├── client.go │ ├── client_test.go │ └── interface.go ├── logger │ ├── logger.go │ └── logger_test.go ├── signer │ ├── signer.go │ └── signer_test.go ├── slices │ ├── slices.go │ └── slices_test.go ├── verifier │ ├── verifier.go │ └── verifier_test.go └── version │ ├── version.go │ └── version_test.go └── plugin ├── plugin.go └── plugin_test.go /.github/.codecov.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | # not use this file except in compliance with the License. A copy of the 5 | # License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | ignore: 14 | - "internal/client/mock_client.go" 15 | 16 | coverage: 17 | status: 18 | project: 19 | default: 20 | target: 80% -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 19 | 20 | **Describe the bug** 21 | A clear and concise description of what the bug is. 22 | 23 | **Steps to reproduce** 24 | If possible, provide a recipe for reproducing the error. 25 | 26 | **What did you expect to see?** 27 | A clear and concise description of what you expected to see. 28 | 29 | **What did you see instead?** 30 | A clear and concise description of what you saw instead. 31 | 32 | **What plugin version did you use?** 33 | Version: (e.g., `1.0.298`, etc) 34 | 35 | **What config did you use?** 36 | Config: (e.g. the agent json config file) 37 | 38 | **Environment** 39 | OS: (e.g., "Ubuntu 20.04") 40 | notation version : (e.g., "notation-1.0.0") 41 | 42 | **Additional context** 43 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | # not use this file except in compliance with the License. A copy of the 5 | # License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | version: 2 15 | updates: 16 | - package-ecosystem: "gomod" 17 | directory: "/" # Location of package manifests 18 | schedule: 19 | interval: "weekly" 20 | - package-ecosystem: "gomod" 21 | directory: "/examples" # Location of package manifests 22 | schedule: 23 | interval: "weekly" 24 | - package-ecosystem: "github-actions" 25 | directory: "/" # default location of `.github/workflows` 26 | schedule: 27 | interval: "weekly" 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | # not use this file except in compliance with the License. A copy of the 5 | # License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | name: build 15 | 16 | on: 17 | push: 18 | pull_request: 19 | 20 | permissions: 21 | contents: read 22 | 23 | jobs: 24 | build: 25 | runs-on: ubuntu-latest 26 | strategy: 27 | matrix: 28 | go-version: [ "1.23.1" ] 29 | fail-fast: true 30 | steps: 31 | - name: Check out code 32 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 33 | - name: Set up Go ${{ matrix.go-version }} 34 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 35 | with: 36 | go-version: ${{ matrix.go-version }} 37 | check-latest: true 38 | - name: Run unit tests 39 | run: make test 40 | - name: Upload coverage to codecov.io 41 | uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 42 | env: 43 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 44 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | # not use this file except in compliance with the License. A copy of the 5 | # License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | name: "CodeQL" 15 | 16 | on: 17 | push: 18 | branches: main 19 | pull_request: 20 | branches: main 21 | schedule: 22 | - cron: '29 2 * * 5' 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze 27 | runs-on: ubuntu-latest 28 | permissions: 29 | actions: read 30 | contents: read 31 | security-events: write 32 | strategy: 33 | matrix: 34 | go-version: ["1.23.1"] 35 | fail-fast: false 36 | steps: 37 | - name: Check out code 38 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 39 | - name: Set up Go ${{ matrix.go-version }} 40 | uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.5.0 41 | with: 42 | go-version: ${{ matrix.go-version }} 43 | check-latest: true 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: go 48 | - name: Perform CodeQL Analysis 49 | uses: github/codeql-action/analyze@v3 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | bin 3 | internal/client/mock_client.go 4 | coverage.txt 5 | 6 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 2 | 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence, these accounts 5 | # will be requested for review when someone opens a pull request. 6 | * @aws/aws-signer -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | # not use this file except in compliance with the License. A copy of the 5 | # License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0 8 | # 9 | # or in the "license" file accompanying this file. This file is distributed 10 | # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | # express or implied. See the License for the specific language governing 12 | # permissions and limitations under the License. 13 | 14 | .PHONY: help 15 | help: 16 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' 17 | 18 | .PHONY: build 19 | build: test ## build the aws signer notation plugin 20 | go build -o ./build/bin/notation-com.amazonaws.signer.notation.plugin ./cmd 21 | 22 | .PHONY: test 23 | test: generate-mocks ## run the unit tests 24 | go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 25 | 26 | .PHONY: generate-mocks 27 | generate-mocks: ## generate mocks required for unit tests 28 | @if ! command -v mockgen &> /dev/null; then \ 29 | echo "Installing mockgen as it is not present in the system..."; \ 30 | go install github.com/golang/mock/mockgen@v1.6.0; \ 31 | fi 32 | @echo "Generating Mocks..." 33 | mockgen -package client -destination=./internal/client/mock_client.go "github.com/aws/aws-signer-notation-plugin/internal/client" Interface 34 | @echo "Mocks generated successfully." 35 | 36 | .PHONY: clean 37 | clean: ## remove build artifacts and mocks 38 | rm -rf ./internal/client/mock_client.go 39 | rm -rf ./build 40 | git status --ignored --short | grep '^!! ' | sed 's/!! //' | xargs rm -rf 41 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## AWS Signer Plugin for Notation 2 | 3 | [![Build Status](https://github.com/aws/aws-signer-notation-plugin/actions/workflows/build.yml/badge.svg?event=push&branch=main)](https://github.com/aws/aws-signer-notation-plugin/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain) 4 | [![Codecov](https://codecov.io/gh/aws/aws-signer-notation-plugin/branch/main/graph/badge.svg)](https://codecov.io/gh/aws/aws-signer-notation-plugin) 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/aws/aws-signer-notation-plugin.svg)](https://pkg.go.dev/github.com/aws/aws-signer-notation-plugin@main) 6 | 7 | [Notation](https://github.com/notaryproject/notation) is an open source tool developed by the [Notary Project](https://notaryproject.dev/), which supports signing and verifying container images and other artifacts. The AWS Signer Notation plugin, allows users of Notation ([notation CLI](https://github.com/notaryproject/notation) and [notation-go](https://github.com/notaryproject/notation-go)) to sign and verify artifacts (such as container images) using AWS Signer. [AWS Signer](https://docs.aws.amazon.com/signer/latest/developerguide/Welcome.html) is a fully managed code-signing service to ensure the trust and integrity of your code. AWS Signer manages the code-signing certificates, secures private keys, and manages key rotation without requiring users to take any action. 8 | 9 | The plugin is compliant with the [Notary Project specification](https://github.com/notaryproject/specifications/tree/main). It uses the AWS Signer _SignPayload_ API for signing, and _GetRevocationStatus_ API for signature verification. 10 | 11 | ## Getting Started 12 | To use AWS Signer Notation plugin: 13 | 14 | * Notation CLI - Please refer [AWS Signer documentation](https://docs.aws.amazon.com/signer/latest/developerguide/container-workflow.html) for guidance on signing and verifying OCI artifacts. 15 | * notation-go library - You can use this plugin as library with notation-go, eliminating the need for invoking plugin executable. Please refer the provided [examples](https://github.com/aws/aws-signer-notation-plugin/tree/main/examples) on how to use plugin as library with notation-go. 16 | 17 | ## Building from Source 18 | 19 | 1. Install go. For more information, refer [go documentation](https://golang.org/doc/install). 20 | 2. The plugin uses go modules for dependency management. For more information, refer [go modules](https://github.com/golang/go/wiki/Modules). 21 | 3. Run `make build` to build the AWS Signer Notation plugin. 22 | 4. Upon completion of the build process, the plugin executable will be created at `build/bin/notation-com.amazonaws.signer.notation.plugin`. 23 | 24 | Now you can use this plugin executable with notation CLI by using the following command: 25 | 26 | `notation plugin install --file ./build/bin/notation-com.amazonaws.signer.notation.plugin` 27 | 28 | ### Make Targets 29 | The following targets are available. Each may be run with `make `. 30 | 31 | | Make Target | Description | 32 | |:-----------------|:--------------------------------------------------------------------------------------| 33 | | `help` | shows available make targets | 34 | | `build` | builds the plugin executable for current environment (e.g. Linux, Darwin and Windows) | 35 | | `test` | runs all the unit tests using `go test` | 36 | | `generate-mocks` | generates the mocks required for unit tests | 37 | | `clean` | removes build artifacts and auto generated mocks. | 38 | 39 | ## Security disclosures 40 | To report a potential security issue, please do not create a new Issue in the repository. Instead, please report using the instructions [here](https://aws.amazon.com/security/vulnerability-reporting/) or email [AWS security directly](mailto:aws-security@amazon.com). 41 | 42 | ## License 43 | This project is licensed under the [Apache-2.0](LICENSE) License. 44 | 45 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "os" 20 | "time" 21 | 22 | "github.com/aws/aws-signer-notation-plugin/internal/logger" 23 | "github.com/aws/aws-signer-notation-plugin/plugin" 24 | 25 | "github.com/notaryproject/notation-plugin-framework-go/cli" 26 | ) 27 | 28 | const debugFlag = "AWS_SIGNER_NOTATION_PLUGIN_DEBUG" 29 | 30 | func main() { 31 | awsPlugin := plugin.NewAWSSignerForCLI() 32 | // plugin should finish execution in 10 seconds 33 | ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 34 | defer cancel() 35 | 36 | var pluginCli *cli.CLI 37 | var err error 38 | if os.Getenv(debugFlag) == "true" { 39 | log, logErr := logger.New() 40 | if logErr != nil { 41 | os.Exit(100) 42 | } 43 | defer log.Close() 44 | ctx = log.UpdateContext(ctx) 45 | pluginCli, err = cli.NewWithLogger(awsPlugin, log) 46 | } else { 47 | pluginCli, err = cli.New(awsPlugin) 48 | } 49 | if err != nil { 50 | _, _ = fmt.Fprintf(os.Stderr, "failed to create executable: %v\n", err) 51 | os.Exit(101) 52 | } 53 | pluginCli.Execute(ctx, os.Args) 54 | } 55 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples of using AWS Signer Notation Plugin with notation-go 2 | This package contains examples for using AWS Signer Notation plugin as library with [notation-go](https://github.com/notaryproject/notation-go). 3 | 4 | * [Sign Example](./sign) 5 | * [Verify Example](./verify) 6 | 7 | These examples are intended for __non-production use only__. 8 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module example 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0 7 | github.com/aws/aws-signer-notation-plugin v0.0.0-00010101000000-000000000000 8 | github.com/notaryproject/notation-go v1.3.2 9 | github.com/notaryproject/notation-plugin-framework-go v1.0.0 10 | ) 11 | 12 | require ( 13 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 14 | github.com/notaryproject/tspclient-go v1.0.0 // indirect 15 | github.com/veraison/go-cose v1.3.0 // indirect 16 | github.com/x448/float16 v0.8.4 // indirect 17 | ) 18 | 19 | require ( 20 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect 21 | github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect 22 | github.com/aws/aws-sdk-go-v2/config v1.29.14 23 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect 24 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 26 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/signer v1.27.2 31 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect 32 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect 33 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect 34 | github.com/aws/smithy-go v1.22.3 // indirect 35 | github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect 36 | github.com/go-ldap/ldap/v3 v3.4.10 // indirect; indirectgo get -u 37 | github.com/golang-jwt/jwt/v4 v4.5.2 // indirect 38 | github.com/google/uuid v1.6.0 // indirect 39 | github.com/notaryproject/notation-core-go v1.3.0 40 | github.com/opencontainers/go-digest v1.0.0 // indirect 41 | github.com/opencontainers/image-spec v1.1.1 // indirect 42 | golang.org/x/crypto v0.38.0 // indirect 43 | golang.org/x/mod v0.24.0 // indirect 44 | golang.org/x/sync v0.14.0 // indirect 45 | oras.land/oras-go/v2 v2.6.0 46 | ) 47 | 48 | replace github.com/aws/aws-signer-notation-plugin => ../ 49 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= 2 | github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= 3 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= 4 | github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= 5 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 6 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 7 | github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= 8 | github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= 9 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= 10 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 11 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 12 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 13 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 14 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 15 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 16 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 17 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 18 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 19 | github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0 h1:E+UTVTDH6XTSjqxHWRuY8nB6s+05UllneWxnycplHFk= 20 | github.com/aws/aws-sdk-go-v2/service/ecr v1.44.0/go.mod h1:iQ1skgw1XRK+6Lgkb0I9ODatAP72WoTILh0zXQ5DtbU= 21 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 22 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 23 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 24 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 25 | github.com/aws/aws-sdk-go-v2/service/signer v1.27.2 h1:yPuDQ0bNgRr0y3wTHqNb24mXjJhKn/LteC/kKxEZZ1I= 26 | github.com/aws/aws-sdk-go-v2/service/signer v1.27.2/go.mod h1:ah9nQOLyu0iCUzc8EBFWkScOCTl15idSD9zxICUiSFY= 27 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= 28 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 29 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= 30 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 31 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= 32 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 33 | github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= 34 | github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 37 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 38 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 39 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 40 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 41 | github.com/go-asn1-ber/asn1-ber v1.5.7 h1:DTX+lbVTWaTw1hQ+PbZPlnDZPEIs0SS/GCZAl535dDk= 42 | github.com/go-asn1-ber/asn1-ber v1.5.7/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= 43 | github.com/go-ldap/ldap/v3 v3.4.10 h1:ot/iwPOhfpNVgB1o+AVXljizWZ9JTp7YF5oeyONmcJU= 44 | github.com/go-ldap/ldap/v3 v3.4.10/go.mod h1:JXh4Uxgi40P6E9rdsYqpUtbW46D9UTjJ9QSwGRznplY= 45 | github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= 46 | github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 47 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 48 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 49 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 50 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 51 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 52 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 53 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 54 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 55 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 56 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 57 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 58 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 59 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 60 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 61 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 62 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 63 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 64 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 65 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 66 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 67 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 68 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 69 | github.com/notaryproject/notation-core-go v1.3.0 h1:mWJaw1QBpBxpjLSiKOjzbZvB+xh2Abzk14FHWQ+9Kfs= 70 | github.com/notaryproject/notation-core-go v1.3.0/go.mod h1:hzvEOit5lXfNATGNBT8UQRx2J6Fiw/dq/78TQL8aE64= 71 | github.com/notaryproject/notation-go v1.3.2 h1:4223iLXOHhEV7ZPzIUJEwwMkhlgzoYFCsMJvSH1Chb8= 72 | github.com/notaryproject/notation-go v1.3.2/go.mod h1:/1kuq5WuLF6Gaer5re0Z6HlkQRlKYO4EbWWT/L7J1Uw= 73 | github.com/notaryproject/notation-plugin-framework-go v1.0.0 h1:6Qzr7DGXoCgXEQN+1gTZWuJAZvxh3p8Lryjn5FaLzi4= 74 | github.com/notaryproject/notation-plugin-framework-go v1.0.0/go.mod h1:RqWSrTOtEASCrGOEffq0n8pSg2KOgKYiWqFWczRSics= 75 | github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4= 76 | github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs= 77 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 78 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 79 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 80 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 82 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 83 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 84 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 85 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 86 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 87 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 88 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 89 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 90 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 91 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 92 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 93 | github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7rk= 94 | github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc= 95 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 96 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 97 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 98 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 99 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 100 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 101 | golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= 102 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 103 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= 104 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 105 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 106 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 107 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 108 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 109 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 110 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 111 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 112 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= 113 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= 114 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 115 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 116 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 117 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 118 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 119 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 120 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 121 | golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= 122 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 123 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 124 | golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= 125 | golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= 126 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 127 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 129 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 130 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 131 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 132 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 133 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 134 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 135 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 136 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 137 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 138 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 139 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 140 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 141 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 142 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 143 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 144 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 145 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 146 | golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= 147 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 148 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 149 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 150 | golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= 151 | golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= 152 | golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= 153 | golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= 154 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= 155 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 156 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 157 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 158 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 159 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 160 | golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 161 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 162 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 163 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 164 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 165 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 166 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 167 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 168 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 169 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 170 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 171 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 172 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 173 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 174 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 175 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 176 | oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= 177 | oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= 178 | -------------------------------------------------------------------------------- /examples/sign/main.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | ) 20 | 21 | func main() { 22 | // variable required for signing 23 | ctx := context.Background() 24 | awsRegion := "us-west-2" // AWS region where you created signing profile and ECR image. 25 | ecrImageURI := "111122223333.dkr.ecr.region.amazonaws.com/curl@sha256:EXAMPLEHASH" // ECR image URI 26 | awsSignerProfileArn := "arn:aws:signer:region:111122223333:/signing-profiles/profile_name" // AWS Signer's signing profile ARN 27 | userMetadata := map[string]string{"buildId": "101"} // Optional, add if you want to add metadata to the signature, else use nil 28 | 29 | // signing 30 | signer, err := NewNotationSigner(ctx, awsRegion) 31 | if err != nil { 32 | panic(err) 33 | } 34 | err = signer.Sign(ctx, awsSignerProfileArn, ecrImageURI, userMetadata) 35 | if err != nil { 36 | panic(err) 37 | } 38 | fmt.Printf("Sucessfully signed artifact: %s.", ecrImageURI) 39 | } 40 | -------------------------------------------------------------------------------- /examples/sign/sign.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | 19 | "example/utils" 20 | 21 | "github.com/aws/aws-sdk-go-v2/service/ecr" 22 | awsplugin "github.com/aws/aws-signer-notation-plugin/plugin" 23 | "github.com/notaryproject/notation-core-go/signature/jws" 24 | "github.com/notaryproject/notation-go" 25 | "github.com/notaryproject/notation-go/signer" 26 | ) 27 | 28 | // NotationSigner facilitates signing of OCI artifacts using notation and AWS Signer plugin 29 | type NotationSigner struct { 30 | ecrClient *ecr.Client 31 | signerPlugin *awsplugin.AWSSignerPlugin 32 | } 33 | 34 | // NewNotationSigner creates various AWS service clients and returns NotationSigner 35 | func NewNotationSigner(ctx context.Context, region string) (*NotationSigner, error) { 36 | pl, err := utils.GetAWSSignerPlugin(ctx, region) 37 | if err != nil { 38 | return nil, err 39 | } 40 | ecrClient, err := utils.GetECRClient(ctx, region) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &NotationSigner{ 45 | ecrClient: ecrClient, 46 | signerPlugin: pl, 47 | }, nil 48 | } 49 | 50 | // Sign function signs the given reference stored in registry and pushes signature back to registry. 51 | func (n *NotationSigner) Sign(ctx context.Context, keyId, reference string, userMetadata map[string]string) error { 52 | ref, err := utils.ParseReference(reference) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | regClient, err := utils.GetNotationRepository(ctx, n.ecrClient, ref) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | opts := notation.SignOptions{ 63 | SignerSignOptions: notation.SignerSignOptions{ 64 | SignatureMediaType: jws.MediaTypeEnvelope, 65 | SigningAgent: "aws-signer-notation-go-example/1.0.0", 66 | }, 67 | ArtifactReference: reference, 68 | UserMetadata: userMetadata, 69 | } 70 | 71 | sigSigner, err := signer.NewFromPlugin(n.signerPlugin, keyId, map[string]string{}) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | _, err = notation.Sign(ctx, sigSigner, regClient, opts) 77 | return err 78 | } 79 | -------------------------------------------------------------------------------- /examples/utils/ecr.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package utils 15 | 16 | import ( 17 | "context" 18 | "encoding/base64" 19 | "strings" 20 | "time" 21 | 22 | "oras.land/oras-go/v2/registry" 23 | "oras.land/oras-go/v2/registry/remote" 24 | "oras.land/oras-go/v2/registry/remote/auth" 25 | 26 | "github.com/aws/aws-sdk-go-v2/config" 27 | "github.com/aws/aws-sdk-go-v2/service/ecr" 28 | notationregistry "github.com/notaryproject/notation-go/registry" 29 | ) 30 | 31 | type repoAndExpiry struct { 32 | repo notationregistry.Repository 33 | expiry time.Time 34 | } 35 | 36 | // credentialCache is unbounded cache used to store notationregistry.Repository. 37 | // The cache is required since Amazon ECR credentials expire after 12 hours. 38 | var credentialCache = map[string]repoAndExpiry{} 39 | 40 | // GetECRClient returns ecr client for give region. 41 | func GetECRClient(ctx context.Context, region string) (*ecr.Client, error) { 42 | awsConfig, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return ecr.NewFromConfig(awsConfig), nil 48 | } 49 | 50 | // GetNotationRepository creates notationregistry.Repository required to access artifacts in Amazon ECR for sign and verify operations. 51 | func GetNotationRepository(ctx context.Context, client *ecr.Client, ref registry.Reference) (notationregistry.Repository, error) { 52 | repoPass, ok := credentialCache[ref.Host()] 53 | // Check if Repository object exists in cache and is not expired 54 | if ok && time.Now().Before(repoPass.expiry) { 55 | return repoPass.repo, nil 56 | } 57 | 58 | // else fetch credential from ECR 59 | cred, expiry, err := getECRCredentials(ctx, client) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | authClient := &auth.Client{ 65 | Credential: auth.StaticCredential(ref.Host(), cred), 66 | Cache: auth.NewCache(), 67 | ClientID: "example-notation-go", 68 | } 69 | authClient.SetUserAgent("aws-signer-notation-go-example/1.0") 70 | 71 | remoteRepo := &remote.Repository{ 72 | Client: authClient, 73 | Reference: ref, 74 | } 75 | remoteRepo.SetReferrersCapability(false) 76 | 77 | notationRepo := notationregistry.NewRepository(remoteRepo) 78 | credentialCache[ref.Host()] = repoAndExpiry{ 79 | repo: notationRepo, 80 | expiry: *expiry, 81 | } 82 | return notationRepo, nil 83 | } 84 | 85 | func getECRCredentials(ctx context.Context, client *ecr.Client) (auth.Credential, *time.Time, error) { 86 | token, err := client.GetAuthorizationToken(ctx, &ecr.GetAuthorizationTokenInput{}) 87 | if err != nil { 88 | return auth.Credential{}, nil, err 89 | } 90 | authData := token.AuthorizationData[0] 91 | creds, err := base64.StdEncoding.DecodeString(*authData.AuthorizationToken) 92 | if err != nil { 93 | return auth.Credential{}, nil, err 94 | } 95 | 96 | // Get password from credential pair 97 | credsSplit := strings.Split(string(creds), ":") 98 | credential := auth.Credential{ 99 | Username: "AWS", 100 | Password: credsSplit[1], 101 | } 102 | 103 | return credential, authData.ExpiresAt, nil 104 | } 105 | -------------------------------------------------------------------------------- /examples/utils/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package utils 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | 20 | "oras.land/oras-go/v2/registry" 21 | 22 | "github.com/aws/aws-sdk-go-v2/config" 23 | "github.com/aws/aws-sdk-go-v2/service/signer" 24 | awsplugin "github.com/aws/aws-signer-notation-plugin/plugin" 25 | ) 26 | 27 | func ParseReference(reference string) (registry.Reference, error) { 28 | ref, err := registry.ParseReference(reference) 29 | if err != nil { 30 | return registry.Reference{}, fmt.Errorf("%q: %w. Expecting /@", reference, err) 31 | } 32 | if ref.Reference == "" { 33 | return registry.Reference{}, fmt.Errorf("%q: invalid reference. Expecting /@", reference) 34 | } 35 | if err := ref.ValidateReferenceAsDigest(); err != nil { 36 | return registry.Reference{}, fmt.Errorf("%q: tag resolution not supported. Expecting /@", reference) 37 | 38 | } 39 | return ref, nil 40 | } 41 | 42 | // GetAWSSignerPlugin returns the AWS Signer's Notation plugin 43 | func GetAWSSignerPlugin(ctx context.Context, region string) (*awsplugin.AWSSignerPlugin, error) { 44 | awsConfig, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return awsplugin.NewAWSSigner(signer.NewFromConfig(awsConfig)), nil 50 | } 51 | -------------------------------------------------------------------------------- /examples/verify/main.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | "crypto/x509" 19 | "encoding/pem" 20 | "fmt" 21 | "io" 22 | "net/http" 23 | 24 | "github.com/notaryproject/notation-go/verifier/trustpolicy" 25 | ) 26 | 27 | // awsSignerRootURL is the url of AWS Signer's root certificate. The URL is copied from AWS Signer's documentation 28 | // https://docs.aws.amazon.com/signer/latest/developerguide/image-signing-prerequisites.html 29 | const awsSignerRootURL = "https://d2hvyiie56hcat.cloudfront.net/aws-signer-notation-root.cert" 30 | 31 | // Downloads and caches AWS Signer's Root Certificate required for signature verification. 32 | var awsSignerRoot = getAWSSignerRootCert() 33 | 34 | func main() { 35 | // variable required for verification 36 | ctx := context.Background() 37 | awsRegion := "us-west-2" // AWS region where you created signing profile and ECR image. 38 | ecrImageURI := "111122223333.dkr.ecr.region.amazonaws.com/curl@sha256:EXAMPLEHASH" // ECR image URI 39 | awsSignerProfileArn := "arn:aws:signer:region:111122223333:/signing-profiles/profile_name" // AWS Signer's signing profile ARN 40 | userMetadata := map[string]string{"buildId": "101"} // Optional, add if you want to verify metadata in the signature, else use nil 41 | tPolicy := getTrustPolicy(awsSignerProfileArn) 42 | 43 | // signature verification 44 | verifier, err := NewNotationVerifier(ctx, awsRegion) 45 | if err != nil { 46 | panic(err) 47 | } 48 | outcome, err := verifier.Verify(context.Background(), ecrImageURI, []*x509.Certificate{awsSignerRoot}, tPolicy, userMetadata) 49 | if err != nil { 50 | panic(err) 51 | } 52 | fmt.Printf("Sucessfully verified signature associated with %s.\n", ecrImageURI) 53 | for _, out := range outcome.VerificationResults { 54 | fmt.Printf(" individual validation result: %+v \n", out) 55 | } 56 | } 57 | 58 | // getTrustPolicy returns global trust policy 59 | func getTrustPolicy(awsSignerProfileArn string) *trustpolicy.Document { 60 | return &trustpolicy.Document{ 61 | Version: "1.0", 62 | TrustPolicies: []trustpolicy.TrustPolicy{ 63 | { 64 | Name: "global_trust_policy", 65 | RegistryScopes: []string{"*"}, 66 | SignatureVerification: trustpolicy.SignatureVerification{ 67 | VerificationLevel: "strict", 68 | }, 69 | TrustStores: []string{"signingAuthority:aws-signer-ts"}, 70 | TrustedIdentities: []string{awsSignerProfileArn}, 71 | }, 72 | }, 73 | } 74 | } 75 | 76 | // getAWSSignerRootCert returns the AWS Signer's root certificate 77 | func getAWSSignerRootCert() *x509.Certificate { 78 | resp, err := http.Get(awsSignerRootURL) 79 | if err != nil { 80 | panic(fmt.Sprintf("failed to get AWS Signer's root certificate: %s", err.Error())) // handle error 81 | } 82 | defer resp.Body.Close() 83 | 84 | data, err := io.ReadAll(resp.Body) 85 | if err != nil { 86 | panic(fmt.Sprintf("failed to get AWS Signer's root certificate: %s", err.Error())) // handle error 87 | } 88 | 89 | block, _ := pem.Decode(data) 90 | switch block.Type { 91 | case "CERTIFICATE": 92 | cert, err := x509.ParseCertificate(block.Bytes) 93 | if err != nil { 94 | panic(fmt.Sprintf("failed to parse AWS Signer's root certificate: %s", err.Error())) // handle error 95 | } 96 | return cert 97 | default: 98 | panic(fmt.Sprintf("failed to parse AWS Signer's root certificate: unsupported certificate type :%s", block.Type)) // handle error 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /examples/verify/verify.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package main 15 | 16 | import ( 17 | "context" 18 | "crypto/x509" 19 | "fmt" 20 | 21 | "example/utils" 22 | 23 | "github.com/aws/aws-sdk-go-v2/service/ecr" 24 | awsplugin "github.com/aws/aws-signer-notation-plugin/plugin" 25 | _ "github.com/notaryproject/notation-core-go/signature/jws" 26 | "github.com/notaryproject/notation-go" 27 | "github.com/notaryproject/notation-go/verifier" 28 | "github.com/notaryproject/notation-go/verifier/trustpolicy" 29 | "github.com/notaryproject/notation-go/verifier/truststore" 30 | "github.com/notaryproject/notation-plugin-framework-go/plugin" 31 | ) 32 | 33 | // NotationVerifier facilitates signature verification for OCI artifacts using notation and AWS Signer plugin 34 | type NotationVerifier struct { 35 | ecrClient *ecr.Client 36 | signerPlugin *awsplugin.AWSSignerPlugin 37 | } 38 | 39 | // NewNotationVerifier creates various AWS service clients and returns NotationVerifier 40 | func NewNotationVerifier(ctx context.Context, region string) (*NotationVerifier, error) { 41 | pl, err := utils.GetAWSSignerPlugin(ctx, region) 42 | if err != nil { 43 | return nil, err 44 | } 45 | ecrClient, err := utils.GetECRClient(ctx, region) 46 | if err != nil { 47 | return nil, err 48 | } 49 | return &NotationVerifier{ 50 | ecrClient: ecrClient, 51 | signerPlugin: pl, 52 | }, nil 53 | } 54 | 55 | // The Verify function verifies the signature stored in the registry against the provided truststore and trust policy using the Notation and AWS Signer plugin 56 | func (n *NotationVerifier) Verify(ctx context.Context, reference string, trustedRoots []*x509.Certificate, tPolicy *trustpolicy.Document, userMetadata map[string]string) (*notation.VerificationOutcome, error) { 57 | ref, err := utils.ParseReference(reference) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | regClient, err := utils.GetNotationRepository(ctx, n.ecrClient, ref) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | tStore := &customTrustStore{certs: trustedRoots} 68 | pluginManager := &customPluginManager{awsPlugin: n.signerPlugin} 69 | sigVerifier, err := verifier.New(tPolicy, tStore, pluginManager) 70 | if err != nil { 71 | return nil, err 72 | } 73 | verifyOpts := notation.VerifyOptions{ 74 | ArtifactReference: reference, 75 | MaxSignatureAttempts: 100, 76 | UserMetadata: userMetadata, 77 | } 78 | _, outcome, err := notation.Verify(ctx, sigVerifier, regClient, verifyOpts) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | return outcome[len(outcome)-1], nil 84 | } 85 | 86 | // customTrustStore implements truststore.X509TrustStore and returns the trusted certificates for a given trust-store. 87 | // This implementation currently returns only AWS Signer trusted root but can be extended to support multiple trust-stores. 88 | type customTrustStore struct { 89 | certs []*x509.Certificate 90 | } 91 | 92 | func (ts *customTrustStore) GetCertificates(_ context.Context, _ truststore.Type, _ string) ([]*x509.Certificate, error) { 93 | return ts.certs, nil 94 | } 95 | 96 | // customPluginManager implements plugin.Manager. 97 | // This implementation currently supports AWS Signer plugin but can be extended to support any plugin. 98 | type customPluginManager struct { 99 | awsPlugin *awsplugin.AWSSignerPlugin 100 | } 101 | 102 | func (p *customPluginManager) Get(_ context.Context, name string) (plugin.Plugin, error) { 103 | if name == awsplugin.Name { 104 | return p.awsPlugin, nil 105 | } 106 | return nil, fmt.Errorf("%s plugin not supported", name) 107 | } 108 | 109 | func (p *customPluginManager) List(_ context.Context) ([]string, error) { 110 | return []string{awsplugin.Name}, nil 111 | } 112 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | go 1.23.1 2 | 3 | module github.com/aws/aws-signer-notation-plugin 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.36.3 7 | github.com/aws/aws-sdk-go-v2/config v1.29.14 8 | github.com/aws/aws-sdk-go-v2/service/signer v1.27.2 9 | github.com/aws/smithy-go v1.22.3 10 | github.com/golang/mock v1.6.0 11 | github.com/notaryproject/notation-plugin-framework-go v1.0.0 12 | github.com/stretchr/testify v1.10.0 13 | ) 14 | 15 | require ( 16 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 // indirect 17 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 18 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 19 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 20 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 23 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 // indirect 24 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 // indirect 25 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 // indirect 26 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 27 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 28 | gopkg.in/yaml.v3 v3.0.1 // indirect 29 | ) 30 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM= 2 | github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg= 3 | github.com/aws/aws-sdk-go-v2/config v1.29.14 h1:f+eEi/2cKCg9pqKBoAIwRGzVb70MRKqWX4dg1BDcSJM= 4 | github.com/aws/aws-sdk-go-v2/config v1.29.14/go.mod h1:wVPHWcIFv3WO89w0rE10gzf17ZYy+UVS1Geq8Iei34g= 5 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67 h1:9KxtdcIA/5xPNQyZRgUSpYOE6j9Bc4+D7nZua0KGYOM= 6 | github.com/aws/aws-sdk-go-v2/credentials v1.17.67/go.mod h1:p3C44m+cfnbv763s52gCqrjaqyPikj9Sg47kUVaNZQQ= 7 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw= 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M= 9 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q= 10 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY= 11 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0= 12 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q= 13 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= 14 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= 15 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE= 16 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA= 17 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM= 18 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY= 19 | github.com/aws/aws-sdk-go-v2/service/signer v1.27.2 h1:yPuDQ0bNgRr0y3wTHqNb24mXjJhKn/LteC/kKxEZZ1I= 20 | github.com/aws/aws-sdk-go-v2/service/signer v1.27.2/go.mod h1:ah9nQOLyu0iCUzc8EBFWkScOCTl15idSD9zxICUiSFY= 21 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3 h1:1Gw+9ajCV1jogloEv1RRnvfRFia2cL6c9cuKV2Ps+G8= 22 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.3/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI= 23 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1 h1:hXmVKytPfTy5axZ+fYbR5d0cFmC3JvwLm5kM83luako= 24 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.1/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs= 25 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19 h1:1XuUZ8mYJw9B6lzAkXhqHlJd/XvaX32evhproijJEZY= 26 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.19/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4= 27 | github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k= 28 | github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= 29 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 30 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 32 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 33 | github.com/notaryproject/notation-plugin-framework-go v1.0.0 h1:6Qzr7DGXoCgXEQN+1gTZWuJAZvxh3p8Lryjn5FaLzi4= 34 | github.com/notaryproject/notation-plugin-framework-go v1.0.0/go.mod h1:RqWSrTOtEASCrGOEffq0n8pSg2KOgKYiWqFWczRSics= 35 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 36 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 37 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 38 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 39 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 40 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 41 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 42 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 43 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 44 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 45 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 46 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 47 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 48 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 49 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 50 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 54 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 55 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 56 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 57 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 58 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 59 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 60 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 61 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 63 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 64 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 65 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 66 | -------------------------------------------------------------------------------- /internal/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package client creates AWS service like AWS Signer client required by plugin. 15 | package client 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | 21 | "github.com/aws/aws-signer-notation-plugin/internal/logger" 22 | "github.com/aws/aws-signer-notation-plugin/internal/version" 23 | 24 | "github.com/aws/aws-sdk-go-v2/aws" 25 | "github.com/aws/aws-sdk-go-v2/config" 26 | "github.com/aws/aws-sdk-go-v2/service/signer" 27 | "github.com/aws/smithy-go/logging" 28 | "github.com/aws/smithy-go/middleware" 29 | "github.com/notaryproject/notation-plugin-framework-go/plugin" 30 | 31 | awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" 32 | ) 33 | 34 | const ( 35 | configKeyAwsProfile = "aws-profile" 36 | configKeyAwsRegion = "aws-region" 37 | configKeySignerEndpoint = "aws-signer-endpoint-url" 38 | ) 39 | 40 | // NewAWSSigner creates new AWS Signer client from given pluginConfig 41 | func NewAWSSigner(ctx context.Context, pluginConfig map[string]string) (*signer.Client, error) { 42 | log := logger.GetLogger(ctx) 43 | log.Debugln("Initializing Signer Client") 44 | loadOptions := getLoadOptions(ctx, pluginConfig) 45 | 46 | // Use default config for aws credentials 47 | defaultConfig, err := config.LoadDefaultConfig(ctx, loadOptions...) 48 | if err != nil { 49 | return nil, plugin.NewGenericError(err.Error()) 50 | } 51 | s, err := signer.NewFromConfig(defaultConfig), nil 52 | 53 | log.Debugln("Initialized Signer Client") 54 | return s, err 55 | } 56 | 57 | func getLoadOptions(ctx context.Context, pluginConfig map[string]string) []func(*config.LoadOptions) error { 58 | log := logger.GetLogger(ctx) 59 | var loadOptions []func(*config.LoadOptions) error 60 | if customEndpoint, ok := pluginConfig[configKeySignerEndpoint]; ok { 61 | customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { 62 | if service == signer.ServiceID && customEndpoint != "" { 63 | log.Debug("AWS Signer endpoint override: " + customEndpoint) 64 | return aws.Endpoint{ 65 | PartitionID: "aws", 66 | URL: customEndpoint, 67 | SigningRegion: region, 68 | }, nil 69 | } 70 | // returning EndpointNotFoundError will allow the service to fall back to its default resolution 71 | return aws.Endpoint{}, &aws.EndpointNotFoundError{} 72 | }) 73 | loadOptions = append(loadOptions, config.WithEndpointResolverWithOptions(customResolver)) 74 | } 75 | 76 | if region, ok := pluginConfig[configKeyAwsRegion]; ok { 77 | loadOptions = append(loadOptions, config.WithRegion(region)) 78 | log.Debugf("AWS Signer region override: %s\n", region) 79 | } 80 | 81 | if credentialProfile, ok := pluginConfig[configKeyAwsProfile]; ok { 82 | loadOptions = append(loadOptions, config.WithSharedConfigProfile(credentialProfile)) 83 | log.Debugf("AWS Signer credential profile: %s\n", credentialProfile) 84 | } 85 | 86 | loadOptions = append(loadOptions, config.WithAPIOptions([]func(*middleware.Stack) error{ 87 | awsmiddleware.AddUserAgentKeyValue("aws-signer-caller", "NotationPlugin/"+version.GetVersion()), 88 | })) 89 | 90 | if log.IsDebug() { 91 | loadOptions = append(loadOptions, config.WithClientLogMode(aws.LogRequestWithBody|aws.LogResponseWithBody)) 92 | loadOptions = append(loadOptions, config.WithLogConfigurationWarnings(true)) 93 | loadOptions = append(loadOptions, config.WithLogger(logging.LoggerFunc(func(_ logging.Classification, format string, v ...interface{}) { 94 | log.Debugf("AWS call %s\n", fmt.Sprintf(format, v)) 95 | }))) 96 | } 97 | 98 | return loadOptions 99 | } 100 | -------------------------------------------------------------------------------- /internal/client/client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package client 15 | 16 | import ( 17 | "context" 18 | "os" 19 | "testing" 20 | 21 | "github.com/aws/aws-signer-notation-plugin/internal/logger" 22 | "github.com/stretchr/testify/assert" 23 | ) 24 | 25 | func TestNewAWSSigner(t *testing.T) { 26 | tests := map[string]map[string]string{ 27 | "emptyConfig": {}, 28 | configKeySignerEndpoint: {configKeySignerEndpoint: "https://127.0.0.1:80/some-endpoint"}, 29 | configKeyAwsRegion: {configKeyAwsRegion: "us-east-1"}, 30 | } 31 | for name, config := range tests { 32 | t.Run(name, func(t *testing.T) { 33 | _, err := NewAWSSigner(context.TODO(), config) 34 | assert.Nil(t, err, "NewAWSSigner returned error") 35 | }) 36 | } 37 | } 38 | 39 | func TestNewAWSSigner_Debug(t *testing.T) { 40 | // we need this because build fleet might not have XDG_CONFIG_HOME set 41 | tempDir, _ := os.MkdirTemp("", "tempDir") 42 | defer os.RemoveAll(tempDir) 43 | t.Setenv("XDG_CONFIG_HOME", os.TempDir()) 44 | 45 | ctx := context.TODO() 46 | dl, _ := logger.New() 47 | ctx = dl.UpdateContext(ctx) 48 | _, err := NewAWSSigner(ctx, map[string]string{}) 49 | assert.Nil(t, err, "NewAWSSigner returned error") 50 | } 51 | 52 | func TestNewAWSSigner_InvalidProfile(t *testing.T) { 53 | // we need this because build fleet might not have XDG_CONFIG_HOME set 54 | tempDir, _ := os.MkdirTemp("", "tempDir") 55 | defer os.RemoveAll(tempDir) 56 | t.Setenv("XDG_CONFIG_HOME", os.TempDir()) 57 | 58 | ctx := context.TODO() 59 | dl, _ := logger.New() 60 | ctx = dl.UpdateContext(ctx) 61 | _, err := NewAWSSigner(ctx, map[string]string{configKeyAwsProfile: "someProfile"}) 62 | assert.Error(t, err, "NewAWSSigner returned error") 63 | } 64 | -------------------------------------------------------------------------------- /internal/client/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package client 15 | 16 | import ( 17 | "context" 18 | 19 | "github.com/aws/aws-sdk-go-v2/service/signer" 20 | ) 21 | 22 | // Interface facilitates unit testing 23 | type Interface interface { 24 | SignPayload(ctx context.Context, params *signer.SignPayloadInput, optFns ...func(*signer.Options)) (*signer.SignPayloadOutput, error) 25 | GetRevocationStatus(ctx context.Context, params *signer.GetRevocationStatusInput, optFns ...func(*signer.Options)) (*signer.GetRevocationStatusOutput, error) 26 | } 27 | -------------------------------------------------------------------------------- /internal/logger/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package logger provides logging functionality. 15 | package logger 16 | 17 | import ( 18 | "context" 19 | "fmt" 20 | "os" 21 | "path/filepath" 22 | "time" 23 | 24 | "github.com/aws/aws-signer-notation-plugin/internal/version" 25 | ) 26 | 27 | type contextKey int 28 | 29 | const logContextKey contextKey = iota 30 | 31 | var userConfigDir = os.UserConfigDir // for unit test 32 | var discardLogger = &debugLogger{} 33 | 34 | type debugLogger struct { 35 | file *os.File 36 | } 37 | 38 | // New creates a new debugLogger instance 39 | func New() (*debugLogger, error) { 40 | cfgDir, err := userConfigDir() 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | path := filepath.Join(cfgDir, "notation-aws-signer", "plugin.log") 46 | if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { 47 | return nil, err 48 | } 49 | 50 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | dl := &debugLogger{ 56 | file: file, 57 | } 58 | 59 | dl.Debugln("-----------------------------------------------------------------------------------------") 60 | dl.Debugf("Logs from execution of AWS signer plugin version: %s\n", version.GetVersion()) 61 | return dl, nil 62 | } 63 | 64 | // Close closes the logger and associated resources 65 | func (l *debugLogger) Close() { 66 | if l.file != nil { 67 | err := l.file.Close() 68 | if err != nil { 69 | l.Errorf("error while closing the log file", err) 70 | } 71 | } 72 | } 73 | 74 | // GetLogger returns the logger instance 75 | func GetLogger(ctx context.Context) *debugLogger { 76 | debugLogger, ok := ctx.Value(logContextKey).(*debugLogger) 77 | if !ok { 78 | return discardLogger 79 | } 80 | return debugLogger 81 | } 82 | 83 | // UpdateContext returns context with the logger entry 84 | func (l *debugLogger) UpdateContext(ctx context.Context) context.Context { 85 | return context.WithValue(ctx, logContextKey, l) 86 | } 87 | 88 | // IsDebug returns true if Debug log is enabled 89 | func (l *debugLogger) IsDebug() bool { 90 | return l.file != nil 91 | } 92 | 93 | func (l *debugLogger) Debug(args ...interface{}) { 94 | l.log("DEBUG", args...) 95 | } 96 | 97 | func (l *debugLogger) Debugf(format string, args ...interface{}) { 98 | l.logf("DEBUG", format, args...) 99 | } 100 | 101 | func (l *debugLogger) Debugln(args ...interface{}) { 102 | l.logln("DEBUG", args...) 103 | } 104 | 105 | func (l *debugLogger) Info(args ...interface{}) { 106 | l.log("INFO", args...) 107 | } 108 | 109 | func (l *debugLogger) Infof(format string, args ...interface{}) { 110 | l.logf("INFO", format, args...) 111 | } 112 | 113 | func (l *debugLogger) Infoln(args ...interface{}) { 114 | l.logln("INFO", args...) 115 | } 116 | 117 | func (l *debugLogger) Warn(args ...interface{}) { 118 | l.log("WARN", args...) 119 | } 120 | 121 | func (l *debugLogger) Warnf(format string, args ...interface{}) { 122 | l.logf("WARN", format, args...) 123 | } 124 | 125 | func (l *debugLogger) Warnln(args ...interface{}) { 126 | l.logln("WARN", args...) 127 | } 128 | 129 | func (l *debugLogger) Error(args ...interface{}) { 130 | l.log("ERROR", args...) 131 | } 132 | 133 | func (l *debugLogger) Errorf(format string, args ...interface{}) { 134 | l.logf("ERROR", format, args...) 135 | } 136 | 137 | func (l *debugLogger) Errorln(args ...interface{}) { 138 | l.logln("ERROR", args...) 139 | } 140 | 141 | func (l *debugLogger) logf(levelPrefix, format string, args ...interface{}) { 142 | if l.file != nil { 143 | _, _ = fmt.Fprintf(l.file, "%s [%s] "+format, append([]interface{}{time.Now().Format(time.RFC3339Nano), levelPrefix}, args...)...) 144 | } 145 | } 146 | 147 | func (l *debugLogger) log(levelPrefix string, args ...interface{}) { 148 | l.logf(levelPrefix, "%v", args...) 149 | } 150 | 151 | func (l *debugLogger) logln(levelPrefix string, args ...interface{}) { 152 | l.logf(levelPrefix, "%v\n", args...) 153 | } 154 | -------------------------------------------------------------------------------- /internal/logger/logger_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package logger 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/stretchr/testify/assert" 25 | ) 26 | 27 | func TestDebugLogger(t *testing.T) { 28 | t.Run("DebugLoggerNotEnabled", func(t *testing.T) { 29 | tempDir, _ := os.MkdirTemp("", "tempDir") 30 | defer os.RemoveAll(tempDir) 31 | 32 | userConfigDir = func() (string, error) { 33 | return tempDir, nil 34 | } 35 | logFilePath := filepath.Join(tempDir, "notation-aws-signer", "plugin.log") 36 | GetLogger(context.TODO()).Debug("This is a Debug log!") 37 | _, err := os.ReadFile(logFilePath) 38 | if err == nil { 39 | t.Fatalf("debug log file found at " + logFilePath) 40 | } 41 | }) 42 | 43 | t.Run("DebugLoggerEnabled", func(t *testing.T) { 44 | logger, logFilePath := setupTestLogger(t) 45 | msg := "This is a Debug log!" 46 | logger.Debug(msg) 47 | validateLogEntry(logFilePath, "[DEBUG] "+msg, t) 48 | }) 49 | 50 | t.Run("UnableToCreateFile", func(t *testing.T) { 51 | userConfigDir = func() (string, error) { 52 | return "", fmt.Errorf("expected error thrown") 53 | } 54 | _, err := New() 55 | assert.Error(t, err, "expected error not found") 56 | }) 57 | } 58 | 59 | func TestDebugLogger_UpdateContext(t *testing.T) { 60 | ctx := context.TODO() 61 | logger, _ := setupTestLogger(t) 62 | ctx = logger.UpdateContext(ctx) 63 | ctxLogger := GetLogger(ctx) 64 | assert.Equal(t, logger, ctxLogger, "debugLogger in context should match the original logger") 65 | } 66 | 67 | func TestDebugLogger_Info(t *testing.T) { 68 | logger, logFilePath := setupTestLogger(t) 69 | msg := "This is a Info log!" 70 | logger.Info(msg) 71 | validateLogEntry(logFilePath, "[INFO] "+msg, t) 72 | 73 | logger.Infoln(msg) 74 | validateLogEntry(logFilePath, "[INFO] "+msg+"\n", t) 75 | } 76 | 77 | func TestDebugLogger_Warn(t *testing.T) { 78 | logger, logFilePath := setupTestLogger(t) 79 | msg := "This is a Warn log!" 80 | logger.Warn(msg) 81 | validateLogEntry(logFilePath, "[WARN] "+msg, t) 82 | 83 | logger.Warnln(msg) 84 | validateLogEntry(logFilePath, "[WARN] "+msg+"\n", t) 85 | } 86 | 87 | func TestDebugLogger_Debug(t *testing.T) { 88 | logger, logFilePath := setupTestLogger(t) 89 | msg := "This is a Debug log!" 90 | logger.Debug(msg) 91 | validateLogEntry(logFilePath, "[DEBUG] "+msg, t) 92 | 93 | logger.Debugln(msg) 94 | validateLogEntry(logFilePath, "[DEBUG] "+msg+"\n", t) 95 | } 96 | 97 | func TestDebugLogger_Error(t *testing.T) { 98 | logger, logFilePath := setupTestLogger(t) 99 | msg := "This is a Debug log!" 100 | logger.Error(msg) 101 | validateLogEntry(logFilePath, "[ERROR] "+msg, t) 102 | 103 | logger.Errorln(msg) 104 | validateLogEntry(logFilePath, "[ERROR] "+msg+"\n", t) 105 | } 106 | 107 | func setupTestLogger(t *testing.T) (*debugLogger, string) { 108 | userConfigDir = func() (string, error) { 109 | return os.TempDir(), nil 110 | } 111 | expectedLogFilePath := filepath.Join(os.TempDir(), "notation-aws-signer", "plugin.log") 112 | logger, _ := New() 113 | t.Cleanup(func() { 114 | logger.Close() 115 | os.RemoveAll(expectedLogFilePath) 116 | }) 117 | 118 | return logger, expectedLogFilePath 119 | } 120 | 121 | func validateLogEntry(logfilePath, expectedLogLine string, t *testing.T) { 122 | logs, err := os.ReadFile(logfilePath) 123 | if err != nil { 124 | t.Fatalf("Log file not found at %s. Error: %v", logfilePath, err) 125 | } 126 | if !strings.HasSuffix(string(logs), expectedLogLine) { 127 | t.Errorf("Expected log message '%s' not found in the log file '%s'", expectedLogLine, string(logs)) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /internal/signer/signer.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package signer provides functionality to generate signatures using AWS Signer 15 | // in accordance with the NotaryProject Plugin contract. 16 | package signer 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "fmt" 22 | "strings" 23 | 24 | "github.com/aws/aws-signer-notation-plugin/internal/client" 25 | "github.com/aws/aws-signer-notation-plugin/internal/logger" 26 | 27 | "github.com/aws/aws-sdk-go-v2/aws/arn" 28 | "github.com/aws/aws-sdk-go-v2/aws/transport/http" 29 | "github.com/aws/aws-sdk-go-v2/service/signer" 30 | "github.com/aws/smithy-go" 31 | 32 | "github.com/notaryproject/notation-plugin-framework-go/plugin" 33 | ) 34 | 35 | const ( 36 | mediaTypeJwsEnvelope = "application/jose+json" 37 | errorMsgMalformedSigningProfileFmt = "%s is not a valid AWS Signer signing profile or signing profile version ARN." 38 | errorMSGExpiryPassed = "AWSSigner plugin doesn't support -e (--expiry) argument. Please use signing profile to set signature expiry." 39 | ) 40 | 41 | // Signer generates signature generated using AWS Signer. 42 | type Signer struct { 43 | awssigner client.Interface 44 | } 45 | 46 | // New returns Signer given an AWS Signer client. 47 | func New(s client.Interface) *Signer { 48 | return &Signer{awssigner: s} 49 | } 50 | 51 | // GenerateEnvelope generates signature envelope by calling AWS Signer 52 | func (s *Signer) GenerateEnvelope(ctx context.Context, request *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) { 53 | log := logger.GetLogger(ctx) 54 | 55 | log.Debug("validating request") 56 | if err := validate(request); err != nil { 57 | return nil, err 58 | } 59 | log.Debug("succeeded request validation") 60 | 61 | log.Debug("validating signing profile") 62 | signingProfileArn, err := arn.Parse(request.KeyID) 63 | if err != nil { 64 | return nil, plugin.NewValidationErrorf(errorMsgMalformedSigningProfileFmt, request.KeyID) 65 | } 66 | signingProfileName, err := getProfileName(signingProfileArn) 67 | if err != nil { 68 | return nil, err 69 | } 70 | log.Debug("succeeded signing profile validation") 71 | 72 | log.Debug("calling AWS Signer's SignPayload API") 73 | input := &signer.SignPayloadInput{ 74 | Payload: request.Payload, 75 | ProfileName: &signingProfileName, 76 | PayloadFormat: &request.PayloadType, 77 | ProfileOwner: &signingProfileArn.AccountID, 78 | } 79 | output, err := s.awssigner.SignPayload(ctx, input) 80 | if err != nil { 81 | log.Debugf("failed AWS Signer's SignPayload API call with error: %v", err) 82 | return nil, parseAwsError(err) 83 | } 84 | 85 | res := &plugin.GenerateEnvelopeResponse{ 86 | SignatureEnvelope: output.Signature, 87 | SignatureEnvelopeType: request.SignatureEnvelopeType, 88 | Annotations: output.Metadata} 89 | log.Debugf("succeeded AWS Signer's SignPayload API call. output: %s", res) 90 | 91 | return res, nil 92 | } 93 | 94 | func getProfileName(arn arn.ARN) (string, error) { 95 | //resource name will be in format /signing-profiles/ProfileName 96 | profileArnParts := strings.Split(arn.Resource, "/") 97 | if len(profileArnParts) != 3 { 98 | return "", plugin.NewValidationErrorf(errorMsgMalformedSigningProfileFmt, arn) 99 | } 100 | return profileArnParts[2], nil 101 | } 102 | 103 | func validate(request *plugin.GenerateEnvelopeRequest) error { 104 | if request.ExpiryDurationInSeconds != 0 { 105 | return plugin.NewError(plugin.ErrorCodeValidation, errorMSGExpiryPassed) 106 | } 107 | if request.ContractVersion != plugin.ContractVersion { 108 | return plugin.NewUnsupportedContractVersionError(request.ContractVersion) 109 | } 110 | if request.SignatureEnvelopeType != mediaTypeJwsEnvelope { 111 | return plugin.NewUnsupportedError(fmt.Sprintf("envelope type %q", request.SignatureEnvelopeType)) 112 | } 113 | return nil 114 | } 115 | 116 | // ParseAwsError converts error from SignPayload API to plugin error 117 | func parseAwsError(err error) *plugin.Error { 118 | var apiError smithy.APIError 119 | if errors.As(err, &apiError) { 120 | var re *http.ResponseError 121 | errMsgSuffix := "" 122 | if errors.As(err, &re) { 123 | errMsgSuffix = fmt.Sprintf(" RequestID: %s.", re.ServiceRequestID()) 124 | } 125 | errMsg := fmt.Sprintf("Failed to call AWSSigner. Error: %s.%s", apiError.ErrorMessage(), errMsgSuffix) 126 | switch apiError.ErrorCode() { 127 | case "NotFoundException", "ResourceNotFoundException", "ValidationException", "BadRequestException": 128 | return plugin.NewValidationError(errMsg) 129 | case "ThrottlingException": 130 | return plugin.NewError(plugin.ErrorCodeThrottled, errMsg) 131 | case "AccessDeniedException": 132 | return plugin.NewError(plugin.ErrorCodeAccessDenied, errMsg) 133 | default: 134 | return plugin.NewGenericError(errMsg) 135 | } 136 | } 137 | return plugin.NewGenericError(err.Error()) 138 | } 139 | -------------------------------------------------------------------------------- /internal/signer/signer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package signer 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | nethttp "net/http" 20 | "testing" 21 | 22 | "github.com/aws/aws-sdk-go-v2/aws/transport/http" 23 | "github.com/aws/aws-sdk-go-v2/service/signer" 24 | "github.com/aws/aws-signer-notation-plugin/internal/client" 25 | "github.com/aws/smithy-go" 26 | smithyhttp "github.com/aws/smithy-go/transport/http" 27 | "github.com/golang/mock/gomock" 28 | "github.com/notaryproject/notation-plugin-framework-go/plugin" 29 | "github.com/stretchr/testify/assert" 30 | ) 31 | 32 | const testProfile = "NotationProfile" 33 | const testPayloadType = "application/vnd.oci.descriptor.v1+json" 34 | const testSigEnvType = "application/jose+json" 35 | 36 | var testPayload = []byte("Sign ME") 37 | var testSig = []byte("dummySignature") 38 | var testSigMetadata = map[string]string{"metadatakey": "metadatavalue"} 39 | 40 | func TestGenerateEnvelope(t *testing.T) { 41 | mockCtrl := gomock.NewController(t) 42 | defer mockCtrl.Finish() 43 | mockSignerClient := client.NewMockInterface(mockCtrl) 44 | 45 | jobId := "1" 46 | mockSignerClient.EXPECT().SignPayload(gomock.Any(), gomock.Any()).DoAndReturn( 47 | func(ctx context.Context, input *signer.SignPayloadInput, optFns ...func(*signer.Options)) (*signer.SignPayloadOutput, error) { 48 | assert.Equal(t, testProfile, *input.ProfileName, "ProfileName mismatch") 49 | assert.Equal(t, testPayloadType, *input.PayloadFormat, "PayloadFormat name mismatch") 50 | assert.Equal(t, testPayload, input.Payload, "Payload mismatch") 51 | output := &signer.SignPayloadOutput{ 52 | Signature: testSig, 53 | JobId: &jobId, 54 | Metadata: testSigMetadata, 55 | } 56 | return output, nil 57 | }) 58 | 59 | response, _ := New(mockSignerClient).GenerateEnvelope(context.TODO(), mockGenerateEnvReq()) 60 | assert.Equal(t, testSigEnvType, response.SignatureEnvelopeType, "SignatureEnvelopeType mismatch") 61 | assert.Equal(t, testSig, response.SignatureEnvelope, "Signature mismatch") 62 | assert.Equal(t, map[string]string{"metadatakey": "metadatavalue"}, response.Annotations, "metadata mismatch") 63 | } 64 | 65 | func TestGenerateEnvelope_MalformedRequest(t *testing.T) { 66 | badEnvTypeReq := mockGenerateEnvReq() 67 | badEnvTypeReq.SignatureEnvelopeType = "badType" 68 | 69 | badContractVersionReq := mockGenerateEnvReq() 70 | badContractVersionReq.ContractVersion = "2.0" 71 | 72 | badProfileArnReq := mockGenerateEnvReq() 73 | badProfileArnReq.KeyID = "NotationProfile" 74 | 75 | invalidSigningProfileArnReq := mockGenerateEnvReq() 76 | invalidSigningProfileArnReq.KeyID = "arn:aws:signer:us-west-2:123:/signing-profiles/name/version/invalid" 77 | 78 | nonNilExpiryReq := mockGenerateEnvReq() 79 | nonNilExpiryReq.ExpiryDurationInSeconds = 4 80 | 81 | tests := map[string]struct { 82 | req *plugin.GenerateEnvelopeRequest 83 | errorMsg string 84 | }{ 85 | "badEnvelopeTypeReq": { 86 | req: badEnvTypeReq, 87 | errorMsg: "envelope type \"badType\" is not supported", 88 | }, 89 | "badContractVersionReq": { 90 | req: badContractVersionReq, 91 | errorMsg: "\"2.0\" is not a supported notary plugin contract version", 92 | }, 93 | "badProfileArnReq": { 94 | req: badProfileArnReq, 95 | errorMsg: fmt.Sprintf(errorMsgMalformedSigningProfileFmt, "NotationProfile"), 96 | }, 97 | "invalidSigningProfileArnReq": { 98 | req: invalidSigningProfileArnReq, 99 | errorMsg: fmt.Sprintf(errorMsgMalformedSigningProfileFmt, "arn:aws:signer:us-west-2:123:/signing-profiles/name/version/invalid"), 100 | }, 101 | "nonNilExpiryReq": { 102 | req: nonNilExpiryReq, 103 | errorMsg: "AWSSigner plugin doesn't support -e (--expiry) argument. Please use signing profile to set signature expiry.", 104 | }, 105 | } 106 | for name, test := range tests { 107 | t.Run(name, func(t *testing.T) { 108 | awsSigner, _ := client.NewAWSSigner(context.TODO(), map[string]string{}) 109 | _, err := New(awsSigner).GenerateEnvelope(context.TODO(), test.req) 110 | plgErr := toPluginError(err, t) 111 | assert.Equal(t, test.errorMsg, plgErr.Message, "error message mismatch") 112 | }) 113 | } 114 | } 115 | 116 | func TestGenerateEnvelope_AWSSignerError(t *testing.T) { 117 | awsErrMsg := "aws error message" 118 | tests := map[string]struct { 119 | err error 120 | errorMsg string 121 | errCode plugin.ErrorCode 122 | }{ 123 | "AccessDeniedException": { 124 | err: &smithy.GenericAPIError{ 125 | Code: "AccessDeniedException", 126 | Message: awsErrMsg, 127 | }, 128 | errorMsg: "Failed to call AWSSigner. Error: aws error message.", 129 | errCode: plugin.ErrorCodeAccessDenied, 130 | }, 131 | "ThrottlingException": { 132 | err: &smithy.GenericAPIError{ 133 | Code: "ThrottlingException", 134 | Message: awsErrMsg, 135 | }, 136 | errorMsg: "Failed to call AWSSigner. Error: aws error message.", 137 | errCode: plugin.ErrorCodeThrottled, 138 | }, 139 | "ResourceNotFoundException": { 140 | err: &smithy.GenericAPIError{ 141 | Code: "ResourceNotFoundException", 142 | Message: awsErrMsg, 143 | }, 144 | errorMsg: "Failed to call AWSSigner. Error: aws error message.", 145 | errCode: plugin.ErrorCodeValidation, 146 | }, 147 | "GenericException": { 148 | err: &smithy.GenericAPIError{ 149 | Code: "GenericException", 150 | Message: awsErrMsg, 151 | }, 152 | errorMsg: "Failed to call AWSSigner. Error: aws error message.", 153 | errCode: plugin.ErrorCodeGeneric, 154 | }, 155 | "HttpError": { 156 | err: &http.ResponseError{ 157 | ResponseError: &smithyhttp.ResponseError{ 158 | Response: &smithyhttp.Response{ 159 | Response: &nethttp.Response{ 160 | StatusCode: 200, 161 | }, 162 | }, 163 | }, 164 | RequestID: "123456789", 165 | }, 166 | errorMsg: "https response error StatusCode: 200, RequestID: 123456789, ", 167 | errCode: plugin.ErrorCodeGeneric, 168 | }, 169 | } 170 | 171 | for name, test := range tests { 172 | t.Run(name, func(t *testing.T) { 173 | mockSignerClient, mockCtrl := getMockErrorClient(test.err, t) 174 | defer mockCtrl.Finish() 175 | 176 | _, err := New(mockSignerClient).GenerateEnvelope(context.TODO(), mockGenerateEnvReq()) 177 | plgErr := toPluginError(err, t) 178 | assert.Equal(t, test.errorMsg, plgErr.Message, "Error message mismatch") 179 | assert.Equal(t, test.errCode, plgErr.ErrCode, "Wrong error code.") 180 | }) 181 | } 182 | } 183 | 184 | func toPluginError(err error, t *testing.T) *plugin.Error { 185 | if err == nil { 186 | t.Error("expected error but not found") 187 | } 188 | plgErr, ok := err.(*plugin.Error) 189 | if !ok { 190 | t.Errorf("Expected error of type 'plugin.Error' but not found") 191 | } 192 | return plgErr 193 | } 194 | 195 | func mockGenerateEnvReq() *plugin.GenerateEnvelopeRequest { 196 | return &plugin.GenerateEnvelopeRequest{ 197 | ContractVersion: plugin.ContractVersion, 198 | SignatureEnvelopeType: testSigEnvType, 199 | Payload: testPayload, 200 | PayloadType: testPayloadType, 201 | KeyID: "arn:aws:signer:us-west-2:780792624090:/signing-profiles/NotationProfile", 202 | } 203 | } 204 | 205 | func getMockErrorClient(err error, t *testing.T) (client.Interface, *gomock.Controller) { 206 | mockCtrl := gomock.NewController(t) 207 | mockSignerClient := client.NewMockInterface(mockCtrl) 208 | 209 | mockSignerClient.EXPECT().SignPayload(gomock.Any(), gomock.Any()).Return(nil, err) 210 | return mockSignerClient, mockCtrl 211 | } 212 | -------------------------------------------------------------------------------- /internal/slices/slices.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package slices provides utility methods for slice. 15 | package slices 16 | 17 | // AppendIfNotPresent appends t to s if t is not already present. 18 | func AppendIfNotPresent[T comparable](s []T, t T) []T { 19 | if !Contains(s, t) { 20 | return append(s, t) 21 | } 22 | return s 23 | } 24 | 25 | // Contains reports whether v is present in s. 26 | func Contains[E comparable](s []E, v E) bool { 27 | for _, vs := range s { 28 | if v == vs { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | -------------------------------------------------------------------------------- /internal/slices/slices_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package slices 15 | 16 | import ( 17 | "reflect" 18 | "testing" 19 | ) 20 | 21 | func TestContains(t *testing.T) { 22 | tests := []struct { 23 | s []string 24 | v string 25 | want bool 26 | }{ 27 | {nil, "", false}, 28 | {[]string{}, "", false}, 29 | {[]string{"1", "2", "3"}, "2", true}, 30 | {[]string{"1", "2", "2", "3"}, "2", true}, 31 | {[]string{"1", "2", "3", "2"}, "2", true}, 32 | } 33 | for _, tt := range tests { 34 | if got := Contains(tt.s, tt.v); got != tt.want { 35 | t.Errorf("index() = %v, want %v", got, tt.want) 36 | } 37 | } 38 | } 39 | 40 | func TestAppendIfNotPresent(t *testing.T) { 41 | tests := []struct { 42 | s []string 43 | v string 44 | want []string 45 | }{ 46 | {[]string{"1", "2"}, "3", []string{"1", "2", "3"}}, 47 | {[]string{"1", "2", "3"}, "3", []string{"1", "2", "3"}}, 48 | } 49 | for _, tt := range tests { 50 | if got := AppendIfNotPresent(tt.s, tt.v); !reflect.DeepEqual(got, tt.want) { 51 | t.Errorf("AppendIfNotPresent() = %v, want %v", got, tt.want) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/verifier/verifier.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package verifier verified provides functionality to verify signatures generated using AWS Signer 15 | // in accordance with the NotaryProject Plugin contract. 16 | package verifier 17 | 18 | import ( 19 | "context" 20 | "crypto/sha512" 21 | "crypto/x509" 22 | "fmt" 23 | "strings" 24 | 25 | "github.com/aws/aws-sdk-go-v2/aws" 26 | "github.com/aws/aws-sdk-go-v2/aws/arn" 27 | "github.com/aws/aws-sdk-go-v2/service/signer" 28 | "github.com/aws/aws-signer-notation-plugin/internal/client" 29 | "github.com/aws/aws-signer-notation-plugin/internal/logger" 30 | "github.com/aws/aws-signer-notation-plugin/internal/slices" 31 | 32 | "github.com/notaryproject/notation-plugin-framework-go/plugin" 33 | ) 34 | 35 | const ( 36 | wildcardIdentity = "*" 37 | errMsgWildcardIdentity = "The AWSSigner plugin does not support wildcard identity in the trust policy." 38 | 39 | attrSigningProfileVersion = "com.amazonaws.signer.signingProfileVersion" 40 | attrSigningJob = "com.amazonaws.signer.signingJob" 41 | signingSchemeAuthority = "notary.x509.signingAuthority" 42 | 43 | errMsgCertificateParse = "unable to parse certificates in certificate chain." 44 | errMsgAttributeParse = "unable to parse attribute %q." 45 | 46 | reasonTrustedIdentityFailure = "Signature publisher doesn't match any trusted identities." 47 | reasonTrustedIdentitySuccessFmt = "Signature publisher matched %q trusted identity." 48 | reasonNotRevoked = "Signature is not revoked." 49 | reasonRevokedResourceFmt = "Resource(s) %s have been revoked." 50 | reasonRevokedCertificate = "Certificate(s) have been revoked." 51 | 52 | platformNotation = "Notation-OCI-SHA384-ECDSA" 53 | ) 54 | 55 | var verificationCapabilities = []plugin.Capability{ 56 | plugin.CapabilityTrustedIdentityVerifier, 57 | plugin.CapabilityRevocationCheckVerifier} 58 | 59 | // Verifier verifies signature generated using AWS Signer. 60 | type Verifier struct { 61 | awssigner client.Interface 62 | } 63 | 64 | // New returns Verifier given an AWS Signer client. 65 | func New(s client.Interface) *Verifier { 66 | return &Verifier{awssigner: s} 67 | } 68 | 69 | // Verify provides extended verification (including trusted-identity and revocation check) 70 | // for signatures generated using AWS Signer. 71 | func (v *Verifier) Verify(ctx context.Context, request *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) { 72 | log := logger.GetLogger(ctx) 73 | log.Debug("validating VerifySignatureRequest") 74 | if err := validate(request); err != nil { 75 | log.Debugf("validate VerifySignatureRequest error :%s", err) 76 | return nil, err 77 | } 78 | 79 | response := plugin.VerifySignatureResponse{ 80 | VerificationResults: make(map[plugin.Capability]*plugin.VerificationResult), 81 | } 82 | if slices.Contains(request.TrustPolicy.SignatureVerification, plugin.CapabilityTrustedIdentityVerifier) { 83 | log.Debug("validating trusted identity") 84 | if err := validateTrustedIdentity(request, &response); err != nil { 85 | log.Debugf("validate trusted identity error :%v", err) 86 | return nil, err 87 | } 88 | log.Debugf("verification response: %+v\n", response) 89 | } 90 | if slices.Contains(request.TrustPolicy.SignatureVerification, plugin.CapabilityRevocationCheckVerifier) { 91 | log.Debug("validating revocation status") 92 | if err := v.validateRevocation(ctx, request, &response); err != nil { 93 | log.Debugf("validate revocation status error :%v", err) 94 | return nil, err 95 | } 96 | log.Debugf("verification response: %+v\n", response) 97 | } 98 | 99 | // marking both signing-job ARN and signing-profile-version arn as processed attributes here because the plugin should 100 | // return both of them as processed even if the revocation call was skipped 101 | response.ProcessedAttributes = slices.AppendIfNotPresent(response.ProcessedAttributes, attrSigningProfileVersion) 102 | response.ProcessedAttributes = slices.AppendIfNotPresent(response.ProcessedAttributes, attrSigningJob) 103 | return &response, nil 104 | } 105 | 106 | func validate(req *plugin.VerifySignatureRequest) error { 107 | if req.ContractVersion != plugin.ContractVersion { 108 | return plugin.NewUnsupportedContractVersionError(req.ContractVersion) 109 | } 110 | 111 | if slices.Contains(req.TrustPolicy.TrustedIdentities, wildcardIdentity) { 112 | return plugin.NewValidationError(errMsgWildcardIdentity) 113 | } 114 | 115 | for _, value := range req.TrustPolicy.SignatureVerification { 116 | if !pluginCapabilitySupported(value) { 117 | return plugin.NewValidationErrorf("'%s' is not a supported plugin capability", value) 118 | } 119 | } 120 | 121 | critcAttr := req.Signature.CriticalAttributes 122 | if critcAttr.AuthenticSigningTime.IsZero() { 123 | return plugin.NewValidationError("missing authenticSigningTime") 124 | } 125 | 126 | if !strings.EqualFold(critcAttr.SigningScheme, signingSchemeAuthority) { 127 | return plugin.NewUnsupportedError(fmt.Sprintf("'%s' signing scheme", req.Signature.CriticalAttributes.SigningScheme)) 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func validateTrustedIdentity(request *plugin.VerifySignatureRequest, response *plugin.VerifySignatureResponse) error { 134 | signatureIdentity, err := getValueAsString(request.Signature.CriticalAttributes.ExtendedAttributes, attrSigningProfileVersion) 135 | if err != nil { 136 | return err 137 | } 138 | 139 | var trustedArns []string 140 | for _, identity := range request.TrustPolicy.TrustedIdentities { 141 | if _, ok := isSigningProfileArn(identity); ok { 142 | trustedArns = append(trustedArns, identity) 143 | } 144 | } 145 | 146 | result := &plugin.VerificationResult{ 147 | Success: false, 148 | Reason: reasonTrustedIdentityFailure, 149 | } 150 | 151 | var profileMatch bool 152 | for _, identity := range request.TrustPolicy.TrustedIdentities { 153 | if arn, ok := isSigningProfileArn(identity); ok { 154 | s := strings.Split(arn.Resource, "/") 155 | if len(s) == 3 { // if profile arn 156 | lastIndex := strings.LastIndex(signatureIdentity, "/") 157 | if lastIndex != -1 && strings.EqualFold(signatureIdentity[:lastIndex], identity) { 158 | profileMatch = true 159 | } 160 | } else if len(s) == 4 { // if profile version arn 161 | if strings.EqualFold(signatureIdentity, identity) { 162 | profileMatch = true 163 | } 164 | } 165 | if profileMatch { 166 | result.Success = true 167 | result.Reason = fmt.Sprintf(reasonTrustedIdentitySuccessFmt, identity) 168 | break 169 | } 170 | } 171 | } 172 | 173 | response.VerificationResults[plugin.CapabilityTrustedIdentityVerifier] = result 174 | return nil 175 | } 176 | 177 | func isSigningProfileArn(s string) (arn.ARN, bool) { 178 | if a, err := arn.Parse(s); err == nil { 179 | return a, a.Service == "signer" && strings.HasPrefix(a.Resource, "/signing-profiles/") 180 | } 181 | 182 | return arn.ARN{}, false 183 | } 184 | 185 | func getValueAsString(m map[string]interface{}, k string) (string, error) { 186 | if val, ok := m[k]; ok { 187 | if s, ok := val.(string); ok { 188 | return s, nil 189 | } 190 | } 191 | 192 | return "", plugin.NewValidationErrorf(errMsgAttributeParse, k) 193 | } 194 | 195 | func (v *Verifier) validateRevocation(ctx context.Context, request *plugin.VerifySignatureRequest, response *plugin.VerifySignatureResponse) error { 196 | profileVersionArn, err := getValueAsString(request.Signature.CriticalAttributes.ExtendedAttributes, attrSigningProfileVersion) 197 | if err != nil { 198 | return err 199 | } 200 | 201 | jobArn, err := getValueAsString(request.Signature.CriticalAttributes.ExtendedAttributes, attrSigningJob) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | certHashes, err := hashCertificates(request.Signature.CertificateChain) 207 | if err != nil { 208 | return plugin.NewValidationError(errMsgCertificateParse) 209 | } 210 | 211 | input := &signer.GetRevocationStatusInput{ 212 | CertificateHashes: certHashes, 213 | JobArn: aws.String(jobArn), 214 | PlatformId: aws.String(platformNotation), 215 | ProfileVersionArn: aws.String(profileVersionArn), 216 | SignatureTimestamp: request.Signature.CriticalAttributes.AuthenticSigningTime, 217 | } 218 | 219 | result := &plugin.VerificationResult{ 220 | Success: true, 221 | Reason: reasonNotRevoked, 222 | } 223 | output, err := v.awssigner.GetRevocationStatus(ctx, input) 224 | if err != nil { 225 | result.Success = false 226 | result.Reason = fmt.Sprintf("GetRevocationStatus call failed with error: %+v", err) 227 | } else { 228 | if len(output.RevokedEntities) > 0 { 229 | result.Success = false 230 | result.Reason = getRevocationResultReason(output.RevokedEntities) 231 | } 232 | } 233 | 234 | response.VerificationResults[plugin.CapabilityRevocationCheckVerifier] = result 235 | return nil 236 | } 237 | 238 | func getRevocationResultReason(revokedEntities []string) string { 239 | var resources string 240 | var certRevoked bool 241 | for _, resource := range revokedEntities { 242 | if strings.HasPrefix(resource, "arn") { 243 | if resources == "" { 244 | resources += resource 245 | } else { 246 | resources = resources + ", " + resource 247 | } 248 | } else { 249 | certRevoked = true 250 | } 251 | } 252 | 253 | var reason string 254 | if resources != "" { 255 | reason = fmt.Sprintf(reasonRevokedResourceFmt, resources) 256 | } 257 | if certRevoked { 258 | reason += reasonRevokedCertificate 259 | } 260 | 261 | return reason 262 | } 263 | 264 | func hashCertificates(certStrings [][]byte) ([]string, error) { 265 | var certHashes []string 266 | for _, certString := range certStrings { 267 | // notation always passes cert in DER format 268 | cert, err := x509.ParseCertificate(certString) 269 | if err != nil { 270 | return nil, err 271 | } 272 | 273 | certHashes = append(certHashes, hashCertificate(*cert)) 274 | } 275 | 276 | for i := range certHashes { 277 | if i == len(certHashes)-1 { 278 | certHashes[i] = certHashes[i] + certHashes[i] 279 | } else { 280 | certHashes[i] = certHashes[i] + certHashes[i+1] 281 | } 282 | } 283 | 284 | return certHashes, nil 285 | } 286 | 287 | func hashCertificate(cert x509.Certificate) string { 288 | h := sha512.New384() 289 | h.Write(cert.RawTBSCertificate) 290 | return fmt.Sprintf("%x", h.Sum(nil)) 291 | } 292 | 293 | func pluginCapabilitySupported(capability plugin.Capability) bool { 294 | return slices.Contains(verificationCapabilities, capability) 295 | } 296 | -------------------------------------------------------------------------------- /internal/verifier/verifier_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package verifier 15 | 16 | import ( 17 | "context" 18 | "encoding/pem" 19 | "fmt" 20 | "testing" 21 | "time" 22 | 23 | "github.com/aws/aws-sdk-go-v2/aws" 24 | "github.com/aws/aws-signer-notation-plugin/internal/client" 25 | "github.com/aws/smithy-go" 26 | "github.com/notaryproject/notation-plugin-framework-go/plugin" 27 | "github.com/stretchr/testify/assert" 28 | 29 | "github.com/aws/aws-sdk-go-v2/service/signer" 30 | "github.com/golang/mock/gomock" 31 | ) 32 | 33 | const ( 34 | testProfileArn = "arn:aws:signer:us-west-2:000000000000:/signing-profiles/NotaryPluginIntegProfile" 35 | testProfileVersionArn = testProfileArn + "/OF8IVUsPJq" 36 | testJobArn = "arn:aws:signer:us-west-2:000000000000:/signing-jobs/97af3947-e7b2-4533-8d9d-6741156f0b79" 37 | testCertificate1 = `-----BEGIN CERTIFICATE----- 38 | MIIDQDCCAiigAwIBAgIRAMH0R+Owv6zXRzRJgjkWUPEwDQYJKoZIhvcNAQELBQAw 39 | ETEPMA0GA1UECgwGY2hpZW5iMB4XDTIyMDcxNTE3MjQ0MVoXDTIzMDgxNTE4MjQ0 40 | MFowEjEQMA4GA1UEAwwHZm9vLmJhcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 41 | AQoCggEBANn0mo5gw6VYKfLGHre6zy6eo6f1Fe2p2o5nbClmkA43OgWF0ngnwJPd 42 | Hhfy17pqDOrfs3Uj8gGwhlZYbWVYORWHGwbHRV9FsBP3wq8HrQ2I+7UAZNsRBxWQ 43 | Lbo0ha0NzYLIG1DYuPrNCBSzdlkjNhNZJR8QRn0+5LW8AfcOD3x6UBhDgk8kE/Y/ 44 | 9outGzynHVDXObpylh6xie+PXJ6y8aPM0PZszwWv+mJznXchyvrVDUxpETI/EnL9 45 | QMq2STEgAS0f8PCYQkKxz1s1ODb2AWwuIdqJmDhmwkYs4kqV/kyNN42H6gfgSQXf 46 | IJMLX2fn/ZOz431jV8fUDSKUFSdJw2sCAwEAAaOBkTCBjjASBgNVHREECzAJggdm 47 | b28uYmFyMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAU3gzqhkSDrYSfGn5E8e/3qUAw 48 | xowwHQYDVR0OBBYEFETbSw2Lt2WIQlolvzg1lKadc0oQMA4GA1UdDwEB/wQEAwIF 49 | oDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQAD 50 | ggEBALc3rxSZLVc4ammm7XKQDAh7B+MX4LOj8TleVr/aYZ1iN9y2VVsKmUtLCJBa 51 | gU2HWaoAQN2qO0FvbANuBpgf2dF8fUFygKURo9fnFSIng2rgd38cIrJP0mYYPg4x 52 | EizD3ZznlFE7zu4MVBcZTOTAgqyzsjg/K1YfdBTCmEoNv09P7u4r1KiATBsaiKaH 53 | h770TLUfa+PzpbIinp2cF/XYVchepCiCJDAdTR1tWKHaqeuW/WQHKso7Z6wyPO24 54 | d3m5GyGuIRMddbp6zclSRP/I4TCS/0cOru9ATc94PaKWjDOTClYH8ykRZom8OICq 55 | KCzg3o7lofVNdVFxDM8rrMJ06cY= 56 | -----END CERTIFICATE-----` 57 | testCertificate1Hash = "13a01b7e1de3aee0367615c59f6d001238913e594626d0e3c8784489b15a18fada1c31f39d3ba9318cb673ffd8cd679b" 58 | testCertificate2 = `-----BEGIN CERTIFICATE----- 59 | MIIC7zCCAdegAwIBAgIRAPxhWP65yw1qFSMD39FxuUwwDQYJKoZIhvcNAQELBQAw 60 | ETEPMA0GA1UECgwGY2hpZW5iMB4XDTE5MTAwNzE4MDIxMVoXDTI5MTAwNzE5MDIx 61 | MVowETEPMA0GA1UECgwGY2hpZW5iMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB 62 | CgKCAQEA4SSFBInasnQCgPLDZzz0NNlTlRm4yn2lyUP7gEzBQZc0Hp+PKE3dnMGH 63 | bQ6w0FmGD5sMKMTIfUCRJyjiJPi0RvCEOmU+nY2UYZf+ttrVx33pWrHpkxXORxA4 64 | rp7SzxP5GFl78Mo0CEFxOKHPqLC/Nm4SmQKhhMUJkiqc3X/9WFigBIfkFXLFZQ64 65 | yoCq+ekvKW9GGh2Mq9VwSnB+6wem/3mPJ8x4sX1UtGu/DL5gc7gyzVCbfn8SZpb6 66 | L7y++9zGmRwmcKMv8IaLj07fr9Ho34zm9CbwMHwUZeHC5uXcGR54t9sTNq5rgu1k 67 | Q9LskOmPcEEkTkyKtrAs5WKHrSYdWQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/ 68 | MB0GA1UdDgQWBBTeDOqGRIOthJ8afkTx7/epQDDGjDAOBgNVHQ8BAf8EBAMCAYYw 69 | DQYJKoZIhvcNAQELBQADggEBAJSVnGSdpX6nSYcsCMHu99dN/xVn+Qtvj0ovdKQo 70 | JC5cQNjFQ7wXCSgYa2DtSMQ0McysZ+TkNWDGwi2c+HCoHAL/XNWDU261Hj/VwVI4 71 | 2p46Q4UzpWmhx5dkDV2xRhK8QMPwW2NRQqkd/75FUfRpq5xdL4IzeaNcYKXMBJyX 72 | zSZee7oqEixEVzis7Ex7mvXBiRdjZBp8cFuRJVKPBgK7SmFkJwyLtd2OLtNehUsh 73 | Af8fCVvIhr9YxXK+RqiRUhvJDrS9DlKA6dT4KvR41B/a8NLf6PJGyHdSFuvKZr0z 74 | C+gMfNFGs1L2QLg1+xnoLHIey4tRXYHjpD2b/KALNr4/v+c= 75 | -----END CERTIFICATE-----` 76 | testCertificate2Hash = "ff41924f0940448d7e46b8c327e129813b1442fb17c9b2a86d49edcb00b707c9662f561c8a3e11a592b25061d488f2a3" 77 | ) 78 | 79 | var testTISuccessReason = fmt.Sprintf(reasonTrustedIdentitySuccessFmt, testProfileArn) 80 | 81 | func TestVerify(t *testing.T) { 82 | request := mockVerifySigRequest() 83 | expectedGRSInput := getRevocationStatusInput(request) 84 | grsOutput := &signer.GetRevocationStatusOutput{ 85 | RevokedEntities: []string{}, 86 | } 87 | mockSignerClient, mockCtrl := getMockClient(expectedGRSInput, grsOutput, nil, t) 88 | defer mockCtrl.Finish() 89 | 90 | actualResponse, err := New(mockSignerClient).Verify(context.TODO(), request) 91 | expectedResponse := getVerifySigResponse(true, testTISuccessReason, true, reasonNotRevoked) 92 | validateResponse(t, expectedResponse, *actualResponse, err) 93 | } 94 | 95 | func TestVerify_ValidTrustedIdentity(t *testing.T) { 96 | tests := map[string]struct { 97 | tis []string 98 | expectedTi string 99 | }{ 100 | "signingProfileArn": { 101 | tis: []string{testProfileArn}, 102 | expectedTi: testProfileArn, 103 | }, 104 | "signingProfileVersionArn": { 105 | tis: []string{testProfileVersionArn}, 106 | expectedTi: testProfileVersionArn, 107 | }, 108 | "signingProfileAndVersionArn": { 109 | tis: []string{testProfileArn, testProfileVersionArn}, 110 | expectedTi: testProfileArn, 111 | }, 112 | "signingProfileVersionAndProfileArn": { 113 | tis: []string{testProfileVersionArn, testProfileArn}, 114 | expectedTi: testProfileVersionArn, 115 | }, 116 | "signingProfileArnWithGarbage": { 117 | tis: []string{"shop", testProfileArn, "zop"}, 118 | expectedTi: testProfileArn, 119 | }, 120 | } 121 | 122 | for name, test := range tests { 123 | t.Run(name, func(t *testing.T) { 124 | mockSignerClient, mockCtrl := getMockClient(nil, &signer.GetRevocationStatusOutput{}, nil, t) 125 | defer mockCtrl.Finish() 126 | 127 | expectedResponse := getVerifySigResponse(true, fmt.Sprintf(reasonTrustedIdentitySuccessFmt, test.expectedTi), true, reasonNotRevoked) 128 | 129 | request := mockVerifySigRequest() 130 | request.TrustPolicy.TrustedIdentities = test.tis 131 | actualResponse, err := New(mockSignerClient).Verify(context.TODO(), request) 132 | 133 | validateResponse(t, expectedResponse, *actualResponse, err) 134 | }) 135 | } 136 | } 137 | 138 | func TestVerify_InvalidTrustedIdentity(t *testing.T) { 139 | tests := map[string][]string{ 140 | "empty": {}, 141 | "nonArn": {"Cheers!🍺"}, 142 | "nonAWSSignerArn": {"arn:aws:dynamodb:us-east-2:123456789012:table/myDynamoDBTable"}, 143 | "SigningJobArn": {testJobArn}, 144 | "invalidSignerArn1": {"arn:aws:signer:us-west-2:000000000000:/beer/NotaryPluginIntegProfile"}, 145 | "invalidSigningProfileArn": {"arn:us-west-2:000000000000:/signing-profile/NotaryPluginIntegProfile/1234/asda"}, 146 | "invalidSigningProfileArn2": {testProfileVersionArn + "/asda"}, 147 | } 148 | 149 | for name, tis := range tests { 150 | t.Run(name, func(t *testing.T) { 151 | expectedResponse := getVerifySigResponse(false, reasonTrustedIdentityFailure, true, reasonNotRevoked) 152 | delete(expectedResponse.VerificationResults, plugin.CapabilityRevocationCheckVerifier) 153 | 154 | request := mockVerifySigRequest() 155 | request.TrustPolicy.TrustedIdentities = tis 156 | request.TrustPolicy.SignatureVerification = []plugin.Capability{plugin.CapabilityTrustedIdentityVerifier} 157 | c, _ := client.NewAWSSigner(context.Background(), map[string]string{}) 158 | actualResponse, err := New(c).Verify(context.TODO(), request) 159 | 160 | validateResponse(t, expectedResponse, *actualResponse, err) 161 | }) 162 | } 163 | } 164 | 165 | func TestVerify_RevokedResources(t *testing.T) { 166 | tests := map[string]struct { 167 | revokedResources []string 168 | errorMsg string 169 | }{ 170 | "revokedSigningJob": { 171 | revokedResources: []string{testJobArn}, 172 | errorMsg: fmt.Sprintf(reasonRevokedResourceFmt, testJobArn), 173 | }, 174 | "revokedSigningProfile": { 175 | revokedResources: []string{testProfileVersionArn}, 176 | errorMsg: fmt.Sprintf(reasonRevokedResourceFmt, testProfileVersionArn), 177 | }, 178 | "revokedCertificate": { 179 | revokedResources: []string{testCertificate1Hash}, 180 | errorMsg: reasonRevokedCertificate, 181 | }, 182 | "revokedCertificates": { 183 | revokedResources: []string{testCertificate1Hash, testCertificate2Hash}, 184 | errorMsg: reasonRevokedCertificate, 185 | }, 186 | "revokedSigningJobAndProfile": { 187 | revokedResources: []string{testJobArn, testProfileArn}, 188 | errorMsg: fmt.Sprintf(reasonRevokedResourceFmt, testJobArn+", "+testProfileArn), 189 | }, 190 | "revokedSigningJobAndCert": { 191 | revokedResources: []string{testJobArn, testCertificate1}, 192 | errorMsg: fmt.Sprintf(reasonRevokedResourceFmt, testJobArn) + reasonRevokedCertificate, 193 | }, 194 | } 195 | 196 | for name, test := range tests { 197 | t.Run(name, func(t *testing.T) { 198 | revokedGRSOutput := signer.GetRevocationStatusOutput{ 199 | RevokedEntities: test.revokedResources, 200 | } 201 | mockSignerClient, mockCtrl := getMockClient(nil, &revokedGRSOutput, nil, t) 202 | defer mockCtrl.Finish() 203 | 204 | expectedResponse := getVerifySigResponse(true, testTISuccessReason, false, test.errorMsg) 205 | actualResponse, err := New(mockSignerClient).Verify(context.TODO(), mockVerifySigRequest()) 206 | 207 | validateResponse(t, expectedResponse, *actualResponse, err) 208 | }) 209 | } 210 | } 211 | 212 | func TestVerify_RevocationCheckError(t *testing.T) { 213 | apiError := smithy.GenericAPIError{ 214 | Code: "ERROR", 215 | Message: "AWSSigner unreachable. 5xx", 216 | } 217 | mockSignerClient, mockCtrl := getMockClient(nil, nil, &apiError, t) 218 | defer mockCtrl.Finish() 219 | 220 | revReason := "GetRevocationStatus call failed with error: api error ERROR: " + apiError.ErrorMessage() 221 | expectedResponse := getVerifySigResponse(true, testTISuccessReason, false, revReason) 222 | actualResponse, err := New(mockSignerClient).Verify(context.TODO(), mockVerifySigRequest()) 223 | 224 | validateResponse(t, expectedResponse, *actualResponse, err) 225 | } 226 | 227 | func TestVerify_MalformedRequest(t *testing.T) { 228 | badContractVersionReq := mockVerifySigRequest() 229 | badContractVersionReq.ContractVersion = "2.0" 230 | 231 | wildcardTrustedIdentityReq := mockVerifySigRequest() 232 | wildcardTrustedIdentityReq.TrustPolicy.TrustedIdentities = []string{"*"} 233 | 234 | invalidArnTrustedIdentityReq := mockVerifySigRequest() 235 | invalidArnTrustedIdentityReq.TrustPolicy.TrustedIdentities = []string{"*"} 236 | 237 | unsupportedCapability := mockVerifySigRequest() 238 | unsupportedCapability.TrustPolicy.SignatureVerification = append(unsupportedCapability.TrustPolicy.SignatureVerification, "unsupported") 239 | 240 | invalidCertReq := mockVerifySigRequest() 241 | invalidCertReq.Signature.CertificateChain = [][]byte{[]byte("BadCertificate")} 242 | 243 | zeroAuthSignTImeReq := mockVerifySigRequest() 244 | zeroAuthSignTImeReq.Signature.CriticalAttributes.AuthenticSigningTime = &time.Time{} 245 | 246 | invalidSignSchemeReq := mockVerifySigRequest() 247 | invalidSignSchemeReq.Signature.CriticalAttributes.SigningScheme = "badSignScheme" 248 | 249 | invalidCritAttr := mockVerifySigRequest() 250 | invalidCritAttr.Signature.CriticalAttributes.ExtendedAttributes[attrSigningProfileVersion] = nil 251 | 252 | invalidCritAttrJob := mockVerifySigRequest() 253 | //invalidCritAttrJob.TrustPolicy.SignatureVerification = []plugin.Capability{plugin.CapabilityRevocationCheckVerifier} 254 | //invalidCritAttr.Signature.CriticalAttributes.ExtendedAttributes[attrSigningJob] = "asda" 255 | delete(invalidCritAttrJob.Signature.CriticalAttributes.ExtendedAttributes, attrSigningJob) 256 | invalidCritAttrJob.Signature.CriticalAttributes.ExtendedAttributes = nil 257 | 258 | invalidCritAttrProfile := mockVerifySigRequest() 259 | invalidCritAttrProfile.TrustPolicy.SignatureVerification = []plugin.Capability{plugin.CapabilityRevocationCheckVerifier} 260 | delete(invalidCritAttrProfile.Signature.CriticalAttributes.ExtendedAttributes, attrSigningProfileVersion) 261 | 262 | tests := map[string]struct { 263 | req *plugin.VerifySignatureRequest 264 | code plugin.ErrorCode 265 | errorMsg string 266 | }{ 267 | "badContractVersionReq": { 268 | req: badContractVersionReq, 269 | code: plugin.ErrorCodeUnsupportedContractVersion, 270 | errorMsg: "\"2.0\" is not a supported notary plugin contract version", 271 | }, 272 | "wildcardTrustedIdentityReq": { 273 | req: wildcardTrustedIdentityReq, 274 | code: plugin.ErrorCodeValidation, 275 | errorMsg: errMsgWildcardIdentity, 276 | }, 277 | "unsupportedCapability": { 278 | req: unsupportedCapability, 279 | code: plugin.ErrorCodeValidation, 280 | errorMsg: "'unsupported' is not a supported plugin capability", 281 | }, 282 | "invalidCertReq": { 283 | req: invalidCertReq, 284 | code: plugin.ErrorCodeValidation, 285 | errorMsg: errMsgCertificateParse, 286 | }, 287 | "zeroAuthSignTImeReq": { 288 | req: zeroAuthSignTImeReq, 289 | code: plugin.ErrorCodeValidation, 290 | errorMsg: "missing authenticSigningTime", 291 | }, 292 | "invalidSignSchemeReq": { 293 | req: invalidSignSchemeReq, 294 | code: plugin.ErrorCodeValidation, 295 | errorMsg: "'badSignScheme' signing scheme is not supported", 296 | }, 297 | "invalidCritAttr": { 298 | req: invalidCritAttr, 299 | code: plugin.ErrorCodeValidation, 300 | errorMsg: "unable to parse attribute \"com.amazonaws.signer.signingProfileVersion\".", 301 | }, 302 | "invalidCritAttrJob": { 303 | req: invalidCritAttrJob, 304 | code: plugin.ErrorCodeValidation, 305 | errorMsg: "unable to parse attribute \"com.amazonaws.signer.signingProfileVersion\".", 306 | }, 307 | "invalidCritAttrProfile": { 308 | req: invalidCritAttrProfile, 309 | code: plugin.ErrorCodeValidation, 310 | errorMsg: "unable to parse attribute \"com.amazonaws.signer.signingProfileVersion\".", 311 | }, 312 | } 313 | for name, test := range tests { 314 | t.Run(name, func(t *testing.T) { 315 | awsSigner, _ := client.NewAWSSigner(context.TODO(), map[string]string{}) 316 | _, err := New(awsSigner).Verify(context.TODO(), test.req) 317 | plgErr := toPluginError(err, t) 318 | assert.Equal(t, test.errorMsg, plgErr.Message, "error message mismatch") 319 | assert.Equal(t, test.code, plgErr.ErrCode, "error code mismatch") 320 | }) 321 | } 322 | } 323 | 324 | func validateResponse(t *testing.T, expected plugin.VerifySignatureResponse, actual plugin.VerifySignatureResponse, err error) { 325 | if err != nil { 326 | t.Fatalf("Unexpected error: %+v", err) 327 | } else { 328 | assert.Equal(t, expected, actual, "VerifySignatureResponse mismatch") 329 | } 330 | } 331 | 332 | func getVerifySigResponse(ti bool, tiReason string, notRevoked bool, revokedReason string) plugin.VerifySignatureResponse { 333 | return plugin.VerifySignatureResponse{ 334 | VerificationResults: map[plugin.Capability]*plugin.VerificationResult{ 335 | plugin.CapabilityTrustedIdentityVerifier: {Success: ti, Reason: tiReason}, 336 | plugin.CapabilityRevocationCheckVerifier: {Success: notRevoked, Reason: revokedReason}, 337 | }, 338 | ProcessedAttributes: []interface{}{ 339 | attrSigningProfileVersion, 340 | attrSigningJob, 341 | }, 342 | } 343 | } 344 | 345 | func mockVerifySigRequest() *plugin.VerifySignatureRequest { 346 | signingTime, _ := time.Parse(time.RFC3339, "2022-07-06T19:10:28+00:00") 347 | expiryTime, _ := time.Parse(time.RFC3339, "2022-10-06T07:01:20Z") 348 | req := plugin.VerifySignatureRequest{ 349 | ContractVersion: "1.0", 350 | Signature: plugin.Signature{ 351 | CriticalAttributes: plugin.CriticalAttributes{ 352 | ContentType: "application/vnd.cncf.notary.payload.v1+json", 353 | SigningScheme: signingSchemeAuthority, 354 | AuthenticSigningTime: &signingTime, 355 | Expiry: &expiryTime, 356 | ExtendedAttributes: map[string]interface{}{ 357 | attrSigningJob: testJobArn, 358 | attrSigningProfileVersion: testProfileVersionArn, 359 | }, 360 | }, 361 | UnprocessedAttributes: []string{ 362 | attrSigningJob, 363 | attrSigningProfileVersion, 364 | }, 365 | CertificateChain: convertCert(testCertificate1, testCertificate2), 366 | }, 367 | TrustPolicy: plugin.TrustPolicy{ 368 | TrustedIdentities: []string{ 369 | testProfileArn, 370 | "x509.subject: C=US, ST=WA, L=Seattle, O=acme-rockets.io, OU=Finance, CN=SecureBuilder", 371 | }, 372 | SignatureVerification: []plugin.Capability{ 373 | plugin.CapabilityTrustedIdentityVerifier, 374 | plugin.CapabilityRevocationCheckVerifier, 375 | }, 376 | }, 377 | } 378 | return &req 379 | } 380 | 381 | func convertCert(certs ...string) [][]byte { 382 | var o [][]byte 383 | for _, cert := range certs { 384 | block, _ := pem.Decode([]byte(cert)) 385 | o = append(o, block.Bytes) 386 | } 387 | return o 388 | } 389 | 390 | func getRevocationStatusInput(request *plugin.VerifySignatureRequest) *signer.GetRevocationStatusInput { 391 | signingJobArn, _ := request.Signature.CriticalAttributes.ExtendedAttributes[attrSigningJob].(string) 392 | profileVersionArn, _ := request.Signature.CriticalAttributes.ExtendedAttributes[attrSigningProfileVersion].(string) 393 | 394 | return &signer.GetRevocationStatusInput{ 395 | CertificateHashes: []string{ 396 | testCertificate1Hash + testCertificate2Hash, 397 | testCertificate2Hash + testCertificate2Hash, 398 | }, 399 | JobArn: &signingJobArn, 400 | PlatformId: aws.String(platformNotation), 401 | ProfileVersionArn: &profileVersionArn, 402 | SignatureTimestamp: request.Signature.CriticalAttributes.AuthenticSigningTime, 403 | } 404 | } 405 | 406 | func toPluginError(err error, t *testing.T) *plugin.Error { 407 | if err == nil { 408 | t.Fatal("expected error but not found") 409 | } 410 | plgErr, ok := err.(*plugin.Error) 411 | if !ok { 412 | t.Errorf("Expected error of type 'plugin.Error' but not found") 413 | } 414 | return plgErr 415 | } 416 | 417 | func getMockClient(expectedInput *signer.GetRevocationStatusInput, op *signer.GetRevocationStatusOutput, err error, t *testing.T) (client.Interface, *gomock.Controller) { 418 | mockCtrl := gomock.NewController(t) 419 | mockSignerClient := client.NewMockInterface(mockCtrl) 420 | mockSignerClient.EXPECT().GetRevocationStatus(gomock.Any(), gomock.Any()).DoAndReturn( 421 | func(ctx context.Context, input *signer.GetRevocationStatusInput, optFns ...func(*signer.Options)) (*signer.GetRevocationStatusOutput, error) { 422 | if expectedInput != nil { 423 | assert.Equal(t, expectedInput, input, "GetRevocationStatusInput mismatch") 424 | } 425 | return op, err 426 | }) 427 | 428 | return mockSignerClient, mockCtrl 429 | } 430 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package version provides utility methods for AWS Signer's plugin version. 15 | package version 16 | 17 | var version = "1.0.350" 18 | 19 | // GetVersion returns the plugin version in Semantic Versioning 2.0.0 format. 20 | func GetVersion() string { 21 | return version 22 | } 23 | -------------------------------------------------------------------------------- /internal/version/version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package version 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | func TestGetVersion(t *testing.T) { 23 | version = "1.1.1" 24 | assert.Equal(t, "1.1.1", GetVersion()) 25 | } 26 | -------------------------------------------------------------------------------- /plugin/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | // Package plugin implements the interface [github.com/notaryproject/notation-plugin-framework-go/plugin], 15 | // enabling its use as a library in the notation-go package and to generate executable 16 | package plugin 17 | 18 | import ( 19 | "context" 20 | 21 | "github.com/aws/aws-signer-notation-plugin/internal/client" 22 | "github.com/aws/aws-signer-notation-plugin/internal/signer" 23 | "github.com/aws/aws-signer-notation-plugin/internal/verifier" 24 | "github.com/aws/aws-signer-notation-plugin/internal/version" 25 | "github.com/notaryproject/notation-plugin-framework-go/plugin" 26 | ) 27 | 28 | const Name = "com.amazonaws.signer.notation.plugin" 29 | 30 | // AWSSignerPlugin provides functionality for signing and verification in accordance with the NotaryProject AWSSignerPlugin contract. 31 | type AWSSignerPlugin struct { 32 | awssigner client.Interface 33 | } 34 | 35 | // NewAWSSigner creates new AWSSignerPlugin 36 | func NewAWSSigner(s client.Interface) *AWSSignerPlugin { 37 | return &AWSSignerPlugin{awssigner: s} 38 | } 39 | 40 | // NewAWSSignerForCLI creates a new AWSSignerPlugin and is intended solely for generating executables. 41 | func NewAWSSignerForCLI() *AWSSignerPlugin { 42 | return &AWSSignerPlugin{} 43 | } 44 | 45 | // VerifySignature performs the extended verification of signature by optionally calling AWS Signer. 46 | func (sp *AWSSignerPlugin) VerifySignature(ctx context.Context, req *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) { 47 | if req == nil { 48 | return nil, plugin.NewValidationError("verifySignature req is nil") 49 | } 50 | if err := req.Validate(); err != nil { 51 | return nil, err 52 | } 53 | if err := sp.setSignerClientIfNotPresent(ctx, req.PluginConfig); err != nil { 54 | return nil, err 55 | } 56 | 57 | return verifier.New(sp.awssigner).Verify(ctx, req) 58 | } 59 | 60 | // GetMetadata returns the metadata information of the plugin. 61 | func (sp *AWSSignerPlugin) GetMetadata(_ context.Context, _ *plugin.GetMetadataRequest) (*plugin.GetMetadataResponse, error) { 62 | return &plugin.GetMetadataResponse{ 63 | Name: Name, 64 | Description: "AWS Signer plugin for Notation", 65 | Version: version.GetVersion(), 66 | URL: "https://docs.aws.amazon.com/signer", 67 | SupportedContractVersions: []string{plugin.ContractVersion}, 68 | Capabilities: []plugin.Capability{ 69 | plugin.CapabilityEnvelopeGenerator, 70 | plugin.CapabilityTrustedIdentityVerifier, 71 | plugin.CapabilityRevocationCheckVerifier, 72 | }, 73 | }, nil 74 | } 75 | 76 | // DescribeKey describes the key being used for signing. This method is not supported by AWS Signer's plugin. 77 | func (sp *AWSSignerPlugin) DescribeKey(_ context.Context, _ *plugin.DescribeKeyRequest) (*plugin.DescribeKeyResponse, error) { 78 | return nil, plugin.NewUnsupportedError("DescribeKey operation") 79 | } 80 | 81 | // GenerateSignature generates the raw signature. This method is not supported by AWS Signer's plugin. 82 | func (sp *AWSSignerPlugin) GenerateSignature(_ context.Context, _ *plugin.GenerateSignatureRequest) (*plugin.GenerateSignatureResponse, error) { 83 | return nil, plugin.NewUnsupportedError("GenerateSignature operation") 84 | } 85 | 86 | // GenerateEnvelope returns the signature envelope generated by calling AWS Signer. 87 | func (sp *AWSSignerPlugin) GenerateEnvelope(ctx context.Context, req *plugin.GenerateEnvelopeRequest) (*plugin.GenerateEnvelopeResponse, error) { 88 | if req == nil { 89 | return nil, plugin.NewValidationError("generateEnvelope request is nil") 90 | } 91 | if err := req.Validate(); err != nil { 92 | return nil, err 93 | } 94 | if err := sp.setSignerClientIfNotPresent(ctx, req.PluginConfig); err != nil { 95 | return nil, err 96 | } 97 | 98 | return signer.New(sp.awssigner).GenerateEnvelope(ctx, req) 99 | } 100 | 101 | func (sp *AWSSignerPlugin) setSignerClientIfNotPresent(ctx context.Context, plConfig map[string]string) error { 102 | if sp.awssigner == nil { 103 | s, err := client.NewAWSSigner(ctx, plConfig) 104 | if err != nil { 105 | return err 106 | } 107 | sp.awssigner = s 108 | } 109 | return nil 110 | } 111 | -------------------------------------------------------------------------------- /plugin/plugin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may 4 | // not use this file except in compliance with the License. A copy of the 5 | // License is located at 6 | // 7 | // http://aws.amazon.com/apache2.0 8 | // 9 | // or in the "license" file accompanying this file. This file is distributed 10 | // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 11 | // express or implied. See the License for the specific language governing 12 | // permissions and limitations under the License. 13 | 14 | package plugin 15 | 16 | import ( 17 | "context" 18 | "encoding/pem" 19 | "fmt" 20 | "testing" 21 | "time" 22 | 23 | "github.com/aws/aws-sdk-go-v2/service/signer" 24 | "github.com/aws/aws-signer-notation-plugin/internal/client" 25 | "github.com/golang/mock/gomock" 26 | "github.com/notaryproject/notation-plugin-framework-go/plugin" 27 | "github.com/stretchr/testify/assert" 28 | ) 29 | 30 | const ( 31 | testProfileArn = "arn:aws:signer:us-west-2:000000000000:/signing-profiles/NotaryPluginIntegProfile" 32 | testProfileVersionArn = testProfileArn + "/OF8IVUsPJq" 33 | testJobArn = "arn:aws:signer:us-west-2:000000000000:/signing-jobs/97af3947-e7b2-4533-8d9d-6741156f0b79" 34 | testCertificate = `-----BEGIN CERTIFICATE----- 35 | MIIDQDCCAiigAwIBAgIRAMH0R+Owv6zXRzRJgjkWUPEwDQYJKoZIhvcNAQELBQAw 36 | ETEPMA0GA1UECgwGY2hpZW5iMB4XDTIyMDcxNTE3MjQ0MVoXDTIzMDgxNTE4MjQ0 37 | MFowEjEQMA4GA1UEAwwHZm9vLmJhcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 38 | AQoCggEBANn0mo5gw6VYKfLGHre6zy6eo6f1Fe2p2o5nbClmkA43OgWF0ngnwJPd 39 | Hhfy17pqDOrfs3Uj8gGwhlZYbWVYORWHGwbHRV9FsBP3wq8HrQ2I+7UAZNsRBxWQ 40 | Lbo0ha0NzYLIG1DYuPrNCBSzdlkjNhNZJR8QRn0+5LW8AfcOD3x6UBhDgk8kE/Y/ 41 | 9outGzynHVDXObpylh6xie+PXJ6y8aPM0PZszwWv+mJznXchyvrVDUxpETI/EnL9 42 | QMq2STEgAS0f8PCYQkKxz1s1ODb2AWwuIdqJmDhmwkYs4kqV/kyNN42H6gfgSQXf 43 | IJMLX2fn/ZOz431jV8fUDSKUFSdJw2sCAwEAAaOBkTCBjjASBgNVHREECzAJggdm 44 | b28uYmFyMAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAU3gzqhkSDrYSfGn5E8e/3qUAw 45 | xowwHQYDVR0OBBYEFETbSw2Lt2WIQlolvzg1lKadc0oQMA4GA1UdDwEB/wQEAwIF 46 | oDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDQYJKoZIhvcNAQELBQAD 47 | ggEBALc3rxSZLVc4ammm7XKQDAh7B+MX4LOj8TleVr/aYZ1iN9y2VVsKmUtLCJBa 48 | gU2HWaoAQN2qO0FvbANuBpgf2dF8fUFygKURo9fnFSIng2rgd38cIrJP0mYYPg4x 49 | EizD3ZznlFE7zu4MVBcZTOTAgqyzsjg/K1YfdBTCmEoNv09P7u4r1KiATBsaiKaH 50 | h770TLUfa+PzpbIinp2cF/XYVchepCiCJDAdTR1tWKHaqeuW/WQHKso7Z6wyPO24 51 | d3m5GyGuIRMddbp6zclSRP/I4TCS/0cOru9ATc94PaKWjDOTClYH8ykRZom8OICq 52 | KCzg3o7lofVNdVFxDM8rrMJ06cY= 53 | -----END CERTIFICATE-----` 54 | ) 55 | 56 | func TestNew(t *testing.T) { 57 | c, _ := client.NewAWSSigner(context.TODO(), map[string]string{}) 58 | awsSignerPlugin := NewAWSSigner(c) 59 | assert.NotNil(t, awsSignerPlugin, "NewAWSSigner should return a non-nil instance of AWSSignerPlugin") 60 | } 61 | 62 | func TestNewForCLI(t *testing.T) { 63 | awsSignerPlugin := NewAWSSignerForCLI() 64 | assert.NotNil(t, awsSignerPlugin, "NewAWSSignerForCLI should return a non-nil instance of AWSSignerPlugin") 65 | } 66 | 67 | func TestVerifySignature(t *testing.T) { 68 | request, expectedResp := getVerifySignatureRequestResponse() 69 | 70 | mockCtrl := gomock.NewController(t) 71 | defer mockCtrl.Finish() 72 | mockSignerClient := client.NewMockInterface(mockCtrl) 73 | mockSignerClient.EXPECT().GetRevocationStatus(gomock.Any(), gomock.Any()).Return(&signer.GetRevocationStatusOutput{RevokedEntities: []string{}}, nil) 74 | 75 | resp, err := NewAWSSigner(mockSignerClient).VerifySignature(context.TODO(), request) 76 | assert.NoError(t, err, "VerifySignature() returned error") 77 | assert.Equal(t, expectedResp, resp, "VerifySignatureResponse mismatch") 78 | } 79 | 80 | func TestVerifySignature_ValidationError(t *testing.T) { 81 | tests := map[string]*plugin.VerifySignatureRequest{ 82 | "nilRequest": nil, 83 | "invalidRequest": {ContractVersion: ""}, 84 | } 85 | for name, req := range tests { 86 | t.Run(name, func(t *testing.T) { 87 | _, err := NewAWSSignerForCLI().VerifySignature(context.TODO(), req) 88 | assert.Error(t, err, "VerifySignatureRequest() expected error but not found") 89 | }) 90 | } 91 | } 92 | 93 | func TestGenerateEnvelope(t *testing.T) { 94 | request, expectedResp := getGenerateEnvRequestResponse() 95 | 96 | mockCtrl := gomock.NewController(t) 97 | defer mockCtrl.Finish() 98 | mockSignerClient := client.NewMockInterface(mockCtrl) 99 | mockSignerClient.EXPECT().SignPayload(gomock.Any(), gomock.Any()).Return(&signer.SignPayloadOutput{Signature: expectedResp.SignatureEnvelope}, nil) 100 | 101 | resp, err := NewAWSSigner(mockSignerClient).GenerateEnvelope(context.TODO(), request) 102 | assert.NoError(t, err, "GenerateEnvelope() returned error") 103 | assert.Equal(t, expectedResp, resp, "GenerateEnvelopeResponse mismatch") 104 | } 105 | 106 | func TestGenerateEnvelope_ValidationError(t *testing.T) { 107 | tests := map[string]*plugin.GenerateEnvelopeRequest{ 108 | "nilRequest": nil, 109 | "invalidRequest": {ContractVersion: ""}, 110 | } 111 | for name, req := range tests { 112 | t.Run(name, func(t *testing.T) { 113 | _, err := NewAWSSignerForCLI().GenerateEnvelope(context.TODO(), req) 114 | assert.Error(t, err, "GenerateEnvelope() expected error but not found") 115 | }) 116 | } 117 | } 118 | 119 | func TestGetMetadata(t *testing.T) { 120 | resp, err := NewAWSSignerForCLI().GetMetadata(context.TODO(), nil) 121 | assert.NoError(t, err, "GetMetadata() returned error") 122 | assert.Equal(t, "com.amazonaws.signer.notation.plugin", resp.Name) 123 | assert.Equal(t, "AWS Signer plugin for Notation", resp.Description) 124 | assert.Equal(t, "https://docs.aws.amazon.com/signer", resp.URL) 125 | assert.Equal(t, []string{plugin.ContractVersion}, resp.SupportedContractVersions) 126 | assert.NotEmpty(t, resp.Version, "version is empty") 127 | } 128 | 129 | func TestGenerateSignature_Error(t *testing.T) { 130 | _, err := NewAWSSignerForCLI().GenerateSignature(context.TODO(), nil) 131 | assert.Error(t, err, "expected UnsupportedError but not found") 132 | } 133 | 134 | func TestDescribeKey_Error(t *testing.T) { 135 | _, err := NewAWSSignerForCLI().DescribeKey(context.TODO(), nil) 136 | assert.Error(t, err, "expected UnsupportedError but not found") 137 | } 138 | 139 | func TestGetSignerClientIfNotPresent(t *testing.T) { 140 | signerPlugin := AWSSignerPlugin{} 141 | err := signerPlugin.setSignerClientIfNotPresent(context.TODO(), nil) 142 | assert.NoError(t, err) 143 | } 144 | 145 | func convertCert(certs ...string) [][]byte { 146 | var o [][]byte 147 | for _, cert := range certs { 148 | block, _ := pem.Decode([]byte(cert)) 149 | o = append(o, block.Bytes) 150 | } 151 | return o 152 | } 153 | 154 | func getVerifySignatureRequestResponse() (*plugin.VerifySignatureRequest, *plugin.VerifySignatureResponse) { 155 | attrSigningJob := "com.amazonaws.signer.signingJob" 156 | attrSigningProfileVersion := "com.amazonaws.signer.signingProfileVersion" 157 | now := time.Now() 158 | req := &plugin.VerifySignatureRequest{ 159 | ContractVersion: "1.0", 160 | Signature: plugin.Signature{ 161 | CriticalAttributes: plugin.CriticalAttributes{ 162 | ContentType: "application/vnd.cncf.notary.payload.v1+json", 163 | SigningScheme: "notary.x509.signingAuthority", 164 | AuthenticSigningTime: &now, 165 | Expiry: &now, 166 | ExtendedAttributes: map[string]interface{}{ 167 | attrSigningJob: testJobArn, 168 | attrSigningProfileVersion: testProfileVersionArn, 169 | }, 170 | }, 171 | UnprocessedAttributes: []string{ 172 | attrSigningJob, 173 | attrSigningProfileVersion, 174 | }, 175 | CertificateChain: convertCert(testCertificate), 176 | }, 177 | TrustPolicy: plugin.TrustPolicy{ 178 | TrustedIdentities: []string{testProfileArn}, 179 | SignatureVerification: []plugin.Capability{ 180 | plugin.CapabilityTrustedIdentityVerifier, 181 | plugin.CapabilityRevocationCheckVerifier, 182 | }, 183 | }, 184 | } 185 | 186 | expectedResp := &plugin.VerifySignatureResponse{ 187 | VerificationResults: map[plugin.Capability]*plugin.VerificationResult{ 188 | plugin.CapabilityTrustedIdentityVerifier: { 189 | Success: true, 190 | Reason: fmt.Sprintf("Signature publisher matched \"%s\" trusted identity.", testProfileArn), 191 | }, 192 | plugin.CapabilityRevocationCheckVerifier: { 193 | Success: true, 194 | Reason: "Signature is not revoked.", 195 | }, 196 | }, 197 | ProcessedAttributes: []interface{}{attrSigningProfileVersion, attrSigningJob}, 198 | } 199 | 200 | return req, expectedResp 201 | } 202 | 203 | func getGenerateEnvRequestResponse() (*plugin.GenerateEnvelopeRequest, *plugin.GenerateEnvelopeResponse) { 204 | testPayloadType := "application/vnd.oci.descriptor.v1+json" 205 | testSigEnvType := "application/jose+json" 206 | req := &plugin.GenerateEnvelopeRequest{ 207 | ContractVersion: plugin.ContractVersion, 208 | SignatureEnvelopeType: testSigEnvType, 209 | Payload: []byte("sigME!"), 210 | PayloadType: testPayloadType, 211 | KeyID: "arn:aws:signer:us-west-2:780792624090:/signing-profiles/NotationProfile", 212 | } 213 | expectedResp := &plugin.GenerateEnvelopeResponse{ 214 | SignatureEnvelope: []byte("sigEnv"), 215 | SignatureEnvelopeType: testSigEnvType, 216 | Annotations: nil, 217 | } 218 | return req, expectedResp 219 | } 220 | --------------------------------------------------------------------------------