pax_global_header00006660000000000000000000000064146632142300014513gustar00rootroot0000000000000052 comment=55e35686875491a8d6ad8a47de9c566383fb1b42 notation-core-go-1.1.0/000077500000000000000000000000001466321423000146765ustar00rootroot00000000000000notation-core-go-1.1.0/.github/000077500000000000000000000000001466321423000162365ustar00rootroot00000000000000notation-core-go-1.1.0/.github/.codecov.yml000066400000000000000000000012111466321423000204540ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. coverage: status: project: default: target: 89% notation-core-go-1.1.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001466321423000204215ustar00rootroot00000000000000notation-core-go-1.1.0/.github/ISSUE_TEMPLATE/bug-or-issue.yaml000066400000000000000000000045111466321423000236270ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: 🐛 Bug or Issue description: Something is not working as expected or not working at all! Report it here! labels: [bug, triage] body: - type: markdown attributes: value: | Thank you for taking the time to fill out this issue report. 🛑 Please check existing issues first before continuing: https://github.com/notaryproject/notation-core-go/issues - type: textarea id: verbatim validations: required: true attributes: label: "What is not working as expected?" description: "In your own words, describe what the issue is." - type: textarea id: expect validations: required: true attributes: label: "What did you expect to happen?" description: "A clear and concise description of what you expected to happen." - type: textarea id: reproduce validations: required: true attributes: label: "How can we reproduce it?" description: "Detailed steps to reproduce the behavior. Commands and their outputs are always helpful, code snippets are welcome." - type: textarea id: environment validations: required: true attributes: label: Describe your environment description: "OS and Golang version" - type: textarea id: version validations: required: true attributes: label: What is the version of your notation-core-go Library? description: "Check the `go.mod` file for the library version" - type: markdown attributes: value: | If you want to contribute to this project, we will be happy to guide you through the contribution process especially when you already have a good proposal or understanding of how to fix this issue. Join us at https://slack.cncf.io/ and choose #notary-project channel. notation-core-go-1.1.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000013411466321423000224100ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. blank_issues_enabled: false contact_links: - name: Ask a question url: https://slack.cncf.io/ about: "Join #notary-project channel on CNCF Slack" notation-core-go-1.1.0/.github/ISSUE_TEMPLATE/feature-request.yaml000066400000000000000000000041111466321423000244230ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: 🚀 Feature Request description: Suggest an idea for this project. labels: [enhancement, triage] body: - type: markdown attributes: value: | Thank you for taking the time to suggest a useful feature for the project! - type: textarea id: problem validations: required: true attributes: label: "Is your feature request related to a problem?" description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]" - type: textarea id: solution validations: required: true attributes: label: "What solution do you propose?" description: "A clear and concise description of what you want to happen." - type: textarea id: alternatives validations: required: true attributes: label: "What alternatives have you considered?" description: "A clear and concise description of any alternative solutions or features you've considered." - type: textarea id: context validations: required: false attributes: label: "Any additional context?" description: "Add any other context or screenshots about the feature request here." - type: markdown attributes: value: | If you want to contribute to this project, we will be happy to guide you through the contribution process especially when you already have a good proposal or understanding of how to improve the functionality. Join us at https://slack.cncf.io/ and choose #notary-project channel. notation-core-go-1.1.0/.github/dependabot.yml000066400000000000000000000023611466321423000210700ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # To get started with Dependabot version updates, you'll need to specify which # package ecosystems to update and where the package manifests are located. # Please see the documentation for all configuration options: # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates version: 2 updates: - package-ecosystem: "gomod" # See documentation for possible values directory: "/" # Location of package manifests schedule: interval: "weekly" - package-ecosystem: "github-actions" # Workflow files stored in the # default location of `.github/workflows` directory: "/" schedule: interval: "weekly" notation-core-go-1.1.0/.github/licenserc.yml000066400000000000000000000026571466321423000207420ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. header: license: spdx-id: Apache-2.0 content: | Copyright The Notary Project Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. paths-ignore: - '**/*.md' - 'CODEOWNERS' - 'LICENSE' - 'MAINTAINERS' - 'go.mod' - 'go.sum' - '**/testdata/**' comment: on-failure dependency: files: - go.modnotation-core-go-1.1.0/.github/workflows/000077500000000000000000000000001466321423000202735ustar00rootroot00000000000000notation-core-go-1.1.0/.github/workflows/build.yml000066400000000000000000000014231466321423000221150ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Build on: push: branches: main pull_request: branches: main jobs: build: uses: ./.github/workflows/reusable-build.yml secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}notation-core-go-1.1.0/.github/workflows/codeql.yml000066400000000000000000000014011466321423000222610ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: "CodeQL" on: push: branches: main pull_request: branches: main schedule: - cron: '38 15 * * 1' jobs: analyze: uses: ./.github/workflows/reusable-codeql.yml notation-core-go-1.1.0/.github/workflows/license-checker.yml000066400000000000000000000014471466321423000240500ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: License Checker on: push: branches: main pull_request: branches: main permissions: contents: write pull-requests: write jobs: check-license: uses: ./.github/workflows/reusable-license-checker.ymlnotation-core-go-1.1.0/.github/workflows/reusable-build.yml000066400000000000000000000024331466321423000237170ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Reusable build on: workflow_call: secrets: CODECOV_TOKEN: required: true jobs: build: runs-on: ubuntu-latest strategy: matrix: go-version: ["1.22", "1.23"] fail-fast: true steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Go ${{ matrix.go-version }} environment uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} check-latest: true - name: Run unit tests run: make test - name: Upload coverage to codecov.io uses: codecov/codecov-action@v4 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}notation-core-go-1.1.0/.github/workflows/reusable-codeql.yml000066400000000000000000000025261466321423000240720ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Reusable CodeQL on: workflow_call: jobs: analyze: name: Analyze runs-on: ubuntu-latest permissions: actions: read contents: read security-events: write strategy: matrix: go-version: ["1.22", "1.23"] fail-fast: false steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Go ${{ matrix.go-version }} environment uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} check-latest: true - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: go - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3notation-core-go-1.1.0/.github/workflows/reusable-license-checker.yml000066400000000000000000000022411466321423000256410ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: Reusable license checker on: workflow_call: jobs: check-license: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Check license header uses: apache/skywalking-eyes/header@cd7b195c51fd3d6ad52afceb760719ddc6b3ee91 with: mode: check config: .github/licenserc.yml - name: Check dependencies license uses: apache/skywalking-eyes/dependency@cd7b195c51fd3d6ad52afceb760719ddc6b3ee91 with: config: .github/licenserc.yml flags: --weak-compatible=truenotation-core-go-1.1.0/.github/workflows/stale.yml000066400000000000000000000026701466321423000221330ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. name: "Close stale issues and PRs" on: schedule: - cron: "30 1 * * *" jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: stale-issue-message: "This issue is stale because it has been opened for 60 days with no activity. Remove stale label or comment. Otherwise, it will be closed in 30 days." stale-pr-message: "This PR is stale because it has been opened for 45 days with no activity. Remove stale label or comment. Otherwise, it will be closed in 30 days." close-issue-message: "Issue closed due to no activity in the past 30 days." close-pr-message: "PR closed due to no activity in the past 30 days." days-before-issue-stale: 60 days-before-pr-stale: 45 days-before-issue-close: 30 days-before-pr-close: 30 exempt-all-milestones: true notation-core-go-1.1.0/.gitignore000066400000000000000000000011551466321423000166700ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # VS Code .vscode # Custom coverage.txtnotation-core-go-1.1.0/CODEOWNERS000066400000000000000000000003251466321423000162710ustar00rootroot00000000000000# Repo-Level Owners (in alphabetical order) # Note: This is only for the notaryproject/notation-core-go repo * @gokarnm @JeyJeyGao @niazfk @priteshbandi @rgnote @shizhMSFT @toddysm @Two-Hearts @vaninrao10 @yizha1 notation-core-go-1.1.0/LICENSE000066400000000000000000000261351466321423000157120ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. notation-core-go-1.1.0/MAINTAINERS000066400000000000000000000015511466321423000163750ustar00rootroot00000000000000# Org-Level Maintainers (in alphabetical order) # Pattern: [First Name] [Last Name] <[Email Address]> ([GitHub Handle]) Niaz Khan (@niazfk) Pritesh Bandi (@priteshbandi) Shiwei Zhang (@shizhMSFT) Toddy Mladenov (@toddysm) Vani Rao (@vaninrao10) Yi Zha (@yizha1) # Repo-Level Maintainers (in alphabetical order) # Note: This is for the notaryproject/notation-core-go repo Junjie Gao (@JeyJeyGao) Milind Gokarn (@gokarnm) Patrick Zheng (@Two-Hearts) Rakesh Gariganti (@rgnote) # Emeritus Org Maintainers (in alphabetical order) Justin Cormack (@justincormack) Steve Lasker (@stevelasker)notation-core-go-1.1.0/Makefile000066400000000000000000000024371466321423000163440ustar00rootroot00000000000000# Copyright The Notary Project Authors. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. .PHONY: help help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' .PHONY: all all: test .PHONY: test test: check-line-endings ## run unit tests go test -race -v -coverprofile=coverage.txt -covermode=atomic ./... .PHONY: clean clean: git status --ignored --short | grep '^!! ' | sed 's/!! //' | xargs rm -rf .PHONY: check-line-endings check-line-endings: ## check line endings ! find . -name "*.go" -type f -exec file "{}" ";" | grep CRLF ! find scripts -name "*.sh" -type f -exec file "{}" ";" | grep CRLF .PHONY: fix-line-endings fix-line-endings: ## fix line endings find . -type f -name "*.go" -exec sed -i -e "s/\r//g" {} + notation-core-go-1.1.0/README.md000066400000000000000000000034301466321423000161550ustar00rootroot00000000000000# notation-core-go [![Build Status](https://github.com/notaryproject/notation-core-go/actions/workflows/build.yml/badge.svg?event=push&branch=main)](https://github.com/notaryproject/notation-core-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain) [![codecov](https://codecov.io/gh/notaryproject/notation-core-go/branch/main/graph/badge.svg)](https://codecov.io/gh/notaryproject/notation-core-go) [![Go Reference](https://pkg.go.dev/badge/github.com/notaryproject/notation-core-go.svg)](https://pkg.go.dev/github.com/notaryproject/notation-core-go@main) notation-core-go provides core crypto functionality for notation-go and implements signature generation, parsing and revocation related functionalities based on the [Notary Project specifications](https://github.com/notaryproject/specifications). It also provides validation functionality for certificate and certificate chain. notation-core-go reached a stable release as of July 2023 and continues to be actively developed and maintained. Please visit [README](https://github.com/notaryproject/.github/blob/main/README.md) to know more about Notary Project. > [!NOTE] > The Notary Project documentation is available [here](https://notaryproject.dev/docs/). ## Table of Contents - [Documentation](#documentation) - [Code of Conduct](#code-of-conduct) - [License](#license) ## Documentation Library documentation is available at [Go Reference](https://pkg.go.dev/github.com/notaryproject/notation-core-go). ## Code of Conduct This project has adopted the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for further details. ## License This project is covered under the Apache 2.0 license. You can read the license [here](LICENSE). notation-core-go-1.1.0/go.mod000066400000000000000000000004641466321423000160100ustar00rootroot00000000000000module github.com/notaryproject/notation-core-go go 1.22 require ( github.com/fxamacker/cbor/v2 v2.7.0 github.com/golang-jwt/jwt/v4 v4.5.0 github.com/notaryproject/tspclient-go v0.2.0 github.com/veraison/go-cose v1.1.0 golang.org/x/crypto v0.26.0 ) require github.com/x448/float16 v0.8.4 // indirect notation-core-go-1.1.0/go.sum000066400000000000000000000020201466321423000160230ustar00rootroot00000000000000github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/notaryproject/tspclient-go v0.2.0 h1:g/KpQGmyk/h7j60irIRG1mfWnibNOzJ8WhLqAzuiQAQ= github.com/notaryproject/tspclient-go v0.2.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs= github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o= github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= notation-core-go-1.1.0/internal/000077500000000000000000000000001466321423000165125ustar00rootroot00000000000000notation-core-go-1.1.0/internal/oid/000077500000000000000000000000001466321423000172655ustar00rootroot00000000000000notation-core-go-1.1.0/internal/oid/oid.go000066400000000000000000000022711466321423000203710ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package oid import "encoding/asn1" // KeyUsage (id-ce-keyUsage) is defined in RFC 5280 // // Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.3 var KeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15} // ExtKeyUsage (id-ce-extKeyUsage) is defined in RFC 5280 // // Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.12 var ExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37} // Timestamping (id-kp-timeStamping) is defined in RFC 3161 2.3 // // Reference: https://datatracker.ietf.org/doc/html/rfc3161#section-2.3 var Timestamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} notation-core-go-1.1.0/internal/timestamp/000077500000000000000000000000001466321423000205155ustar00rootroot00000000000000notation-core-go-1.1.0/internal/timestamp/testdata/000077500000000000000000000000001466321423000223265ustar00rootroot00000000000000notation-core-go-1.1.0/internal/timestamp/testdata/TimeStampToken.p7s000066400000000000000000000147031466321423000256720ustar00rootroot000000000000000 *H 010  `He0 *H  0 +2010  `He &p@ޠH@@(kQY<&g*Mty20210917140910Z0WU0S1 0 UBE10U GlobalSign nv-sa1)0'U Globalsign TSA for Advanced - G4d0U0=FiPpMA0  *H  0[1 0 UBE10U GlobalSign nv-sa110/U(GlobalSign Timestamping CA - SHA384 - G40 210527095523Z 320628095522Z0S1 0 UBE10U GlobalSign nv-sa1)0'U Globalsign TSA for Advanced - G400  *H 00i{ nT9_dYu"Niott}QF h.eґU;bBz2Lѹ1%Vg22\-bM 7*`ol=1])Uvl{a)3,nZC;0 xT`8dCpȴ ܻn!5~„8~dz& *%RmL0 x-FW ?yP32@ PAN !D2H,K W[dTjC v֣ 0@6Ўx/ ;aGTpCZhᮓ,r2K+%3h*y00U0U% 0 +0U~px )xY̦$0LU E0C0A +20402+&https://www.globalsign.com/repository/0 U00+009+0-http://ocsp.globalsign.com/ca/gstsacasha384g40C+07http://secure.globalsign.com/cacert/gstsacasha384g4.crt0U#0iWE93@ýe0AU:0806420http://crl.globalsign.com/ca/gstsacasha384g4.crl0  *H  bw/BokEY6+fAղ#I"Ӿ_6ZP?NT q؟6<4fJ7]z>=gK붲oE]LYZfsaE6JcCn: F9ts ^B/"l$'Q]Aud鷇 07bY)rwiux:\jr2YxNf+u 7Au?:BٜQz+L%mOH[\r`7q\IL)mW;W.Ba|UbdUJo3X$g& {gXAUnŻLKBo6df!#4DIgZf3ޙ%qn]L`<rBM&wW a&ȲMdE?Tvlj"\ )K7tf4s}|^=PmW~7k)0%0U0U00UiWE93@ýe0U#0lgS0>+2000.+0"http://ocsp2.globalsign.com/rootr606U/0-0+)'%http://crl.globalsign.com/root-r6.crl0GU @0>0<U 0402+&https://www.globalsign.com/repository/0  *H  Wg+B_ Ļ(VLp :`39k@I%֤ AOἫ#ƅK$D¤вVeނȷaӵFHgzJbг4HEUJ^rqvp7W܍8X©~n&*Hdi/Қr$KtZ}r5WLSLIUOuoJ.jʈ0Ns^֙}K,kY7 ,Eֲnuڍ[/.=}0{|ŬQ0QV`|=" $S'H',y<㪄>J!b0H'|uBkkX4 P{H+Uٍ% QclraJCWŝ<1@l - ƍmqR!>9/z -j<SMGY|CA%,~ } `r@P CD YxƤj"0G0/ @B@"lq0  *H  0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0 190220000000Z 290318100000Z0L1 0U GlobalSign Root CA - R610U  GlobalSign10U GlobalSign0"0  *H 0 sf{< E ,H[+2000.+0"http://ocsp2.globalsign.com/rootr306U/0-0+)'%http://crl.globalsign.com/root-r3.crl0GU @0>0<U 0402+&https://www.globalsign.com/repository/0  *H  I^ŃZa*MJ) z5 3mr"NA?m _,;6YytheYBU9&q8!N[`jC} `aݪ^N2l<»ӐvjiܨXO‹2JT8; u  |%'&Sa52݃:hr$7+$N򶹷G a3-<# 40_0G !XS0  *H  0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0 090318100000Z 290318100000Z0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0"0  *H 0 %vyx"(vŭrFCDz_$.K`FR Gpld,= +׶y;wIjb/^h߉'8>&Y sް&[`I(i;(坊aW7tt:r/.л=3+S:sA :O.2`W˹hh8&`uw I@H1a^wdz_b lTin郓qviB0@0U0U00UK.E$MPc0  *H  K@P TEI A(3kt- sgJD{xnlo)39EÎWlS-$lcShgV>5!hS̐]FzX(/7ADmS(~g׊L'Lssvz- ,Z{Ke] Iw^n2usMula;zZ[\?*[#?3"`kc:nT)@mq ס·=آ_ۏ8rER)I ֟gx0 Ryc13([cHBOԽ;> K^b,-ְTEfa6ēm$7uL0ĕXijŞ n_ \K?考ߩ-f- KteT"<_%O6+fAղ#I"Ӿ_6ZP?NT q؟6<4fJ7]z>=gK붲oE]LYZfsaE6JcCn: F9ts ^B/"l$'Q]Aud鷇 07bY)rwiux:\jr2YxNf+u 7Au?:BٜQz+L%mOH[\r`7q\IL)mW;W.Ba|UbdUJo3X$g& {gXAUnŻLKBo6df!#4DIgZf3ޙ%qn]L`<rBM&wW a&ȲMdE?Tvlj"\ )K7tf4s}|^=PmW~7k)0%0U0U00UiWE93@ýe0U#0lgS0>+2000.+0"http://ocsp2.globalsign.com/rootr606U/0-0+)'%http://crl.globalsign.com/root-r6.crl0GU @0>0<U 0402+&https://www.globalsign.com/repository/0  *H  Wg+B_ Ļ(VLp :`39k@I%֤ AOἫ#ƅK$D¤вVeނȷaӵFHgzJbг4HEUJ^rqvp7W܍8X©~n&*Hdi/Қr$KtZ}r5WLSLIUOuoJ.jʈ0Ns^֙}K,kY7 ,Eֲnuڍ[/.=}0{|ŬQ0QV`|=" $S'H',y<㪄>J!b0H'|uBkkX4 P{H+Uٍ% QclraJCWŝ<1@l - ƍmqR!>9/z -j<SMGY|CA%,~ } `r@P CD YxƤj"0G0/ @B@"lq0  *H  0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0 190220000000Z 290318100000Z0L1 0U GlobalSign Root CA - R610U  GlobalSign10U GlobalSign0"0  *H 0 sf{< E ,H[+2000.+0"http://ocsp2.globalsign.com/rootr306U/0-0+)'%http://crl.globalsign.com/root-r3.crl0GU @0>0<U 0402+&https://www.globalsign.com/repository/0  *H  I^ŃZa*MJ) z5 3mr"NA?m _,;6YytheYBU9&q8!N[`jC} `aݪ^N2l<»ӐvjiܨXO‹2JT8; u  |%'&Sa52݃:hr$7+$N򶹷G a3-<# 40_0G !XS0  *H  0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0 090318100000Z 290318100000Z0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0"0  *H 0 %vyx"(vŭrFCDz_$.K`FR Gpld,= +׶y;wIjb/^h߉'8>&Y sް&[`I(i;(坊aW7tt:r/.л=3+S:sA :O.2`W˹hh8&`uw I@H1a^wdz_b lTin郓qviB0@0U0U00UK.E$MPc0  *H  K@P TEI A(3kt- sgJD{xnlo)39EÎWlS-$lcShgV>5!hS̐]FzX(/7ADmS(~g׊L'Lssvz- ,Z{Ke] Iw^n2usMula;zZ[\?*[#?3"`kc:nT)@mq ס·=آ_ۏ8rER)I ֟gx0 Ryc13([cHBOԽ;> K^b,-ְTEfa6ēm$7uL0ĕXijŞ n_ \K?考ߩ-f- KteT"<_%O6+fAղ#I"Ӿ_6ZP?NT q؟6<4fJ7]z>=gK붲oE]LYZfsaE6JcCn: F9ts ^B/"l$'Q]Aud鷇 07bY)rwiux:\jr2YxNf+u 7Au?:BٜQz+L%mOH[\r`7q\IL)mW;W.Ba|UbdUJo3X$g& {gXAUnŻLKBo6df!#4DIgZf3ޙ%qn]L`<rBM&wW a&ȲMdE?Tvlj"\ )K7tf4s}|^=PmW~7k)0%0U0U00UiWE93@ýe0U#0lgS0>+2000.+0"http://ocsp2.globalsign.com/rootr606U/0-0+)'%http://crl.globalsign.com/root-r6.crl0GU @0>0<U 0402+&https://www.globalsign.com/repository/0  *H  Wg+B_ Ļ(VLp :`39k@I%֤ AOἫ#ƅK$D¤вVeނȷaӵFHgzJbг4HEUJ^rqvp7W܍8X©~n&*Hdi/Қr$KtZ}r5WLSLIUOuoJ.jʈ0Ns^֙}K,kY7 ,Eֲnuڍ[/.=}0{|ŬQ0QV`|=" $S'H',y<㪄>J!b0H'|uBkkX4 P{H+Uٍ% QclraJCWŝ<1@l - ƍmqR!>9/z -j<SMGY|CA%,~ } `r@P CD YxƤj"0G0/ @B@"lq0  *H  0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0 190220000000Z 290318100000Z0L1 0U GlobalSign Root CA - R610U  GlobalSign10U GlobalSign0"0  *H 0 sf{< E ,H[+2000.+0"http://ocsp2.globalsign.com/rootr306U/0-0+)'%http://crl.globalsign.com/root-r3.crl0GU @0>0<U 0402+&https://www.globalsign.com/repository/0  *H  I^ŃZa*MJ) z5 3mr"NA?m _,;6YytheYBU9&q8!N[`jC} `aݪ^N2l<»ӐvjiܨXO‹2JT8; u  |%'&Sa52݃:hr$7+$N򶹷G a3-<# 40_0G !XS0  *H  0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0 090318100000Z 290318100000Z0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0"0  *H 0 %vyx"(vŭrFCDz_$.K`FR Gpld,= +׶y;wIjb/^h߉'8>&Y sް&[`I(i;(坊aW7tt:r/.л=3+S:sA :O.2`W˹hh8&`uw I@H1a^wdz_b lTin郓qviB0@0U0U00UK.E$MPc0  *H  K@P TEI A(3kt- sgJD{xnlo)39EÎWlS-$lcShgV>5!hS̐]FzX(/7ADmS(~g׊L'Lssvz- ,Z{Ke] Iw^n2usMula;zZ[\?*[#?3"`kc:nT)@mq ס·=آ_ۏ8rER)I ֟gx0 Ryc13([cHBOԽ;> K^b,-ְTEfa6ēm$7uL0ĕXijŞ n_ \K?考ߩ-f- KteT"<_%O!C7y2,LC)0Ӭ!3vT"*M .phS\ӝD DfFm%]1QTFMۙ\xy]>LUop0BF“}c{?q|蘮x4% k;AHzswiǶ\X(+l^m{%7hB]Pu6i{ni HY{`zdiCLlS-^r>TȽgLEӹ0#LٙZW̻S,Ğ4L$x_|Jt%>K,V8\yt-a ~~OY_-CRF fGywT[$7EZNDHذ IIeqnE>l gZɤnotation-core-go-1.1.0/internal/timestamp/timestamp.go000066400000000000000000000040501466321423000230460ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package timestamp provides functionalities of timestamp countersignature package timestamp import ( "crypto/x509" "github.com/notaryproject/notation-core-go/signature" nx509 "github.com/notaryproject/notation-core-go/x509" "github.com/notaryproject/tspclient-go" ) // Timestamp generates a timestamp request and sends to TSA. It then validates // the TSA certificate chain against Notary Project certificate and signature // algorithm requirements. // On success, it returns the full bytes of the timestamp token received from // TSA. // // Reference: https://github.com/notaryproject/specifications/blob/v1.0.0/specs/signature-specification.md#leaf-certificates func Timestamp(req *signature.SignRequest, opts tspclient.RequestOptions) ([]byte, error) { tsaRequest, err := tspclient.NewRequest(opts) if err != nil { return nil, err } ctx := req.Context() resp, err := req.Timestamper.Timestamp(ctx, tsaRequest) if err != nil { return nil, err } token, err := resp.SignedToken() if err != nil { return nil, err } info, err := token.Info() if err != nil { return nil, err } timestamp, err := info.Validate(opts.Content) if err != nil { return nil, err } tsaCertChain, err := token.Verify(ctx, x509.VerifyOptions{ CurrentTime: timestamp.Value, Roots: req.TSARootCAs, }) if err != nil { return nil, err } if err := nx509.ValidateTimestampingCertChain(tsaCertChain); err != nil { return nil, err } return resp.TimestampToken.FullBytes, nil } notation-core-go-1.1.0/internal/timestamp/timestamp_test.go000066400000000000000000000127321466321423000241130ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package timestamp import ( "context" "crypto" "crypto/x509" "encoding/asn1" "errors" "os" "strings" "testing" "github.com/notaryproject/notation-core-go/signature" nx509 "github.com/notaryproject/notation-core-go/x509" "github.com/notaryproject/tspclient-go" "github.com/notaryproject/tspclient-go/pki" ) const rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" func TestTimestamp(t *testing.T) { rootCerts, err := nx509.ReadCertificateFile("testdata/tsaRootCert.crt") if err != nil || len(rootCerts) == 0 { t.Fatal("failed to read root CA certificate:", err) } rootCert := rootCerts[0] rootCAs := x509.NewCertPool() rootCAs.AddCert(rootCert) // --------------- Success case ---------------------------------- timestamper, err := tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl) if err != nil { t.Fatal(err) } req := &signature.SignRequest{ Timestamper: timestamper, TSARootCAs: rootCAs, } opts := tspclient.RequestOptions{ Content: []byte("notation"), HashAlgorithm: crypto.SHA256, } _, err = Timestamp(req, opts) if err != nil { t.Fatal(err) } // ------------- Failure cases ------------------------ opts = tspclient.RequestOptions{ Content: []byte("notation"), HashAlgorithm: crypto.SHA1, } expectedErr := "malformed timestamping request: unsupported hashing algorithm: SHA-1" _, err = Timestamp(req, opts) assertErrorEqual(expectedErr, err, t) req = &signature.SignRequest{ Timestamper: dummyTimestamper{}, TSARootCAs: rootCAs, } opts = tspclient.RequestOptions{ Content: []byte("notation"), HashAlgorithm: crypto.SHA256, NoNonce: true, } expectedErr = "failed to timestamp" _, err = Timestamp(req, opts) if err == nil || !strings.Contains(err.Error(), expectedErr) { t.Fatalf("expected error message to contain %s, but got %v", expectedErr, err) } req = &signature.SignRequest{ Timestamper: dummyTimestamper{ respWithRejectedStatus: true, }, TSARootCAs: rootCAs, } expectedErr = "invalid timestamping response: invalid response with status code 2: rejected" _, err = Timestamp(req, opts) assertErrorEqual(expectedErr, err, t) req = &signature.SignRequest{ Timestamper: dummyTimestamper{ invalidTSTInfo: true, }, TSARootCAs: rootCAs, } expectedErr = "cannot unmarshal TSTInfo from timestamp token: asn1: structure error: tags don't match (23 vs {class:0 tag:16 length:3 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue: tag: stringType:0 timeType:24 set:false omitEmpty:false} Time @89" _, err = Timestamp(req, opts) assertErrorEqual(expectedErr, err, t) opts = tspclient.RequestOptions{ Content: []byte("mismatch"), HashAlgorithm: crypto.SHA256, NoNonce: true, } req = &signature.SignRequest{ Timestamper: dummyTimestamper{ failValidate: true, }, TSARootCAs: rootCAs, } expectedErr = "invalid TSTInfo: mismatched message" _, err = Timestamp(req, opts) assertErrorEqual(expectedErr, err, t) opts = tspclient.RequestOptions{ Content: []byte("notation"), HashAlgorithm: crypto.SHA256, NoNonce: true, } req = &signature.SignRequest{ Timestamper: dummyTimestamper{ invalidSignature: true, }, TSARootCAs: rootCAs, } expectedErr = "failed to verify signed token: cms verification failure: crypto/rsa: verification error" _, err = Timestamp(req, opts) assertErrorEqual(expectedErr, err, t) } func assertErrorEqual(expected string, err error, t *testing.T) { if err == nil || expected != err.Error() { t.Fatalf("Expected error \"%v\" but was \"%v\"", expected, err) } } type dummyTimestamper struct { respWithRejectedStatus bool invalidTSTInfo bool failValidate bool invalidSignature bool } func (d dummyTimestamper) Timestamp(context.Context, *tspclient.Request) (*tspclient.Response, error) { if d.respWithRejectedStatus { return &tspclient.Response{ Status: pki.StatusInfo{ Status: pki.StatusRejection, }, }, nil } if d.invalidTSTInfo { token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidTSTInfo.p7s") if err != nil { return nil, err } return &tspclient.Response{ Status: pki.StatusInfo{ Status: pki.StatusGranted, }, TimestampToken: asn1.RawValue{ FullBytes: token, }, }, nil } if d.failValidate { token, err := os.ReadFile("testdata/TimeStampToken.p7s") if err != nil { return nil, err } return &tspclient.Response{ Status: pki.StatusInfo{ Status: pki.StatusGranted, }, TimestampToken: asn1.RawValue{ FullBytes: token, }, }, nil } if d.invalidSignature { token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidSignature.p7s") if err != nil { return nil, err } return &tspclient.Response{ Status: pki.StatusInfo{ Status: pki.StatusGranted, }, TimestampToken: asn1.RawValue{ FullBytes: token, }, }, nil } return nil, errors.New("failed to timestamp") } notation-core-go-1.1.0/revocation/000077500000000000000000000000001466321423000170475ustar00rootroot00000000000000notation-core-go-1.1.0/revocation/ocsp/000077500000000000000000000000001466321423000200135ustar00rootroot00000000000000notation-core-go-1.1.0/revocation/ocsp/errors.go000066400000000000000000000037251466321423000216650ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package ocsp provides methods for checking the OCSP revocation status of a // certificate chain, as well as errors related to these checks package ocsp import ( "fmt" "time" ) // RevokedError is returned when the certificate's status for OCSP is // ocsp.Revoked type RevokedError struct{} func (e RevokedError) Error() string { return "certificate is revoked via OCSP" } // UnknownStatusError is returned when the certificate's status for OCSP is // ocsp.Unknown type UnknownStatusError struct{} func (e UnknownStatusError) Error() string { return "certificate has unknown status via OCSP" } // GenericError is returned when there is an error during the OCSP revocation // check, not necessarily a revocation type GenericError struct { Err error } func (e GenericError) Error() string { msg := "error checking revocation status via OCSP" if e.Err != nil { return fmt.Sprintf("%s: %v", msg, e.Err) } return msg } // NoServerError is returned when the OCSPServer is not specified. type NoServerError struct{} func (e NoServerError) Error() string { return "no valid OCSP server found" } // TimeoutError is returned when the connection attempt to an OCSP URL exceeds // the specified threshold type TimeoutError struct { timeout time.Duration } func (e TimeoutError) Error() string { return fmt.Sprintf("exceeded timeout threshold of %.2f seconds for OCSP check", e.timeout.Seconds()) } notation-core-go-1.1.0/revocation/ocsp/errors_test.go000066400000000000000000000042251466321423000227200ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ocsp import ( "errors" "fmt" "testing" "time" ) func TestRevokedError(t *testing.T) { err := &RevokedError{} expectedMsg := "certificate is revoked via OCSP" if err.Error() != expectedMsg { t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) } } func TestUnknownStatusError(t *testing.T) { err := &UnknownStatusError{} expectedMsg := "certificate has unknown status via OCSP" if err.Error() != expectedMsg { t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) } } func TestGenericError(t *testing.T) { t.Run("without_inner_error", func(t *testing.T) { err := &GenericError{} expectedMsg := "error checking revocation status via OCSP" if err.Error() != expectedMsg { t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) } }) t.Run("with_inner_error", func(t *testing.T) { err := &GenericError{Err: errors.New("inner error")} expectedMsg := "error checking revocation status via OCSP: inner error" if err.Error() != expectedMsg { t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) } }) } func TestNoServerError(t *testing.T) { err := &NoServerError{} expectedMsg := "no valid OCSP server found" if err.Error() != expectedMsg { t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) } } func TestTimeoutError(t *testing.T) { duration := 5 * time.Second err := &TimeoutError{duration} expectedMsg := fmt.Sprintf("exceeded timeout threshold of %.2f seconds for OCSP check", duration.Seconds()) if err.Error() != expectedMsg { t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) } } notation-core-go-1.1.0/revocation/ocsp/ocsp.go000066400000000000000000000243371466321423000213170ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package ocsp provides methods for checking the OCSP revocation status of a // certificate chain, as well as errors related to these checks package ocsp import ( "bytes" "crypto" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/base64" "errors" "fmt" "io" "net/http" "net/url" "strings" "sync" "time" "github.com/notaryproject/notation-core-go/revocation/purpose" "github.com/notaryproject/notation-core-go/revocation/result" coreX509 "github.com/notaryproject/notation-core-go/x509" "golang.org/x/crypto/ocsp" ) // Options specifies values that are needed to check OCSP revocation type Options struct { CertChain []*x509.Certificate // CertChainPurpose is the purpose of the certificate chain. Supported // values are CodeSigning and Timestamping. // When not provided, the default value is CodeSigning. CertChainPurpose purpose.Purpose SigningTime time.Time HTTPClient *http.Client } const ( pkixNoCheckOID string = "1.3.6.1.5.5.7.48.1.5" invalidityDateOID string = "2.5.29.24" // Max size determined from https://www.ibm.com/docs/en/sva/9.0.6?topic=stanza-ocsp-max-response-size. // Typical size is ~4 KB ocspMaxResponseSize int64 = 20480 //bytes ) // CheckStatus checks OCSP based on the passed options and returns an array of // result.CertRevocationResult objects that contains the results and error. The // length of this array will always be equal to the length of the certificate // chain. func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { if len(opts.CertChain) == 0 { return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} } switch opts.CertChainPurpose { case purpose.CodeSigning: // Since ValidateCodeSigningCertChain is using authentic signing time, // signing time may be zero. // Thus, it is better to pass nil here than fail for a cert's NotBefore // being after zero time if err := coreX509.ValidateCodeSigningCertChain(opts.CertChain, nil); err != nil { return nil, result.InvalidChainError{Err: err} } case purpose.Timestamping: if err := coreX509.ValidateTimestampingCertChain(opts.CertChain); err != nil { return nil, result.InvalidChainError{Err: err} } default: return nil, result.InvalidChainError{Err: fmt.Errorf("unsupported certificate chain purpose %v", opts.CertChainPurpose)} } certResults := make([]*result.CertRevocationResult, len(opts.CertChain)) // Check status for each cert in cert chain var wg sync.WaitGroup for i, cert := range opts.CertChain[:len(opts.CertChain)-1] { wg.Add(1) // Assume cert chain is accurate and next cert in chain is the issuer go func(i int, cert *x509.Certificate) { defer wg.Done() certResults[i] = certCheckStatus(cert, opts.CertChain[i+1], opts) }(i, cert) } // Last is root cert, which will never be revoked by OCSP certResults[len(opts.CertChain)-1] = &result.CertRevocationResult{ Result: result.ResultNonRevokable, ServerResults: []*result.ServerResult{{ Result: result.ResultNonRevokable, Error: nil, }}, } wg.Wait() return certResults, nil } func certCheckStatus(cert, issuer *x509.Certificate, opts Options) *result.CertRevocationResult { ocspURLs := cert.OCSPServer if len(ocspURLs) == 0 { // OCSP not enabled for this certificate. return &result.CertRevocationResult{ Result: result.ResultNonRevokable, ServerResults: []*result.ServerResult{toServerResult("", NoServerError{})}, } } serverResults := make([]*result.ServerResult, len(ocspURLs)) for serverIndex, server := range ocspURLs { serverResult := checkStatusFromServer(cert, issuer, server, opts) if serverResult.Result == result.ResultOK || serverResult.Result == result.ResultRevoked || (serverResult.Result == result.ResultUnknown && errors.Is(serverResult.Error, UnknownStatusError{})) { // A valid response has been received from an OCSP server // Result should be based on only this response, not any errors from // other servers return serverResultsToCertRevocationResult([]*result.ServerResult{serverResult}) } serverResults[serverIndex] = serverResult } return serverResultsToCertRevocationResult(serverResults) } func checkStatusFromServer(cert, issuer *x509.Certificate, server string, opts Options) *result.ServerResult { // Check valid server if serverURL, err := url.Parse(server); err != nil || !strings.EqualFold(serverURL.Scheme, "http") { // This function is only able to check servers that are accessible via HTTP return toServerResult(server, GenericError{Err: fmt.Errorf("OCSPServer protocol %s is not supported", serverURL.Scheme)}) } // Create OCSP Request resp, err := executeOCSPCheck(cert, issuer, server, opts) if err != nil { // If there is a server error, attempt all servers before determining what to return // to the user return toServerResult(server, err) } // Validate OCSP response isn't expired if time.Now().After(resp.NextUpdate) { return toServerResult(server, GenericError{Err: errors.New("expired OCSP response")}) } // Handle pkix-ocsp-no-check and id-ce-invalidityDate extensions if present // in response extensionMap := extensionsToMap(resp.Extensions) if _, foundNoCheck := extensionMap[pkixNoCheckOID]; !foundNoCheck { // This will be ignored until CRL is implemented // If it isn't found, CRL should be used to verify the OCSP response _ = foundNoCheck // needed to bypass linter warnings (Remove after adding CRL) // TODO: add CRL support // https://github.com/notaryproject/notation-core-go/issues/125 } if invalidityDateBytes, foundInvalidityDate := extensionMap[invalidityDateOID]; foundInvalidityDate && !opts.SigningTime.IsZero() && resp.Status == ocsp.Revoked { var invalidityDate time.Time rest, err := asn1.UnmarshalWithParams(invalidityDateBytes, &invalidityDate, "generalized") if len(rest) == 0 && err == nil && opts.SigningTime.Before(invalidityDate) { return toServerResult(server, nil) } } // No errors, valid server response switch resp.Status { case ocsp.Good: return toServerResult(server, nil) case ocsp.Revoked: return toServerResult(server, RevokedError{}) default: // ocsp.Unknown return toServerResult(server, UnknownStatusError{}) } } func extensionsToMap(extensions []pkix.Extension) map[string][]byte { extensionMap := make(map[string][]byte) for _, extension := range extensions { extensionMap[extension.Id.String()] = extension.Value } return extensionMap } func executeOCSPCheck(cert, issuer *x509.Certificate, server string, opts Options) (*ocsp.Response, error) { // TODO: Look into other alternatives for specifying the Hash // https://github.com/notaryproject/notation-core-go/issues/139 // The following do not support SHA256 hashes: // - Microsoft // - Entrust // - Let's Encrypt // - Digicert (sometimes) // As this represents a large percentage of public CAs, we are using the // hashing algorithm SHA1, which has been confirmed to be supported by all // that were tested. ocspRequest, err := ocsp.CreateRequest(cert, issuer, &ocsp.RequestOptions{Hash: crypto.SHA1}) if err != nil { return nil, GenericError{Err: err} } var resp *http.Response postRequired := base64.StdEncoding.EncodedLen(len(ocspRequest)) >= 255 if !postRequired { encodedReq := url.QueryEscape(base64.StdEncoding.EncodeToString(ocspRequest)) if len(encodedReq) < 255 { var reqURL string reqURL, err = url.JoinPath(server, encodedReq) if err != nil { return nil, GenericError{Err: err} } resp, err = opts.HTTPClient.Get(reqURL) } else { resp, err = postRequest(ocspRequest, server, opts.HTTPClient) } } else { resp, err = postRequest(ocspRequest, server, opts.HTTPClient) } if err != nil { var urlErr *url.Error if errors.As(err, &urlErr) && urlErr.Timeout() { return nil, TimeoutError{} } return nil, GenericError{Err: err} } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("failed to retrieve OCSP: response had status code %d", resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, ocspMaxResponseSize)) if err != nil { return nil, GenericError{Err: err} } switch { case bytes.Equal(body, ocsp.UnauthorizedErrorResponse): return nil, GenericError{Err: errors.New("OCSP unauthorized")} case bytes.Equal(body, ocsp.MalformedRequestErrorResponse): return nil, GenericError{Err: errors.New("OCSP malformed")} case bytes.Equal(body, ocsp.InternalErrorErrorResponse): return nil, GenericError{Err: errors.New("OCSP internal error")} case bytes.Equal(body, ocsp.TryLaterErrorResponse): return nil, GenericError{Err: errors.New("OCSP try later")} case bytes.Equal(body, ocsp.SigRequredErrorResponse): return nil, GenericError{Err: errors.New("OCSP signature required")} } return ocsp.ParseResponseForCert(body, cert, issuer) } func postRequest(req []byte, server string, httpClient *http.Client) (*http.Response, error) { reader := bytes.NewReader(req) return httpClient.Post(server, "application/ocsp-request", reader) } func toServerResult(server string, err error) *result.ServerResult { switch t := err.(type) { case nil: return result.NewServerResult(result.ResultOK, server, nil) case NoServerError: return result.NewServerResult(result.ResultNonRevokable, server, nil) case RevokedError: return result.NewServerResult(result.ResultRevoked, server, t) default: // Includes GenericError, UnknownStatusError, result.InvalidChainError, // and TimeoutError return result.NewServerResult(result.ResultUnknown, server, t) } } func serverResultsToCertRevocationResult(serverResults []*result.ServerResult) *result.CertRevocationResult { return &result.CertRevocationResult{ Result: serverResults[len(serverResults)-1].Result, ServerResults: serverResults, } } notation-core-go-1.1.0/revocation/ocsp/ocsp_test.go000066400000000000000000000707071466321423000223600ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ocsp import ( "crypto/x509" "errors" "fmt" "net/http" "testing" "time" "github.com/notaryproject/notation-core-go/revocation/purpose" "github.com/notaryproject/notation-core-go/revocation/result" "github.com/notaryproject/notation-core-go/testhelper" "golang.org/x/crypto/ocsp" ) func validateEquivalentCertResults(certResults, expectedCertResults []*result.CertRevocationResult, t *testing.T) { if len(certResults) != len(expectedCertResults) { t.Errorf("Length of certResults (%d) did not match expected length (%d)", len(certResults), len(expectedCertResults)) return } for i, certResult := range certResults { if certResult.Result != expectedCertResults[i].Result { t.Errorf("Expected certResults[%d].Result to be %s, but got %s", i, expectedCertResults[i].Result, certResult.Result) } if len(certResult.ServerResults) != len(expectedCertResults[i].ServerResults) { t.Errorf("Length of certResults[%d].ServerResults (%d) did not match expected length (%d)", i, len(certResult.ServerResults), len(expectedCertResults[i].ServerResults)) return } for j, serverResult := range certResult.ServerResults { if serverResult.Result != expectedCertResults[i].ServerResults[j].Result { t.Errorf("Expected certResults[%d].ServerResults[%d].Result to be %s, but got %s", i, j, expectedCertResults[i].ServerResults[j].Result, serverResult.Result) } if serverResult.Server != expectedCertResults[i].ServerResults[j].Server { t.Errorf("Expected certResults[%d].ServerResults[%d].Server to be %s, but got %s", i, j, expectedCertResults[i].ServerResults[j].Server, serverResult.Server) } if serverResult.Error == nil { if expectedCertResults[i].ServerResults[j].Error == nil { continue } t.Errorf("certResults[%d].ServerResults[%d].Error was nil, but expected %v", i, j, expectedCertResults[i].ServerResults[j].Error) } else if expectedCertResults[i].ServerResults[j].Error == nil { t.Errorf("Unexpected error for certResults[%d].ServerResults[%d].Error: %v", i, j, serverResult.Error) } else if serverResult.Error.Error() != expectedCertResults[i].ServerResults[j].Error.Error() { t.Errorf("Expected certResults[%d].ServerResults[%d].Error to be %v, but got %v", i, j, expectedCertResults[i].ServerResults[j].Error, serverResult.Error) } } } } func getOKCertResult(server string) *result.CertRevocationResult { return &result.CertRevocationResult{ Result: result.ResultOK, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultOK, server, nil), }, } } func getRootCertResult() *result.CertRevocationResult { return &result.CertRevocationResult{ Result: result.ResultNonRevokable, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultNonRevokable, "", nil), }, } } func TestCheckStatus(t *testing.T) { revokableCertTuple := testhelper.GetRevokableRSALeafCertificate() revokableIssuerTuple := testhelper.GetRSARootCertificate() ocspServer := revokableCertTuple.Cert.OCSPServer[0] revokableChain := []*x509.Certificate{revokableCertTuple.Cert, revokableIssuerTuple.Cert} testChain := []testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple} t.Run("check non-revoked cert", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ CertChain: revokableChain, SigningTime: time.Now(), HTTPClient: client, } certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) }) t.Run("check cert with Unknown OCSP response", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Unknown}, nil, true) opts := Options{ CertChain: revokableChain, SigningTime: time.Now(), HTTPClient: client, } certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{{ Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, ocspServer, UnknownStatusError{}), }, }} validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) }) t.Run("check OCSP revoked cert", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Revoked}, nil, true) opts := Options{ CertChain: revokableChain, SigningTime: time.Now(), HTTPClient: client, } certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{{ Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, ocspServer, RevokedError{}), }, }} validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) }) t.Run("check OCSP future revoked cert", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Revoked}, &revokedTime, true) opts := Options{ CertChain: revokableChain, SigningTime: time.Now(), HTTPClient: client, } certResult := certCheckStatus(revokableChain[0], revokableChain[1], opts) expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) }) } func TestCheckStatusForSelfSignedCert(t *testing.T) { selfSignedTuple := testhelper.GetRSASelfSignedSigningCertTuple("Notation revocation test self-signed cert") client := testhelper.MockClient([]testhelper.RSACertTuple{selfSignedTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ CertChain: []*x509.Certificate{selfSignedTuple.Cert}, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{getRootCertResult()} validateEquivalentCertResults(certResults, expectedCertResults, t) } func TestCheckStatusForRootCert(t *testing.T) { rootTuple := testhelper.GetRSARootCertificate() client := testhelper.MockClient([]testhelper.RSACertTuple{rootTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ CertChain: []*x509.Certificate{rootTuple.Cert}, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) expectedErr := result.InvalidChainError{Err: errors.New("invalid self-signed certificate. Error: certificate with subject \"CN=Notation Test RSA Root,O=Notary,L=Seattle,ST=WA,C=US\": if the basic constraints extension is present, the ca field must be set to false")} if err == nil || err.Error() != expectedErr.Error() { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", expectedErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } } func TestCheckStatusForNonSelfSignedSingleCert(t *testing.T) { certTuple := testhelper.GetRSALeafCertificate() client := testhelper.MockClient([]testhelper.RSACertTuple{certTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ CertChain: []*x509.Certificate{certTuple.Cert}, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) expectedErr := result.InvalidChainError{Err: errors.New("invalid self-signed certificate. subject: \"CN=Notation Test RSA Leaf Cert,O=Notary,L=Seattle,ST=WA,C=US\". Error: crypto/rsa: verification error")} if err == nil || err.Error() != expectedErr.Error() { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", expectedErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } } func TestCheckStatusForChain(t *testing.T) { zeroTime := time.Time{} testChain := testhelper.GetRevokableRSAChain(6) revokableChain := make([]*x509.Certificate, 6) for i, tuple := range testChain { revokableChain[i] = tuple.Cert revokableChain[i].NotBefore = zeroTime } t.Run("empty chain", func(t *testing.T) { opts := Options{ CertChain: []*x509.Certificate{}, SigningTime: time.Now(), HTTPClient: http.DefaultClient, } certResults, err := CheckStatus(opts) expectedErr := result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} if err == nil || err.Error() != expectedErr.Error() { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", expectedErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("check non-revoked chain", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ CertChain: revokableChain, CertChainPurpose: purpose.CodeSigning, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), getOKCertResult(revokableChain[2].OCSPServer[0]), getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check chain with 1 Unknown cert", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good}, nil, true) // 3rd cert will be unknown, the rest will be good opts := Options{ CertChain: revokableChain, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], UnknownStatusError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 revoked cert", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) // 3rd cert will be revoked, the rest will be good opts := Options{ CertChain: revokableChain, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], RevokedError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 unknown and 1 revoked cert", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) // 3rd cert will be unknown, 5th will be revoked, the rest will be good opts := Options{ CertChain: revokableChain, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], UnknownStatusError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], RevokedError{}), }, }, getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 future revoked cert", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) // 3rd cert will be revoked, the rest will be good opts := Options{ CertChain: revokableChain, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), getOKCertResult(revokableChain[2].OCSPServer[0]), getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 unknown and 1 future revoked cert", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) // 3rd cert will be unknown, 5th will be revoked, the rest will be good opts := Options{ CertChain: revokableChain, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], UnknownStatusError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 revoked cert before signing time", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) // 3rd cert will be revoked, the rest will be good opts := Options{ CertChain: revokableChain, SigningTime: time.Now().Add(time.Hour), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], RevokedError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 revoked cert after zero signing time", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) // 3rd cert will be revoked, the rest will be good opts := Options{ CertChain: revokableChain, SigningTime: zeroTime, HTTPClient: client, } if !zeroTime.IsZero() { t.Errorf("exected zeroTime.IsZero() to be true") } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], RevokedError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) } func TestCheckStatusErrors(t *testing.T) { leafCertTuple := testhelper.GetRSALeafCertificate() rootCertTuple := testhelper.GetRSARootCertificate() noOCSPChain := []*x509.Certificate{leafCertTuple.Cert, rootCertTuple.Cert} revokableTuples := testhelper.GetRevokableRSAChain(3) noRootChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert} backwardsChain := []*x509.Certificate{revokableTuples[2].Cert, revokableTuples[1].Cert, revokableTuples[0].Cert} okChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert, revokableTuples[2].Cert} expiredLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw) expiredLeaf.IsCA = false expiredLeaf.KeyUsage = x509.KeyUsageDigitalSignature expiredLeaf.OCSPServer = []string{"http://example.com/expired_ocsp"} expiredChain := []*x509.Certificate{expiredLeaf, revokableTuples[1].Cert, revokableTuples[2].Cert} noHTTPLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw) noHTTPLeaf.IsCA = false noHTTPLeaf.KeyUsage = x509.KeyUsageDigitalSignature noHTTPLeaf.OCSPServer = []string{"ldap://ds.example.com:123/chain_ocsp/0"} noHTTPChain := []*x509.Certificate{noHTTPLeaf, revokableTuples[1].Cert, revokableTuples[2].Cert} timestampSigningCertErr := result.InvalidChainError{Err: errors.New("timestamp signing certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 3,O=Notary,L=Seattle,ST=WA,C=US\" must have and only have Timestamping as extended key usage")} backwardsChainErr := result.InvalidChainError{Err: errors.New("leaf certificate with subject \"CN=Notation Test Revokable RSA Chain Cert Root,O=Notary,L=Seattle,ST=WA,C=US\" is self-signed. Certificate chain must not contain self-signed leaf certificate")} chainRootErr := result.InvalidChainError{Err: errors.New("root certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US\" is not self-signed. Certificate chain must end with a valid self-signed root certificate")} expiredRespErr := GenericError{Err: errors.New("expired OCSP response")} noHTTPErr := GenericError{Err: errors.New("OCSPServer protocol ldap is not supported")} t.Run("no OCSPServer specified", func(t *testing.T) { opts := Options{ CertChain: noOCSPChain, SigningTime: time.Now(), HTTPClient: http.DefaultClient, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ { Result: result.ResultNonRevokable, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultNonRevokable, "", nil), }, }, getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("chain missing root", func(t *testing.T) { opts := Options{ CertChain: noRootChain, SigningTime: time.Now(), HTTPClient: http.DefaultClient, } certResults, err := CheckStatus(opts) if err == nil || err.Error() != chainRootErr.Error() { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", chainRootErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("backwards chain", func(t *testing.T) { opts := Options{ CertChain: backwardsChain, SigningTime: time.Now(), HTTPClient: http.DefaultClient, } certResults, err := CheckStatus(opts) if err == nil || err.Error() != backwardsChainErr.Error() { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", backwardsChainErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("check codesigning cert with PurposeTimestamping", func(t *testing.T) { opts := Options{ CertChain: okChain, CertChainPurpose: purpose.Timestamping, SigningTime: time.Now(), HTTPClient: http.DefaultClient, } certResults, err := CheckStatus(opts) if err == nil || err.Error() != timestampSigningCertErr.Error() { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", timestampSigningCertErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("check with default CertChainPurpose", func(t *testing.T) { opts := Options{ CertChain: okChain, SigningTime: time.Now(), HTTPClient: http.DefaultClient, } _, err := CheckStatus(opts) if err != nil { t.Fatal(err) } }) t.Run("check with unknwon CertChainPurpose", func(t *testing.T) { opts := Options{ CertChain: okChain, CertChainPurpose: -1, SigningTime: time.Now(), HTTPClient: http.DefaultClient, } certResults, err := CheckStatus(opts) if err == nil || err.Error() != "invalid chain: expected chain to be correct and complete: unsupported certificate chain purpose -1" { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", timestampSigningCertErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("timeout", func(t *testing.T) { timeoutClient := &http.Client{Timeout: 1 * time.Nanosecond} opts := Options{ CertChain: okChain, SigningTime: time.Now(), HTTPClient: timeoutClient, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, okChain[0].OCSPServer[0], TimeoutError{}), }, }, { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, okChain[1].OCSPServer[0], TimeoutError{}), }, }, getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("expired ocsp response", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ CertChain: expiredChain, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, expiredChain[0].OCSPServer[0], expiredRespErr), }, }, getOKCertResult(expiredChain[1].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("pkixNoCheck missing", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, false) opts := Options{ CertChain: okChain, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(okChain[0].OCSPServer[0]), getOKCertResult(okChain[1].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("non-HTTP URI error", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ CertChain: noHTTPChain, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, noHTTPChain[0].OCSPServer[0], noHTTPErr), }, }, getOKCertResult(noHTTPChain[1].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) } func TestCheckOCSPInvalidChain(t *testing.T) { revokableTuples := testhelper.GetRevokableRSAChain(4) misorderedIntermediateTuples := []testhelper.RSACertTuple{revokableTuples[1], revokableTuples[0], revokableTuples[2], revokableTuples[3]} misorderedIntermediateChain := []*x509.Certificate{revokableTuples[1].Cert, revokableTuples[0].Cert, revokableTuples[2].Cert, revokableTuples[3].Cert} for i, cert := range misorderedIntermediateChain { if i != (len(misorderedIntermediateChain) - 1) { // Skip root which won't have an OCSP Server cert.OCSPServer[0] = fmt.Sprintf("http://example.com/chain_ocsp/%d", i) } } missingIntermediateChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[2].Cert, revokableTuples[3].Cert} for i, cert := range missingIntermediateChain { if i != (len(missingIntermediateChain) - 1) { // Skip root which won't have an OCSP Server cert.OCSPServer[0] = fmt.Sprintf("http://example.com/chain_ocsp/%d", i) } } missingIntermediateErr := result.InvalidChainError{Err: errors.New("certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 4,O=Notary,L=Seattle,ST=WA,C=US\" is not issued by \"CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US\"")} misorderedChainErr := result.InvalidChainError{Err: errors.New("invalid certificates or certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 3,O=Notary,L=Seattle,ST=WA,C=US\" is not issued by \"CN=Notation Test Revokable RSA Chain Cert 4,O=Notary,L=Seattle,ST=WA,C=US\". Error: x509: invalid signature: parent certificate cannot sign this kind of certificate")} t.Run("chain missing intermediate", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ CertChain: missingIntermediateChain, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err == nil || err.Error() != missingIntermediateErr.Error() { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", missingIntermediateErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("chain out of order", func(t *testing.T) { client := testhelper.MockClient(misorderedIntermediateTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) opts := Options{ CertChain: misorderedIntermediateChain, SigningTime: time.Now(), HTTPClient: client, } certResults, err := CheckStatus(opts) if err == nil || err.Error() != misorderedChainErr.Error() { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", misorderedChainErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) } notation-core-go-1.1.0/revocation/purpose/000077500000000000000000000000001466321423000205445ustar00rootroot00000000000000notation-core-go-1.1.0/revocation/purpose/purpose.go000066400000000000000000000017671466321423000226030ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package purpose provides purposes of the certificate chain whose revocation // status is checked package purpose // Purpose is an enum for purpose of the certificate chain whose revocation // status is checked type Purpose int const ( // CodeSigning means the certificate chain is a code signing chain CodeSigning Purpose = iota // Timestamping means the certificate chain is a timestamping chain Timestamping ) notation-core-go-1.1.0/revocation/result/000077500000000000000000000000001466321423000203655ustar00rootroot00000000000000notation-core-go-1.1.0/revocation/result/errors.go000066400000000000000000000020471466321423000222330ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package result provides general objects that are used across revocation package result import ( "fmt" ) // InvalidChainError is returned when the certificate chain does not meet the // requirements for a valid certificate chain type InvalidChainError struct { Err error } func (e InvalidChainError) Error() string { msg := "invalid chain: expected chain to be correct and complete" if e.Err != nil { return fmt.Sprintf("%s: %v", msg, e.Err) } return msg } notation-core-go-1.1.0/revocation/result/errors_test.go000066400000000000000000000023431466321423000232710ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package result import ( "errors" "testing" ) func TestInvalidChainError(t *testing.T) { t.Run("without_inner_error", func(t *testing.T) { err := &InvalidChainError{} expectedMsg := "invalid chain: expected chain to be correct and complete" if err.Error() != expectedMsg { t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) } }) t.Run("inner_error", func(t *testing.T) { err := &InvalidChainError{Err: errors.New("inner error")} expectedMsg := "invalid chain: expected chain to be correct and complete: inner error" if err.Error() != expectedMsg { t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) } }) } notation-core-go-1.1.0/revocation/result/results.go000066400000000000000000000073301466321423000224200ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package result provides general objects that are used across revocation package result import "strconv" // Result is a type of enumerated value to help characterize errors. It can be // OK, Unknown, or Revoked type Result int const ( // ResultUnknown is a Result that indicates that some error other than a // revocation was encountered during the revocation check ResultUnknown Result = iota // ResultOK is a Result that indicates that the revocation check resulted in no // important errors ResultOK // ResultNonRevokable is a Result that indicates that the certificate cannot be // checked for revocation. This may be a result of no OCSP servers being // specified, the cert is a root certificate, or other related situations. ResultNonRevokable // ResultRevoked is a Result that indicates that at least one certificate was // revoked when performing a revocation check on the certificate chain ResultRevoked ) // String provides a conversion from a Result to a string func (r Result) String() string { switch r { case ResultOK: return "OK" case ResultNonRevokable: return "NonRevokable" case ResultUnknown: return "Unknown" case ResultRevoked: return "Revoked" default: return "invalid result with value " + strconv.Itoa(int(r)) } } // ServerResult encapsulates the result for a single server for a single // certificate in the chain type ServerResult struct { // Result of revocation for this server (Unknown if there is an error which // prevents the retrieval of a valid status) Result Result // Server is the URI associated with this result. If no server is associated // with the result (e.g. it is a root certificate or no OCSPServers are // specified), then this will be an empty string ("") Server string // Error is set if there is an error associated with the revocation check // to this server Error error } // NewServerResult creates a ServerResult object from its individual parts: a // Result, a string for the server, and an error func NewServerResult(result Result, server string, err error) *ServerResult { return &ServerResult{ Result: result, Server: server, Error: err, } } // CertRevocationResult encapsulates the result for a single certificate in the // chain as well as the results from individual servers associated with this // certificate type CertRevocationResult struct { // Result of revocation for a specific cert in the chain // // If there are multiple ServerResults, this is because no responses were // able to be retrieved, leaving each ServerResult with a Result of Unknown. // Thus, in the case of more than one ServerResult, this will be ResultUnknown Result Result // An array of results for each server associated with the certificate. // The length will be either 1 or the number of OCSPServers for the cert. // // If the length is 1, then a valid status was able to be retrieved. Only // this server result is contained. Any errors for other servers are // discarded in favor of this valid response. // // Otherwise, every server specified had some error that prevented the // status from being retrieved. These are all contained here for evaluation ServerResults []*ServerResult } notation-core-go-1.1.0/revocation/result/results_test.go000066400000000000000000000040441466321423000234560ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package result import ( "errors" "testing" ) func TestResultString(t *testing.T) { t.Run("ok", func(t *testing.T) { if ResultOK.String() != "OK" { t.Errorf("Expected %s but got %s", "OK", ResultOK.String()) } }) t.Run("non-revokable", func(t *testing.T) { if ResultNonRevokable.String() != "NonRevokable" { t.Errorf("Expected %s but got %s", "NonRevokable", ResultNonRevokable.String()) } }) t.Run("unknown", func(t *testing.T) { if ResultUnknown.String() != "Unknown" { t.Errorf("Expected %s but got %s", "Unknown", ResultUnknown.String()) } }) t.Run("revoked", func(t *testing.T) { if ResultRevoked.String() != "Revoked" { t.Errorf("Expected %s but got %s", "Revoked", ResultRevoked.String()) } }) t.Run("invalid result", func(t *testing.T) { if Result(4).String() != "invalid result with value 4" { t.Errorf("Expected %s but got %s", "invalid result with value 4", Result(4).String()) } }) } func TestNewServerResult(t *testing.T) { expectedR := &ServerResult{ Result: ResultNonRevokable, Server: "test server", Error: errors.New("test error"), } r := NewServerResult(expectedR.Result, expectedR.Server, expectedR.Error) if r.Result != expectedR.Result { t.Errorf("Expected %s but got %s", expectedR.Result, r.Result) } if r.Server != expectedR.Server { t.Errorf("Expected %s but got %s", expectedR.Server, r.Server) } if r.Error != expectedR.Error { t.Errorf("Expected %v but got %v", expectedR.Error, r.Error) } } notation-core-go-1.1.0/revocation/revocation.go000066400000000000000000000131231466321423000215470ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package Revocation provides methods for checking the revocation status of a // certificate chain package revocation import ( "context" "crypto/x509" "errors" "fmt" "net/http" "time" "github.com/notaryproject/notation-core-go/revocation/ocsp" "github.com/notaryproject/notation-core-go/revocation/purpose" "github.com/notaryproject/notation-core-go/revocation/result" ) // Revocation is an interface that specifies methods used for revocation checking. // // Deprecated: Revocation exists for backwards compatibility and should not be used. // To perform revocation check, use [Validator]. type Revocation interface { // Validate checks the revocation status for a certificate chain using OCSP // and returns an array of CertRevocationResults that contain the results // and any errors that are encountered during the process Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) } // ValidateContextOptions provides configuration options for revocation checks type ValidateContextOptions struct { // CertChain denotes the certificate chain whose revocation status is // been validated. REQUIRED. CertChain []*x509.Certificate // AuthenticSigningTime denotes the authentic signing time of the signature. // It is used to compare with the InvalidityDate during revocation check. // OPTIONAL. // // Reference: https://github.com/notaryproject/specifications/blob/v1.0.0/specs/trust-store-trust-policy.md#revocation-checking-with-ocsp AuthenticSigningTime time.Time } // Validator is an interface that provides revocation checking with // context type Validator interface { // ValidateContext checks the revocation status given caller provided options // and returns an array of CertRevocationResults that contain the results // and any errors that are encountered during the process ValidateContext(ctx context.Context, validateContextOpts ValidateContextOptions) ([]*result.CertRevocationResult, error) } // revocation is an internal struct used for revocation checking type revocation struct { httpClient *http.Client certChainPurpose purpose.Purpose } // New constructs a revocation object for code signing certificate chain. // // Deprecated: New exists for backwards compatibility and should not be used. // To create a revocation object, use [NewWithOptions]. func New(httpClient *http.Client) (Revocation, error) { if httpClient == nil { return nil, errors.New("invalid input: a non-nil httpClient must be specified") } return &revocation{ httpClient: httpClient, certChainPurpose: purpose.CodeSigning, }, nil } // Options specifies values that are needed to check revocation type Options struct { // OCSPHTTPClient is the HTTP client for OCSP request. If not provided, // a default *http.Client with timeout of 2 seconds will be used. // OPTIONAL. OCSPHTTPClient *http.Client // CertChainPurpose is the purpose of the certificate chain. Supported // values are CodeSigning and Timestamping. Default value is CodeSigning. // OPTIONAL. CertChainPurpose purpose.Purpose } // NewWithOptions constructs a Validator with the specified options func NewWithOptions(opts Options) (Validator, error) { if opts.OCSPHTTPClient == nil { opts.OCSPHTTPClient = &http.Client{Timeout: 2 * time.Second} } switch opts.CertChainPurpose { case purpose.CodeSigning, purpose.Timestamping: default: return nil, fmt.Errorf("unsupported certificate chain purpose %v", opts.CertChainPurpose) } return &revocation{ httpClient: opts.OCSPHTTPClient, certChainPurpose: opts.CertChainPurpose, }, nil } // Validate checks the revocation status for a certificate chain using OCSP and // returns an array of CertRevocationResults that contain the results and any // errors that are encountered during the process // // TODO: add CRL support // https://github.com/notaryproject/notation-core-go/issues/125 func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) { return r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: certChain, AuthenticSigningTime: signingTime, }) } // ValidateContext checks the revocation status for a certificate chain using // OCSP and returns an array of CertRevocationResults that contain the results // and any errors that are encountered during the process // // TODO: add CRL support // https://github.com/notaryproject/notation-core-go/issues/125 func (r *revocation) ValidateContext(ctx context.Context, validateContextOpts ValidateContextOptions) ([]*result.CertRevocationResult, error) { if len(validateContextOpts.CertChain) == 0 { return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} } return ocsp.CheckStatus(ocsp.Options{ CertChain: validateContextOpts.CertChain, CertChainPurpose: r.certChainPurpose, SigningTime: validateContextOpts.AuthenticSigningTime, HTTPClient: r.httpClient, }) // TODO: add CRL support // https://github.com/notaryproject/notation-core-go/issues/125 } notation-core-go-1.1.0/revocation/revocation_test.go000066400000000000000000001212731466321423000226140ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package revocation import ( "context" "crypto/x509" "errors" "fmt" "net/http" "testing" "time" revocationocsp "github.com/notaryproject/notation-core-go/revocation/ocsp" "github.com/notaryproject/notation-core-go/revocation/purpose" "github.com/notaryproject/notation-core-go/revocation/result" "github.com/notaryproject/notation-core-go/testhelper" "golang.org/x/crypto/ocsp" ) func validateEquivalentCertResults(certResults, expectedCertResults []*result.CertRevocationResult, t *testing.T) { if len(certResults) != len(expectedCertResults) { t.Errorf("Length of certResults (%d) did not match expected length (%d)", len(certResults), len(expectedCertResults)) return } for i, certResult := range certResults { if certResult.Result != expectedCertResults[i].Result { t.Errorf("Expected certResults[%d].Result to be %s, but got %s", i, expectedCertResults[i].Result, certResult.Result) } if len(certResult.ServerResults) != len(expectedCertResults[i].ServerResults) { t.Errorf("Length of certResults[%d].ServerResults (%d) did not match expected length (%d)", i, len(certResult.ServerResults), len(expectedCertResults[i].ServerResults)) return } for j, serverResult := range certResult.ServerResults { if serverResult.Result != expectedCertResults[i].ServerResults[j].Result { t.Errorf("Expected certResults[%d].ServerResults[%d].Result to be %s, but got %s", i, j, expectedCertResults[i].ServerResults[j].Result, serverResult.Result) } if serverResult.Server != expectedCertResults[i].ServerResults[j].Server { t.Errorf("Expected certResults[%d].ServerResults[%d].Server to be %s, but got %s", i, j, expectedCertResults[i].ServerResults[j].Server, serverResult.Server) } if serverResult.Error == nil { if expectedCertResults[i].ServerResults[j].Error == nil { continue } t.Errorf("certResults[%d].ServerResults[%d].Error was nil, but expected %v", i, j, expectedCertResults[i].ServerResults[j].Error) } else if expectedCertResults[i].ServerResults[j].Error == nil { t.Errorf("Unexpected error for certResults[%d].ServerResults[%d].Error: %v", i, j, serverResult.Error) } else if serverResult.Error.Error() != expectedCertResults[i].ServerResults[j].Error.Error() { t.Errorf("Expected certResults[%d].ServerResults[%d].Error to be %v, but got %v", i, j, expectedCertResults[i].ServerResults[j].Error, serverResult.Error) } } } } func getOKCertResult(server string) *result.CertRevocationResult { return &result.CertRevocationResult{ Result: result.ResultOK, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultOK, server, nil), }, } } func getRootCertResult() *result.CertRevocationResult { return &result.CertRevocationResult{ Result: result.ResultNonRevokable, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultNonRevokable, "", nil), }, } } func TestNew(t *testing.T) { r, err := New(nil) expectedError := errors.New("invalid input: a non-nil httpClient must be specified") if r != nil && err.Error() != expectedError.Error() { t.Errorf("Expected New(nil) to fail with %v and %v, but received %v and %v", nil, expectedError, r, err) } client := http.DefaultClient r, err = New(client) if err != nil { t.Errorf("Expected to succeed with default client, but received error %v", err) } revR, ok := r.(*revocation) if !ok { t.Error("Expected New to create an object matching the internal revocation struct") } else if revR.httpClient != client { t.Errorf("Expected New to set client to %v, but it was set to %v", client, revR.httpClient) } } func TestNewWithOptions(t *testing.T) { t.Run("nil OCSP HTTP Client", func(t *testing.T) { _, err := NewWithOptions(Options{ CertChainPurpose: purpose.CodeSigning, }) if err != nil { t.Fatal(err) } }) t.Run("invalid CertChainPurpose", func(t *testing.T) { _, err := NewWithOptions(Options{ OCSPHTTPClient: &http.Client{}, CertChainPurpose: -1, }) expectedErrMsg := "unsupported certificate chain purpose -1" if err == nil || err.Error() != expectedErrMsg { t.Fatalf("expected %s, but got %s", expectedErrMsg, err.Error()) } }) } func TestCheckRevocationStatusForSingleCert(t *testing.T) { revokableCertTuple := testhelper.GetRevokableRSALeafCertificate() revokableIssuerTuple := testhelper.GetRSARootCertificate() revokableChain := []*x509.Certificate{revokableCertTuple.Cert, revokableIssuerTuple.Cert} testChain := []testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple} t.Run("check non-revoked cert", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{getOKCertResult(revokableChain[0].OCSPServer[0]), getRootCertResult()} validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check cert with Unknown OCSP response", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Unknown}, nil, true) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[0].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, }, getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP revoked cert", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Revoked}, nil, true) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[0].OCSPServer[0], revocationocsp.RevokedError{}), }, }, getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP future revoked cert", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Revoked}, &revokedTime, true) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) } func TestCheckRevocationStatusForSelfSignedCert(t *testing.T) { selfSignedTuple := testhelper.GetRSASelfSignedSigningCertTuple("Notation revocation test self-signed cert") client := testhelper.MockClient([]testhelper.RSACertTuple{selfSignedTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate([]*x509.Certificate{selfSignedTuple.Cert}, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{getRootCertResult()} validateEquivalentCertResults(certResults, expectedCertResults, t) } func TestCheckRevocationStatusForRootCert(t *testing.T) { rootTuple := testhelper.GetRSARootCertificate() client := testhelper.MockClient([]testhelper.RSACertTuple{rootTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate([]*x509.Certificate{rootTuple.Cert}, time.Now()) expectedErr := result.InvalidChainError{Err: errors.New("invalid self-signed certificate. Error: certificate with subject \"CN=Notation Test RSA Root,O=Notary,L=Seattle,ST=WA,C=US\": if the basic constraints extension is present, the ca field must be set to false")} if err == nil || err.Error() != expectedErr.Error() { t.Errorf("Expected Validate to fail with %v, but got: %v", expectedErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } } func TestCheckRevocationStatusForChain(t *testing.T) { zeroTime := time.Time{} testChain := testhelper.GetRevokableRSAChain(6) revokableChain := make([]*x509.Certificate, 6) for i, tuple := range testChain { revokableChain[i] = tuple.Cert revokableChain[i].NotBefore = zeroTime } t.Run("empty chain", func(t *testing.T) { r, err := New(&http.Client{Timeout: 5 * time.Second}) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate([]*x509.Certificate{}, time.Now()) expectedErr := result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} if err == nil || err.Error() != expectedErr.Error() { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", expectedErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("check non-revoked chain", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), getOKCertResult(revokableChain[2].OCSPServer[0]), getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check chain with 1 Unknown cert", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good}, nil, true) // 3rd cert will be unknown, the rest will be good r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 revoked cert", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) // 3rd cert will be revoked, the rest will be good r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 unknown and 1 revoked cert", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) // 3rd cert will be unknown, 5th will be revoked, the rest will be good r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], revocationocsp.RevokedError{}), }, }, getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 future revoked cert", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) // 3rd cert will be future revoked, the rest will be good r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), getOKCertResult(revokableChain[2].OCSPServer[0]), getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 unknown and 1 future revoked cert", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) // 3rd cert will be unknown, 5th will be future revoked, the rest will be good r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 revoked cert before signing time", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) // 3rd cert will be revoked, the rest will be good r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now().Add(time.Hour)) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 revoked cert after zero signing time", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) // 3rd cert will be revoked, the rest will be good if !zeroTime.IsZero() { t.Errorf("exected zeroTime.IsZero() to be true") } r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(revokableChain, time.Now().Add(time.Hour)) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) } func TestCheckRevocationStatusForTimestampChain(t *testing.T) { zeroTime := time.Time{} testChain := testhelper.GetRevokableRSATimestampChain(6) revokableChain := make([]*x509.Certificate, 6) for i, tuple := range testChain { revokableChain[i] = tuple.Cert revokableChain[i].NotBefore = zeroTime } t.Run("empty chain", func(t *testing.T) { r, err := NewWithOptions(Options{ OCSPHTTPClient: &http.Client{Timeout: 5 * time.Second}, CertChainPurpose: purpose.Timestamping, }) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: []*x509.Certificate{}, AuthenticSigningTime: time.Now(), }) expectedErr := result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} if err == nil || err.Error() != expectedErr.Error() { t.Errorf("Expected CheckStatus to fail with %v, but got: %v", expectedErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("check non-revoked chain", func(t *testing.T) { client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) r, err := NewWithOptions(Options{ OCSPHTTPClient: client, CertChainPurpose: purpose.Timestamping, }) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: revokableChain, AuthenticSigningTime: time.Now(), }) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), getOKCertResult(revokableChain[2].OCSPServer[0]), getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check chain with 1 Unknown cert", func(t *testing.T) { // 3rd cert will be unknown, the rest will be good client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good}, nil, true) r, err := NewWithOptions(Options{ OCSPHTTPClient: client, CertChainPurpose: purpose.Timestamping, }) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: revokableChain, AuthenticSigningTime: time.Now(), }) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 revoked cert", func(t *testing.T) { // 3rd cert will be revoked, the rest will be good client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) r, err := NewWithOptions(Options{ OCSPHTTPClient: client, CertChainPurpose: purpose.Timestamping, }) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: revokableChain, AuthenticSigningTime: time.Now(), }) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 unknown and 1 revoked cert", func(t *testing.T) { // 3rd cert will be unknown, 5th will be revoked, the rest will be good client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) r, err := NewWithOptions(Options{ OCSPHTTPClient: client, CertChainPurpose: purpose.Timestamping, }) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: revokableChain, AuthenticSigningTime: time.Now(), }) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], revocationocsp.RevokedError{}), }, }, getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 future revoked cert", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) // 3rd cert will be future revoked, the rest will be good client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) r, err := NewWithOptions(Options{ OCSPHTTPClient: client, CertChainPurpose: purpose.Timestamping, }) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: revokableChain, AuthenticSigningTime: time.Now(), }) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), getOKCertResult(revokableChain[2].OCSPServer[0]), getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 unknown and 1 future revoked cert", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) // 3rd cert will be unknown, 5th will be future revoked, the rest will be good client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Unknown, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) r, err := NewWithOptions(Options{ OCSPHTTPClient: client, CertChainPurpose: purpose.Timestamping, }) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: revokableChain, AuthenticSigningTime: time.Now(), }) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 revoked cert before signing time", func(t *testing.T) { // 3rd cert will be revoked, the rest will be good client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, nil, true) r, err := NewWithOptions(Options{ OCSPHTTPClient: client, CertChainPurpose: purpose.Timestamping, }) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: revokableChain, AuthenticSigningTime: time.Now().Add(time.Hour), }) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("check OCSP with 1 revoked cert after zero signing time", func(t *testing.T) { revokedTime := time.Now().Add(time.Hour) // 3rd cert will be revoked, the rest will be good client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good, ocsp.Good, ocsp.Revoked, ocsp.Good}, &revokedTime, true) if !zeroTime.IsZero() { t.Errorf("exected zeroTime.IsZero() to be true") } r, err := NewWithOptions(Options{ OCSPHTTPClient: client, CertChainPurpose: purpose.Timestamping, }) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.ValidateContext(context.Background(), ValidateContextOptions{ CertChain: revokableChain, AuthenticSigningTime: time.Now().Add(time.Hour), }) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(revokableChain[0].OCSPServer[0]), getOKCertResult(revokableChain[1].OCSPServer[0]), { Result: result.ResultRevoked, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}), }, }, getOKCertResult(revokableChain[3].OCSPServer[0]), getOKCertResult(revokableChain[4].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) } func TestCheckRevocationErrors(t *testing.T) { leafCertTuple := testhelper.GetRSALeafCertificate() rootCertTuple := testhelper.GetRSARootCertificate() noOCSPChain := []*x509.Certificate{leafCertTuple.Cert, rootCertTuple.Cert} revokableTuples := testhelper.GetRevokableRSAChain(3) noRootChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert} backwardsChain := []*x509.Certificate{revokableTuples[2].Cert, revokableTuples[1].Cert, revokableTuples[0].Cert} okChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[1].Cert, revokableTuples[2].Cert} expiredLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw) expiredLeaf.IsCA = false expiredLeaf.KeyUsage = x509.KeyUsageDigitalSignature expiredLeaf.OCSPServer = []string{"http://example.com/expired_ocsp"} expiredChain := []*x509.Certificate{expiredLeaf, revokableTuples[1].Cert, revokableTuples[2].Cert} noHTTPLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw) noHTTPLeaf.IsCA = false noHTTPLeaf.KeyUsage = x509.KeyUsageDigitalSignature noHTTPLeaf.OCSPServer = []string{"ldap://ds.example.com:123/chain_ocsp/0"} noHTTPChain := []*x509.Certificate{noHTTPLeaf, revokableTuples[1].Cert, revokableTuples[2].Cert} backwardsChainErr := result.InvalidChainError{Err: errors.New("leaf certificate with subject \"CN=Notation Test Revokable RSA Chain Cert Root,O=Notary,L=Seattle,ST=WA,C=US\" is self-signed. Certificate chain must not contain self-signed leaf certificate")} chainRootErr := result.InvalidChainError{Err: errors.New("root certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US\" is not self-signed. Certificate chain must end with a valid self-signed root certificate")} expiredRespErr := revocationocsp.GenericError{Err: errors.New("expired OCSP response")} noHTTPErr := revocationocsp.GenericError{Err: errors.New("OCSPServer protocol ldap is not supported")} r, err := New(&http.Client{Timeout: 5 * time.Second}) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } t.Run("no OCSPServer specified", func(t *testing.T) { certResults, err := r.Validate(noOCSPChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ { Result: result.ResultNonRevokable, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultNonRevokable, "", nil), }, }, getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("chain missing root", func(t *testing.T) { certResults, err := r.Validate(noRootChain, time.Now()) if err == nil || err.Error() != chainRootErr.Error() { t.Errorf("Expected Validate to fail with %v, but got: %v", chainRootErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("backwards chain", func(t *testing.T) { certResults, err := r.Validate(backwardsChain, time.Now()) if err == nil || err.Error() != backwardsChainErr.Error() { t.Errorf("Expected Validate to fail with %v, but got: %v", backwardsChainErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("timeout", func(t *testing.T) { timeoutClient := &http.Client{Timeout: 1 * time.Nanosecond} timeoutR, err := New(timeoutClient) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := timeoutR.Validate(okChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, okChain[0].OCSPServer[0], revocationocsp.TimeoutError{}), }, }, { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, okChain[1].OCSPServer[0], revocationocsp.TimeoutError{}), }, }, getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("expired ocsp response", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) expiredR, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := expiredR.Validate(expiredChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, expiredChain[0].OCSPServer[0], expiredRespErr), }, }, getOKCertResult(expiredChain[1].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("OCSP pkixNoCheck missing", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, false) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(okChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ getOKCertResult(okChain[0].OCSPServer[0]), getOKCertResult(okChain[1].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) t.Run("non-HTTP URI error", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(noHTTPChain, time.Now()) if err != nil { t.Errorf("Expected CheckStatus to succeed, but got error: %v", err) } expectedCertResults := []*result.CertRevocationResult{ { Result: result.ResultUnknown, ServerResults: []*result.ServerResult{ result.NewServerResult(result.ResultUnknown, noHTTPChain[0].OCSPServer[0], noHTTPErr), }, }, getOKCertResult(noHTTPChain[1].OCSPServer[0]), getRootCertResult(), } validateEquivalentCertResults(certResults, expectedCertResults, t) }) } func TestCheckRevocationInvalidChain(t *testing.T) { revokableTuples := testhelper.GetRevokableRSAChain(4) misorderedIntermediateTuples := []testhelper.RSACertTuple{revokableTuples[1], revokableTuples[0], revokableTuples[2], revokableTuples[3]} misorderedIntermediateChain := []*x509.Certificate{revokableTuples[1].Cert, revokableTuples[0].Cert, revokableTuples[2].Cert, revokableTuples[3].Cert} for i, cert := range misorderedIntermediateChain { if i != (len(misorderedIntermediateChain) - 1) { // Skip root which won't have an OCSP Server cert.OCSPServer[0] = fmt.Sprintf("http://example.com/chain_ocsp/%d", i) } } missingIntermediateChain := []*x509.Certificate{revokableTuples[0].Cert, revokableTuples[2].Cert, revokableTuples[3].Cert} for i, cert := range missingIntermediateChain { if i != (len(missingIntermediateChain) - 1) { // Skip root which won't have an OCSP Server cert.OCSPServer[0] = fmt.Sprintf("http://example.com/chain_ocsp/%d", i) } } missingIntermediateErr := result.InvalidChainError{Err: errors.New("certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 4,O=Notary,L=Seattle,ST=WA,C=US\" is not issued by \"CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US\"")} misorderedChainErr := result.InvalidChainError{Err: errors.New("invalid certificates or certificate with subject \"CN=Notation Test Revokable RSA Chain Cert 3,O=Notary,L=Seattle,ST=WA,C=US\" is not issued by \"CN=Notation Test Revokable RSA Chain Cert 4,O=Notary,L=Seattle,ST=WA,C=US\". Error: x509: invalid signature: parent certificate cannot sign this kind of certificate")} t.Run("chain missing intermediate", func(t *testing.T) { client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(missingIntermediateChain, time.Now()) if err == nil || err.Error() != missingIntermediateErr.Error() { t.Errorf("Expected Validate to fail with %v, but got: %v", missingIntermediateErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) t.Run("chain out of order", func(t *testing.T) { client := testhelper.MockClient(misorderedIntermediateTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true) r, err := New(client) if err != nil { t.Errorf("Expected successful creation of revocation, but received error: %v", err) } certResults, err := r.Validate(misorderedIntermediateChain, time.Now()) if err == nil || err.Error() != misorderedChainErr.Error() { t.Errorf("Expected Validate to fail with %v, but got: %v", misorderedChainErr, err) } if certResults != nil { t.Error("Expected certResults to be nil when there is an error") } }) } func TestValidateContext(t *testing.T) { r, err := NewWithOptions(Options{ OCSPHTTPClient: &http.Client{}, }) if err != nil { t.Fatal(err) } expectedErrMsg := "invalid chain: expected chain to be correct and complete: chain does not contain any certificates" _, err = r.ValidateContext(context.Background(), ValidateContextOptions{}) if err == nil || err.Error() != expectedErrMsg { t.Fatalf("expected %s, but got %s", expectedErrMsg, err) } } notation-core-go-1.1.0/signature/000077500000000000000000000000001466321423000166775ustar00rootroot00000000000000notation-core-go-1.1.0/signature/algorithm.go000066400000000000000000000064211466321423000212170ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signature import ( "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" "fmt" ) // Algorithm defines the signature algorithm. type Algorithm int // Signature algorithms supported by this library. // // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection const ( AlgorithmPS256 Algorithm = 1 + iota // RSASSA-PSS with SHA-256 AlgorithmPS384 // RSASSA-PSS with SHA-384 AlgorithmPS512 // RSASSA-PSS with SHA-512 AlgorithmES256 // ECDSA on secp256r1 with SHA-256 AlgorithmES384 // ECDSA on secp384r1 with SHA-384 AlgorithmES512 // ECDSA on secp521r1 with SHA-512 ) // KeyType defines the key type. type KeyType int const ( KeyTypeRSA KeyType = 1 + iota // KeyType RSA KeyTypeEC // KeyType EC ) // KeySpec defines a key type and size. type KeySpec struct { // KeyType is the type of the key. Type KeyType // KeySize is the size of the key in bits. Size int } // Hash returns the hash function of the algorithm. func (alg Algorithm) Hash() crypto.Hash { switch alg { case AlgorithmPS256, AlgorithmES256: return crypto.SHA256 case AlgorithmPS384, AlgorithmES384: return crypto.SHA384 case AlgorithmPS512, AlgorithmES512: return crypto.SHA512 } return 0 } // ExtractKeySpec extracts KeySpec from the signing certificate. func ExtractKeySpec(signingCert *x509.Certificate) (KeySpec, error) { switch key := signingCert.PublicKey.(type) { case *rsa.PublicKey: switch bitSize := key.Size() << 3; bitSize { case 2048, 3072, 4096: return KeySpec{ Type: KeyTypeRSA, Size: bitSize, }, nil default: return KeySpec{}, &UnsupportedSigningKeyError{ Msg: fmt.Sprintf("rsa key size %d bits is not supported", bitSize), } } case *ecdsa.PublicKey: switch bitSize := key.Curve.Params().BitSize; bitSize { case 256, 384, 521: return KeySpec{ Type: KeyTypeEC, Size: bitSize, }, nil default: return KeySpec{}, &UnsupportedSigningKeyError{ Msg: fmt.Sprintf("ecdsa key size %d bits is not supported", bitSize), } } } return KeySpec{}, &UnsupportedSigningKeyError{ Msg: "unsupported public key type", } } // SignatureAlgorithm returns the signing algorithm associated with the KeySpec. func (k KeySpec) SignatureAlgorithm() Algorithm { switch k.Type { case KeyTypeEC: switch k.Size { case 256: return AlgorithmES256 case 384: return AlgorithmES384 case 521: return AlgorithmES512 } case KeyTypeRSA: switch k.Size { case 2048: return AlgorithmPS256 case 3072: return AlgorithmPS384 case 4096: return AlgorithmPS512 } } return 0 } notation-core-go-1.1.0/signature/algorithm_test.go000066400000000000000000000114111466321423000222510ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signature import ( "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "reflect" "strconv" "testing" "github.com/notaryproject/notation-core-go/testhelper" ) func TestHash(t *testing.T) { tests := []struct { name string alg Algorithm expect crypto.Hash }{ { name: "PS256", alg: AlgorithmPS256, expect: crypto.SHA256, }, { name: "ES256", alg: AlgorithmES256, expect: crypto.SHA256, }, { name: "PS384", alg: AlgorithmPS384, expect: crypto.SHA384, }, { name: "ES384", alg: AlgorithmES384, expect: crypto.SHA384, }, { name: "PS512", alg: AlgorithmPS512, expect: crypto.SHA512, }, { name: "ES512", alg: AlgorithmES512, expect: crypto.SHA512, }, { name: "UnsupportedAlgorithm", alg: 0, expect: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hash := tt.alg.Hash() if hash != tt.expect { t.Fatalf("Expected %v, got %v", tt.expect, hash) } }) } } func TestExtractKeySpec(t *testing.T) { type testCase struct { name string cert *x509.Certificate expect KeySpec expectErr bool } // invalid cases tests := []testCase{ { name: "RSA wrong size", cert: testhelper.GetUnsupportedRSACert().Cert, expect: KeySpec{}, expectErr: true, }, { name: "ECDSA wrong size", cert: testhelper.GetUnsupportedECCert().Cert, expect: KeySpec{}, expectErr: true, }, { name: "Unsupported type", cert: &x509.Certificate{ PublicKey: ed25519.PublicKey{}, }, expect: KeySpec{}, expectErr: true, }, } // append valid RSA cases for _, k := range []int{2048, 3072, 4096} { rsaRoot := testhelper.GetRSARootCertificate() priv, _ := rsa.GenerateKey(rand.Reader, k) certTuple := testhelper.GetRSACertTupleWithPK( priv, "Test RSA_"+strconv.Itoa(priv.Size()), &rsaRoot, ) tests = append(tests, testCase{ name: "RSA " + strconv.Itoa(k), cert: certTuple.Cert, expect: KeySpec{ Type: KeyTypeRSA, Size: k, }, expectErr: false, }) } // append valid EDCSA cases for _, curve := range []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} { ecdsaRoot := testhelper.GetECRootCertificate() priv, _ := ecdsa.GenerateKey(curve, rand.Reader) bitSize := priv.Params().BitSize certTuple := testhelper.GetECDSACertTupleWithPK( priv, "Test EC_"+strconv.Itoa(bitSize), &ecdsaRoot, ) tests = append(tests, testCase{ name: "EC " + strconv.Itoa(bitSize), cert: certTuple.Cert, expect: KeySpec{ Type: KeyTypeEC, Size: bitSize, }, expectErr: false, }) } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { keySpec, err := ExtractKeySpec(tt.cert) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } if !reflect.DeepEqual(keySpec, tt.expect) { t.Errorf("expect %+v, got %+v", tt.expect, keySpec) } }) } } func TestSignatureAlgorithm(t *testing.T) { tests := []struct { name string keySpec KeySpec expect Algorithm }{ { name: "EC 256", keySpec: KeySpec{ Type: KeyTypeEC, Size: 256, }, expect: AlgorithmES256, }, { name: "EC 384", keySpec: KeySpec{ Type: KeyTypeEC, Size: 384, }, expect: AlgorithmES384, }, { name: "EC 521", keySpec: KeySpec{ Type: KeyTypeEC, Size: 521, }, expect: AlgorithmES512, }, { name: "RSA 2048", keySpec: KeySpec{ Type: KeyTypeRSA, Size: 2048, }, expect: AlgorithmPS256, }, { name: "RSA 3072", keySpec: KeySpec{ Type: KeyTypeRSA, Size: 3072, }, expect: AlgorithmPS384, }, { name: "RSA 4096", keySpec: KeySpec{ Type: KeyTypeRSA, Size: 4096, }, expect: AlgorithmPS512, }, { name: "Unsupported key spec", keySpec: KeySpec{ Type: 0, Size: 0, }, expect: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { alg := tt.keySpec.SignatureAlgorithm() if alg != tt.expect { t.Errorf("unexpected signature algorithm: %v, expect: %v", alg, tt.expect) } }) } } notation-core-go-1.1.0/signature/cose/000077500000000000000000000000001466321423000176305ustar00rootroot00000000000000notation-core-go-1.1.0/signature/cose/conformance_test.go000066400000000000000000000177361466321423000235260ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cose import ( "bytes" "crypto/x509" "encoding/hex" "encoding/json" "os" "reflect" "sort" "testing" "time" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/testhelper" "github.com/veraison/go-cose" ) type sign1 struct { Payload string `json:"payload"` ProtectedHeaders *cborStruct `json:"protectedHeaders"` UnprotectedHeaders *cborStruct `json:"unprotectedHeaders"` Output cborStruct `json:"expectedOutput"` } type cborStruct struct { CBORHex string `json:"cborHex"` CBORDiag string `json:"cborDiag"` } func TestConformance(t *testing.T) { data, err := os.ReadFile("testdata/conformance.json") if err != nil { t.Fatalf("os.ReadFile() failed. Error = %s", err) } var sign1 sign1 err = json.Unmarshal(data, &sign1) if err != nil { t.Fatalf("json.Unmarshal() failed. Error = %s", err) } testSign(t, &sign1) testVerify(t, &sign1) } // testSign does conformance check on COSE_Sign1_Tagged func testSign(t *testing.T, sign1 *sign1) { signRequest, err := getSignReq() if err != nil { t.Fatalf("getSignReq() failed. Error = %s", err) } env := createNewEnv(nil) encoded, err := env.Sign(signRequest) if err != nil || len(encoded) == 0 { t.Fatalf("Sign() faild. Error = %s", err) } newMsg := generateSign1(env.base) got, err := newMsg.MarshalCBOR() if err != nil { t.Fatalf("MarshalCBOR() faild. Error = %s", err) } // sign1.Output.CBORHex is a manually computed CBOR hex used as ground // truth in the conformance test. want := hexToBytes(sign1.Output.CBORHex) if !bytes.Equal(want, got) { t.Fatalf("unexpected output:\nwant: %x\n got: %x", want, got) } // Verify using the same envelope struct // (Verify with UnmarshalCBOR is covered in the testVerify() part) _, err = env.Verify() if err != nil { t.Fatalf("Verify() failed. Error = %s", err) } } // testVerify does conformance check by decoding COSE_Sign1_Tagged object // into Sign1Message func testVerify(t *testing.T, sign1 *sign1) { signRequest, err := getSignReq() if err != nil { t.Fatalf("getSignReq() failed. Error = %s", err) } env := createNewEnv(nil) encoded, err := env.Sign(signRequest) if err != nil || len(encoded) == 0 { t.Fatalf("Sign() faild. Error = %s", err) } //Verify after UnmarshalCBOR var msg cose.Sign1Message // sign1.Output.CBORHex is a manually computed CBOR hex used as ground // truth in the conformance test. if err := msg.UnmarshalCBOR(hexToBytes(sign1.Output.CBORHex)); err != nil { t.Fatalf("msg.UnmarshalCBOR() failed. Error = %s", err) } certs := []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert} certChain := make([]any, len(certs)) for i, c := range certs { certChain[i] = c.Raw } msg.Headers.Unprotected[cose.HeaderLabelX5Chain] = certChain msg.Signature = env.base.Signature newEnv := createNewEnv(&msg) content, err := newEnv.Verify() if err != nil { t.Fatalf("Verify() failed. Error = %s", err) } verifyPayload(&content.Payload, signRequest, t) verifySignerInfo(&content.SignerInfo, signRequest, t) } func getSignReq() (*signature.SignRequest, error) { certs := []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert} signer, err := signature.NewLocalSigner(certs, testhelper.GetRSALeafCertificate().PrivateKey) if err != nil { return &signature.SignRequest{}, err } signRequest := &signature.SignRequest{ Payload: signature.Payload{ ContentType: "application/vnd.cncf.notary.payload.v1+json", Content: []byte("hello COSE"), }, Signer: signer, SigningTime: time.Unix(1661321924, 0), Expiry: time.Unix(1661408324, 0), ExtendedSignedAttributes: []signature.Attribute{ {Key: "signedCritKey1", Value: "signedCritValue1", Critical: true}, {Key: "signedKey1", Value: "signedValue1", Critical: false}, }, SigningAgent: "NotationConformanceTest/1.0.0", SigningScheme: "notary.x509", } return signRequest, nil } func hexToBytes(s string) []byte { b, err := hex.DecodeString(s) if err != nil { panic(err) } return b } func verifySignerInfo(signInfo *signature.SignerInfo, request *signature.SignRequest, t *testing.T) { if request.SigningAgent != signInfo.UnsignedAttributes.SigningAgent { t.Fatalf("SigningAgent: expected value %q but found %q", request.SigningAgent, signInfo.UnsignedAttributes.SigningAgent) } if request.SigningTime.Format(time.RFC3339) != signInfo.SignedAttributes.SigningTime.Format(time.RFC3339) { t.Fatalf("SigningTime: expected value %q but found %q", request.SigningTime, signInfo.SignedAttributes.SigningTime) } if request.Expiry.Format(time.RFC3339) != signInfo.SignedAttributes.Expiry.Format(time.RFC3339) { t.Fatalf("Expiry: expected value %q but found %q", request.SigningTime, signInfo.SignedAttributes.Expiry) } if !areAttrEqual(request.ExtendedSignedAttributes, signInfo.SignedAttributes.ExtendedAttributes) { if !(len(request.ExtendedSignedAttributes) == 0 && len(signInfo.SignedAttributes.ExtendedAttributes) == 0) { t.Fatalf("Mistmatch between expected and actual ExtendedAttributes") } } signer, err := getSigner(request.Signer) if err != nil { t.Fatalf("getSigner() failed. Error = %s", err) } certs := signer.CertificateChain() if err != nil || !reflect.DeepEqual(certs, signInfo.CertificateChain) { t.Fatalf("Mistmatch between expected and actual CertificateChain") } } func verifyPayload(payload *signature.Payload, request *signature.SignRequest, t *testing.T) { if request.Payload.ContentType != payload.ContentType { t.Fatalf("PayloadContentType: expected value %q but found %q", request.Payload.ContentType, payload.ContentType) } if !bytes.Equal(request.Payload.Content, payload.Content) { t.Fatalf("Payload: expected value %q but found %q", request.Payload.Content, payload.Content) } } func areAttrEqual(u []signature.Attribute, v []signature.Attribute) bool { sortCOSEAttributes(u) sortCOSEAttributes(v) return reflect.DeepEqual(u, v) } func generateSign1(msg *cose.Sign1Message) *cose.Sign1Message { newMsg := cose.NewSign1Message() newMsg.Headers.Protected = msg.Headers.Protected newMsg.Headers.Unprotected["io.cncf.notary.signingAgent"] = msg.Headers.Unprotected["io.cncf.notary.signingAgent"] newMsg.Payload = msg.Payload newMsg.Signature = hexToBytes("5bfec0a345098b9b9b6fb7358face7ab76d191b648ccd19e36fb03c2085ea072ec050d9c6e4fa4845478386d0831a2360d343a1ff027bdd56d496f996b90ac2db9da2460baffec21db7c0ca759ba83ab35cdf521c0926138681bde05277e2976cedbeb4040c930908ef2b113d935378bd3c5e7740119b2b81c59e9c6c24411abdf699547864f68f2e0f6346eeff627bf0d971abdf94e67e12a10134ccbbadfa0ab4031b18705696a9567a0f1f061247fdd00d343ea3a45f63da7f80771612b38fc9877375bcbce28aef1f3ee2b25869722c24737c49d8c6711376dd62b3d32b24d489746e2ba5d25fa76febcc6abf9d2baee67221c85a7a8f8763dadc5e20bb8c5c03a75c68211557813d2d6adea56ec5526f78c18460b1944c8307a4b0ed64a6d6b4abed5067de5a5ad38948a2ea140b01a7762c15b3e63d7d7bdc8962e6c4bff18b34d2a19fc627f02ebf88daf7fb25c55ce1b9ca06ade02f9d60ad16cb306f433f692e598132d67b5d0a02193191d5c9cd52ad81f4e31917e5b5d40ef5ce7") return newMsg } func sortCOSEAttributes(u []signature.Attribute) { sort.Slice(u, func(p, q int) bool { switch k1 := u[p].Key.(type) { case int: switch k2 := u[q].Key.(type) { case int: return k1 < k2 case string: return false } case string: switch k2 := u[q].Key.(type) { case int: return true case string: return k1 < k2 } } return false }) } notation-core-go-1.1.0/signature/cose/envelope.go000066400000000000000000000553071466321423000220060ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cose import ( "crypto" "crypto/rand" "crypto/x509" "errors" "fmt" "io" "strconv" "time" "github.com/fxamacker/cbor/v2" "github.com/notaryproject/notation-core-go/internal/timestamp" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/base" "github.com/notaryproject/tspclient-go" "github.com/veraison/go-cose" ) // MediaTypeEnvelope is the COSE signature envelope blob mediaType. const MediaTypeEnvelope = "application/cose" var ( // encMode is the encoding mode used in Sign encMode cbor.EncMode // decMode is the decoding mode used in Content decMode cbor.DecMode ) func init() { err := signature.RegisterEnvelopeType(MediaTypeEnvelope, NewEnvelope, ParseEnvelope) if err != nil { panic(err) } encOpts := cbor.EncOptions{ Time: cbor.TimeUnix, TimeTag: cbor.EncTagRequired, } encMode, err = encOpts.EncMode() if err != nil { panic(err) } decOpts := cbor.DecOptions{ TimeTag: cbor.DecTagRequired, } decMode, err = decOpts.DecMode() if err != nil { panic(err) } } // Protected Headers // https://github.com/notaryproject/notaryproject/blob/cose-envelope/signature-envelope-cose.md const ( headerLabelExpiry = "io.cncf.notary.expiry" headerLabelSigningScheme = "io.cncf.notary.signingScheme" headerLabelSigningTime = "io.cncf.notary.signingTime" headerLabelAuthenticSigningTime = "io.cncf.notary.authenticSigningTime" ) // Unprotected Headers // https://github.com/notaryproject/notaryproject/blob/cose-envelope/signature-envelope-cose.md const ( headerLabelTimestampSignature = "io.cncf.notary.timestampSignature" headerLabelSigningAgent = "io.cncf.notary.signingAgent" ) // Map of cose.Algorithm to signature.Algorithm var coseAlgSignatureAlgMap = map[cose.Algorithm]signature.Algorithm{ cose.AlgorithmPS256: signature.AlgorithmPS256, cose.AlgorithmPS384: signature.AlgorithmPS384, cose.AlgorithmPS512: signature.AlgorithmPS512, cose.AlgorithmES256: signature.AlgorithmES256, cose.AlgorithmES384: signature.AlgorithmES384, cose.AlgorithmES512: signature.AlgorithmES512, } // Map of signingScheme to signingTime header label var signingSchemeTimeLabelMap = map[signature.SigningScheme]string{ signature.SigningSchemeX509: headerLabelSigningTime, signature.SigningSchemeX509SigningAuthority: headerLabelAuthenticSigningTime, } // signer interface is a cose.Signer with certificate chain fetcher. type signer interface { cose.Signer CertificateChain() []*x509.Certificate } // remoteSigner implements signer interface. // It is used in Sign process when base's Sign implementation is desired. type remoteSigner struct { base signature.Signer alg cose.Algorithm certs []*x509.Certificate } func newRemoteSigner(base signature.Signer) (*remoteSigner, error) { keySpec, err := base.KeySpec() if err != nil { return nil, err } alg, err := getSignatureAlgorithmFromKeySpec(keySpec) if err != nil { return nil, err } return &remoteSigner{ base: base, alg: alg, }, nil } // Algorithm implements cose.Signer interface. func (signer *remoteSigner) Algorithm() cose.Algorithm { return signer.alg } // Sign implements cose.Signer interface. func (signer *remoteSigner) Sign(rand io.Reader, payload []byte) ([]byte, error) { signature, certs, err := signer.base.Sign(payload) if err != nil { return nil, err } signer.certs = certs return signature, nil } // CertificateChain implements signer interface. func (signer *remoteSigner) CertificateChain() []*x509.Certificate { return signer.certs } type localSigner struct { cose.Signer certs []*x509.Certificate } func newLocalSigner(base signature.LocalSigner) (*localSigner, error) { key := base.PrivateKey() if cryptoSigner, ok := key.(crypto.Signer); ok { certs, err := base.CertificateChain() if err != nil { return nil, err } keySpec, err := base.KeySpec() if err != nil { return nil, err } alg, err := getSignatureAlgorithmFromKeySpec(keySpec) if err != nil { return nil, err } coseSigner, err := cose.NewSigner(alg, cryptoSigner) if err != nil { return nil, err } return &localSigner{ Signer: coseSigner, certs: certs, }, nil } return nil, &signature.UnsupportedSigningKeyError{} } // CertificateChain implements signer interface. func (signer *localSigner) CertificateChain() []*x509.Certificate { return signer.certs } type envelope struct { base *cose.Sign1Message } // NewEnvelope initializes an empty COSE signature envelope. func NewEnvelope() signature.Envelope { return &base.Envelope{ Envelope: &envelope{}, } } // ParseEnvelope parses envelopeBytes to a COSE signature envelope. func ParseEnvelope(envelopeBytes []byte) (signature.Envelope, error) { var msg cose.Sign1Message if err := msg.UnmarshalCBOR(envelopeBytes); err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } return &base.Envelope{ Envelope: &envelope{ base: &msg, }, Raw: envelopeBytes, }, nil } // Sign implements signature.Envelope interface. // On success, this function returns the COSE signature envelope byte slice. func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) { // get built-in signer from go-cose or remote signer based on req.Signer signer, err := getSigner(req.Signer) if err != nil { return nil, &signature.InvalidSignRequestError{Msg: err.Error()} } // prepare COSE_Sign1 message msg := cose.NewSign1Message() // generate protected headers of COSE envelope msg.Headers.Protected.SetAlgorithm(signer.Algorithm()) if err := generateProtectedHeaders(req, msg.Headers.Protected); err != nil { return nil, &signature.InvalidSignRequestError{Msg: err.Error()} } // generate payload of COSE envelope msg.Headers.Protected[cose.HeaderLabelContentType] = req.Payload.ContentType msg.Payload = req.Payload.Content // core sign process, generate signature of COSE envelope if err := msg.Sign(rand.Reader, nil, signer); err != nil { return nil, &signature.InvalidSignRequestError{Msg: err.Error()} } // generate unprotected headers of COSE envelope. generateUnprotectedHeaders(req, signer, msg.Headers.Unprotected) // timestamping if req.SigningScheme == signature.SigningSchemeX509 && req.Timestamper != nil { hash, err := hashFromCOSEAlgorithm(signer.Algorithm()) if err != nil { return nil, &signature.TimestampError{Detail: err} } timestampOpts := tspclient.RequestOptions{ Content: msg.Signature, HashAlgorithm: hash, } timestampToken, err := timestamp.Timestamp(req, timestampOpts) if err != nil { return nil, &signature.TimestampError{Detail: err} } // on success, embed the timestamp token to Unprotected header msg.Headers.Unprotected[headerLabelTimestampSignature] = timestampToken } // encode Sign1Message into COSE_Sign1_Tagged object encoded, err := msg.MarshalCBOR() if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } e.base = msg return encoded, nil } // Verify implements signature.Envelope interface. // Note: Verfiy only verifies integrity of the given COSE envelope. func (e *envelope) Verify() (*signature.EnvelopeContent, error) { // sanity check if e.base == nil { return nil, &signature.SignatureEnvelopeNotFoundError{} } certs, ok := e.base.Headers.Unprotected[cose.HeaderLabelX5Chain].([]any) if !ok || len(certs) == 0 { return nil, &signature.InvalidSignatureError{Msg: "certificate chain is not present"} } certRaw, ok := certs[0].([]byte) if !ok { return nil, &signature.InvalidSignatureError{Msg: "COSE envelope malformed leaf certificate"} } cert, err := x509.ParseCertificate(certRaw) if err != nil { return nil, &signature.InvalidSignatureError{Msg: "malformed leaf certificate"} } // core verify process, verify integrity of COSE envelope publicKeyAlg, err := getSignatureAlgorithm(cert) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } verifier, err := cose.NewVerifier(publicKeyAlg, cert.PublicKey) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } err = e.base.Verify(nil, verifier) if err != nil { return nil, &signature.SignatureIntegrityError{Err: err} } // extract content return e.Content() } // Content implements signature.Envelope interface. func (e *envelope) Content() (*signature.EnvelopeContent, error) { // sanity check if e.base == nil { return nil, &signature.SignatureEnvelopeNotFoundError{} } payload, err := e.payload() if err != nil { return nil, err } signerInfo, err := e.signerInfo() if err != nil { return nil, err } return &signature.EnvelopeContent{ SignerInfo: *signerInfo, Payload: *payload, }, nil } // Given a COSE envelope, extracts its signature.Payload. func (e *envelope) payload() (*signature.Payload, error) { cty, ok := e.base.Headers.Protected[cose.HeaderLabelContentType] if !ok { return nil, &signature.InvalidSignatureError{Msg: "missing content type"} } var contentType string if contentType, ok = cty.(string); !ok { return nil, &signature.InvalidSignatureError{Msg: "content type should be of 'tstr' type"} } return &signature.Payload{ ContentType: contentType, Content: e.base.Payload, }, nil } // Given a COSE envelope, extracts its signature.SignerInfo. func (e *envelope) signerInfo() (*signature.SignerInfo, error) { var signerInfo signature.SignerInfo // parse signature of COSE envelope, populate signerInfo.Signature sig := e.base.Signature if len(sig) == 0 { return nil, &signature.InvalidSignatureError{Msg: "signature missing in COSE envelope"} } signerInfo.Signature = sig // parse protected headers of COSE envelope and populate related // signerInfo fields err := parseProtectedHeaders(e.base.Headers.RawProtected, e.base.Headers.Protected, &signerInfo) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } // parse unprotected headers of COSE envelope certs, ok := e.base.Headers.Unprotected[cose.HeaderLabelX5Chain].([]any) if !ok || len(certs) == 0 { return nil, &signature.InvalidSignatureError{Msg: "certificate chain is not present"} } var certChain []*x509.Certificate for _, c := range certs { certRaw, ok := c.([]byte) if !ok { return nil, &signature.InvalidSignatureError{Msg: "certificate chain is not present"} } cert, err := x509.ParseCertificate(certRaw) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } certChain = append(certChain, cert) } // populate signerInfo.CertificateChain signerInfo.CertificateChain = certChain // populate signerInfo.UnsignedAttributes.SigningAgent if h, ok := e.base.Headers.Unprotected[headerLabelSigningAgent].(string); ok { signerInfo.UnsignedAttributes.SigningAgent = h } // populate signerInfo.UnsignedAttributes.TimestampSignature if timestamepToken, ok := e.base.Headers.Unprotected[headerLabelTimestampSignature].([]byte); ok { signerInfo.UnsignedAttributes.TimestampSignature = timestamepToken } return &signerInfo, nil } // getSignatureAlgorithm picks up a recommended signing algorithm for given // certificate. func getSignatureAlgorithm(signingCert *x509.Certificate) (cose.Algorithm, error) { keySpec, err := signature.ExtractKeySpec(signingCert) if err != nil { return 0, err } return getSignatureAlgorithmFromKeySpec(keySpec) } // getSignatureAlgorithmFromKeySpec ensures the signing algorithm satisfies // algorithm requirements. func getSignatureAlgorithmFromKeySpec(keySpec signature.KeySpec) (cose.Algorithm, error) { switch keySpec.Type { case signature.KeyTypeRSA: switch keySpec.Size { case 2048: return cose.AlgorithmPS256, nil case 3072: return cose.AlgorithmPS384, nil case 4096: return cose.AlgorithmPS512, nil default: return 0, &signature.UnsupportedSigningKeyError{Msg: fmt.Sprintf("RSA: key size %d not supported", keySpec.Size)} } case signature.KeyTypeEC: switch keySpec.Size { case 256: return cose.AlgorithmES256, nil case 384: return cose.AlgorithmES384, nil case 521: return cose.AlgorithmES512, nil default: return 0, &signature.UnsupportedSigningKeyError{Msg: fmt.Sprintf("EC: key size %d not supported", keySpec.Size)} } default: return 0, &signature.UnsupportedSigningKeyError{Msg: "key type not supported"} } } // getSigner returns the built-in implementation of cose.Signer from go-cose // or a remote signer implementation of cose.Signer. func getSigner(signer signature.Signer) (signer, error) { if localSigner, ok := signer.(signature.LocalSigner); ok { return newLocalSigner(localSigner) } return newRemoteSigner(signer) } // generateProtectedHeaders creates Protected Headers of the COSE envelope // during Sign process. func generateProtectedHeaders(req *signature.SignRequest, protected cose.ProtectedHeader) error { // signingScheme crit := []any{headerLabelSigningScheme} protected[headerLabelSigningScheme] = string(req.SigningScheme) // signingTime/authenticSigningTime signingTimeLabel, ok := signingSchemeTimeLabelMap[req.SigningScheme] if !ok { return &signature.InvalidSignRequestError{Msg: "signing scheme: require notary.x509 or notary.x509.signingAuthority"} } rawTimeCBOR, err := encodeTime(req.SigningTime) if err != nil { return &signature.InvalidSignRequestError{Msg: fmt.Sprintf("signing time: %q", err)} } protected[signingTimeLabel] = rawTimeCBOR if signingTimeLabel == headerLabelAuthenticSigningTime { crit = append(crit, headerLabelAuthenticSigningTime) } // expiry if !req.Expiry.IsZero() { crit = append(crit, headerLabelExpiry) rawExpiryCBOR, err := encodeTime(req.Expiry) if err != nil { return &signature.InvalidSignRequestError{Msg: fmt.Sprintf("expiry: %q", err)} } protected[headerLabelExpiry] = rawExpiryCBOR } // extended attributes for _, elm := range req.ExtendedSignedAttributes { if _, ok := protected[elm.Key]; ok { return &signature.InvalidSignRequestError{Msg: fmt.Sprintf("%q already exists in the protected header", elm.Key)} } if elm.Critical { crit = append(crit, elm.Key) } protected[elm.Key] = elm.Value } // critical headers protected[cose.HeaderLabelCritical] = crit return nil } // generateUnprotectedHeaders creates Unprotected Headers of the COSE envelope // during Sign process. func generateUnprotectedHeaders(req *signature.SignRequest, signer signer, unprotected cose.UnprotectedHeader) { // signing agent if req.SigningAgent != "" { unprotected[headerLabelSigningAgent] = req.SigningAgent } // certChain certs := signer.CertificateChain() certChain := make([]any, len(certs)) for i, c := range certs { certChain[i] = c.Raw } unprotected[cose.HeaderLabelX5Chain] = certChain } // parseProtectedHeaders parses COSE envelope's protected headers and // populates signature.SignerInfo. func parseProtectedHeaders(rawProtected cbor.RawMessage, protected cose.ProtectedHeader, signerInfo *signature.SignerInfo) error { // validate critical headers and return extendedAttributeKeys extendedAttributeKeys, err := validateCritHeaders(protected) if err != nil { return err } // populate signerInfo.SignatureAlgorithm alg, err := protected.Algorithm() if err != nil { return err } sigAlg, ok := coseAlgSignatureAlgMap[alg] if !ok { return &signature.InvalidSignatureError{Msg: "signature algorithm not supported: " + strconv.Itoa(int(alg))} } signerInfo.SignatureAlgorithm = sigAlg // populate signerInfo.SignedAttributes.SigningScheme // headerLabelSigningScheme header has already been checked by // validateCritHeaders() at the beginning of this function. signingSchemeString := protected[headerLabelSigningScheme].(string) signingScheme := signature.SigningScheme(signingSchemeString) signerInfo.SignedAttributes.SigningScheme = signingScheme signingTimeLabel, ok := signingSchemeTimeLabelMap[signingScheme] if !ok { return &signature.InvalidSignatureError{Msg: "unsupported signingScheme: " + signingSchemeString} } // parse CBOR map from raw protected header for tag validation headerMap, err := generateRawProtectedCBORMap(rawProtected) if err != nil { return &signature.InvalidSignatureError{Msg: "generateRawProtectedCBORMap failed: " + err.Error()} } // populate signerInfo.SignedAttributes.SigningTime signingTime, err := parseTime(headerMap, signingTimeLabel, protected) if err != nil { return &signature.InvalidSignatureError{Msg: fmt.Sprintf("invalid signingTime: %v", err)} } signerInfo.SignedAttributes.SigningTime = signingTime // populate signerInfo.SignedAttributes.Expiry if _, ok := protected[headerLabelExpiry]; ok { expiry, err := parseTime(headerMap, headerLabelExpiry, protected) if err != nil { return &signature.InvalidSignatureError{Msg: fmt.Sprintf("invalid expiry: %v", err)} } signerInfo.SignedAttributes.Expiry = expiry } // populate signerInfo.SignedAttributes.ExtendedAttributes signerInfo.SignedAttributes.ExtendedAttributes, err = generateExtendedAttributes(extendedAttributeKeys, protected) return err } // validateCritHeaders does a two-way check, namely: // 1. validate that all critical headers are present in the protected bucket // 2. validate that all required headers(as per spec) are marked critical // Returns list of extended attribute keys func validateCritHeaders(protected cose.ProtectedHeader) ([]any, error) { // This ensures all critical headers are present in the protected bucket. labels, err := protected.Critical() if err != nil { return nil, err } // set of headers that must be marked as crit mustMarkedCrit := make(map[any]struct{}) mustMarkedCrit[headerLabelSigningScheme] = struct{}{} signingScheme, ok := protected[headerLabelSigningScheme].(string) if !ok { return nil, &signature.InvalidSignatureError{Msg: "invalid signingScheme"} } if signature.SigningScheme(signingScheme) == signature.SigningSchemeX509SigningAuthority { mustMarkedCrit[headerLabelAuthenticSigningTime] = struct{}{} } if _, ok := protected[headerLabelExpiry]; ok { mustMarkedCrit[headerLabelExpiry] = struct{}{} } // validate that all required headers(as per spec) are marked as critical for _, label := range labels { delete(mustMarkedCrit, label) } if len(mustMarkedCrit) != 0 { headers := make([]any, 0, len(mustMarkedCrit)) for k := range mustMarkedCrit { headers = append(headers, k) } return nil, &signature.InvalidSignatureError{Msg: fmt.Sprintf("these required headers are not marked as critical: %v", headers)} } // fetch all the extended signed attributes systemHeaders := []any{cose.HeaderLabelAlgorithm, cose.HeaderLabelCritical, cose.HeaderLabelContentType, headerLabelExpiry, headerLabelSigningScheme, headerLabelSigningTime, headerLabelAuthenticSigningTime} var extendedAttributeKeys []any for label := range protected { if contains(systemHeaders, label) { continue } extendedAttributeKeys = append(extendedAttributeKeys, label) } return extendedAttributeKeys, nil } // generateExtendedAttributes generates []signature.Attribute during // SignerInfo process. func generateExtendedAttributes(extendedAttributeKeys []any, protected cose.ProtectedHeader) ([]signature.Attribute, error) { criticalHeaders, ok := protected[cose.HeaderLabelCritical].([]any) if !ok { return nil, &signature.InvalidSignatureError{Msg: "invalid critical headers"} } var extendedAttr []signature.Attribute for _, key := range extendedAttributeKeys { extendedAttr = append(extendedAttr, signature.Attribute{ Key: key, Critical: contains(criticalHeaders, key), Value: protected[key], }) } return extendedAttr, nil } // contains checks if e is in s func contains(s []any, e any) bool { for _, a := range s { if a == e { return true } } return false } // encodeTime generates a Tag1 Datetime CBOR object and casts it to // cbor.RawMessage func encodeTime(t time.Time) (cbor.RawMessage, error) { timeCBOR, err := encMode.Marshal(t) if err != nil { return nil, err } return cbor.RawMessage(timeCBOR), nil } // decodeTime decodes cbor.RawMessage of Tag1 Datetime CBOR object // into time.Time // // For more details: https://github.com/fxamacker/cbor/blob/7704fa5efaf3ef4ac35aff38f50f6ff567793072/decode.go#L52 func decodeTime(timeRaw cbor.RawMessage) (time.Time, error) { var t time.Time err := decMode.Unmarshal([]byte(timeRaw), &t) if err != nil { return time.Time{}, err } return t, nil } // parseTime validates Tag1 Datetime in headerMap given label, then returns // time.Time value from cose.ProtectedHeader. func parseTime(headerMap map[any]cbor.RawMessage, label string, protected cose.ProtectedHeader) (time.Time, error) { switch t := protected[label].(type) { // cbor.RawMessage indicates the signing process. case cbor.RawMessage: return decodeTime(t) // time.Time indicates the verififcation process. // only need to validate Tag1 Datetime during verification. case time.Time: rawMsg, ok := headerMap[label] if !ok { return time.Time{}, fmt.Errorf("headerMap is missing label %q", label) } rawTag := &cbor.RawTag{} err := rawTag.UnmarshalCBOR([]byte(rawMsg)) if err != nil { return time.Time{}, fmt.Errorf("header %q time value does not have a tag", label) } if rawTag.Number != 1 { return time.Time{}, errors.New("only Tag `1` Datetime CBOR object is supported") } return t, nil case nil: return time.Time{}, fmt.Errorf("protected header %q is missing", label) } return time.Time{}, errors.New("invalid timeValue type") } // generateRawProtectedCBORMap unmarshals rawProtected Header of COSE // envelope into a headerMap. func generateRawProtectedCBORMap(rawProtected cbor.RawMessage) (map[any]cbor.RawMessage, error) { // empty rawProtected indicates signing process if len(rawProtected) == 0 { return nil, nil } var decoded []byte err := decMode.Unmarshal(rawProtected, &decoded) if err != nil { return nil, err } var headerMap map[any]cbor.RawMessage err = cbor.Unmarshal(decoded, &headerMap) if err != nil { return nil, err } return headerMap, nil } // hashFromCOSEAlgorithm maps the cose algorithm supported by go-cose to hash func hashFromCOSEAlgorithm(alg cose.Algorithm) (crypto.Hash, error) { switch alg { case cose.AlgorithmPS256, cose.AlgorithmES256: return crypto.SHA256, nil case cose.AlgorithmPS384, cose.AlgorithmES384: return crypto.SHA384, nil case cose.AlgorithmPS512, cose.AlgorithmES512: return crypto.SHA512, nil default: return 0, fmt.Errorf("unsupported cose algorithm %s", alg) } } notation-core-go-1.1.0/signature/cose/envelope_test.go000066400000000000000000001116511466321423000230400ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cose import ( "crypto" "crypto/x509" "errors" "fmt" "testing" "time" "github.com/fxamacker/cbor/v2" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/signaturetest" "github.com/notaryproject/notation-core-go/testhelper" nx509 "github.com/notaryproject/notation-core-go/x509" "github.com/notaryproject/tspclient-go" "github.com/veraison/go-cose" ) const ( payloadString = "{\"targetArtifact\":{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333\",\"size\":16724,\"annotations\":{\"io.wabbit-networks.buildId\":\"123\"}}}" rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" ) var ( signingSchemeString = []string{"notary.x509", "notary.x509.signingAuthority"} ) func TestParseEnvelopeError(t *testing.T) { var emptyCOSE []byte _, err := ParseEnvelope(emptyCOSE) if err == nil { t.Fatalf("ParseEnvelope() expects signature.InvalidSignatureError, but got nil.") } _, err = ParseEnvelope([]byte("invalid")) if err == nil { t.Fatalf("ParseEnvelope() expects signature.InvalidSignatureError, but got nil.") } } func TestSign(t *testing.T) { env := createNewEnv(nil) for _, signingScheme := range signingSchemeString { for _, keyType := range signaturetest.KeyTypes { for _, size := range signaturetest.GetKeySizes(keyType) { t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize when all arguments are present", signingScheme, keyType, size), func(t *testing.T) { signRequest, err := newSignRequest(signingScheme, keyType, size) if err != nil { t.Fatalf("newSignRequest() failed. Error = %s", err) } encoded, err := env.Sign(signRequest) if err != nil || len(encoded) == 0 { t.Fatalf("Sign() failed. Error = %s", err) } }) t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize when minimal arguments are present", signingScheme, keyType, size), func(t *testing.T) { signer, err := signaturetest.GetTestLocalSigner(keyType, size) if err != nil { t.Fatalf("Sign() failed. Error = %s", err) } signRequest := &signature.SignRequest{ Payload: signature.Payload{ ContentType: "application/vnd.cncf.notary.payload.v1+json", Content: []byte(payloadString), }, Signer: signer, SigningTime: time.Now(), SigningScheme: signature.SigningScheme(signingScheme), } encoded, err := env.Sign(signRequest) if err != nil || len(encoded) == 0 { t.Fatalf("Sign() failed. Error = %s", err) } }) t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize when expiry is not present", signingScheme, keyType, size), func(t *testing.T) { signRequest, err := newSignRequest(signingScheme, keyType, size) if err != nil { t.Fatalf("newSignRequest() failed. Error = %s", err) } signRequest.Expiry = time.Time{} encoded, err := env.Sign(signRequest) if err != nil || len(encoded) == 0 { t.Fatalf("Sign() failed. Error = %s", err) } }) t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize when signingAgent is not present", signingScheme, keyType, size), func(t *testing.T) { signRequest, err := newSignRequest(signingScheme, keyType, size) if err != nil { t.Fatalf("newSignRequest() failed. Error = %s", err) } signRequest.SigningAgent = "" encoded, err := env.Sign(signRequest) if err != nil || len(encoded) == 0 { t.Fatalf("Sign() failed. Error = %s", err) } }) t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize when extended signed attributes are not present", signingScheme, keyType, size), func(t *testing.T) { signRequest, err := newSignRequest(signingScheme, keyType, size) if err != nil { t.Fatalf("newSignRequest() failed. Error = %s", err) } signRequest.ExtendedSignedAttributes = nil encoded, err := env.Sign(signRequest) if err != nil || len(encoded) == 0 { t.Fatalf("Sign() failed. Error = %s", err) } }) } } } t.Run("with timestmap countersignature request", func(t *testing.T) { signRequest, err := newSignRequest("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("newSignRequest() failed. Error = %s", err) } signRequest.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl) if err != nil { t.Fatal(err) } rootCerts, err := nx509.ReadCertificateFile("../../internal/timestamp/testdata/tsaRootCert.crt") if err != nil || len(rootCerts) == 0 { t.Fatal("failed to read root CA certificate:", err) } rootCert := rootCerts[0] rootCAs := x509.NewCertPool() rootCAs.AddCert(rootCert) signRequest.TSARootCAs = rootCAs encoded, err := env.Sign(signRequest) if err != nil || encoded == nil { t.Fatalf("Sign() failed. Error = %s", err) } content, err := env.Content() if err != nil { t.Fatal(err) } timestampToken := content.SignerInfo.UnsignedAttributes.TimestampSignature if len(timestampToken) == 0 { t.Fatal("expected timestamp token to be present") } signedToken, err := tspclient.ParseSignedToken(timestampToken) if err != nil { t.Fatal(err) } info, err := signedToken.Info() if err != nil { t.Fatal(err) } _, err = info.Validate(content.SignerInfo.Signature) if err != nil { t.Fatal(err) } }) } func TestSignErrors(t *testing.T) { env := createNewEnv(nil) // Testing getSigner() t.Run("errorLocalSigner: when getSigner has privateKeyError", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Signer = &errorLocalSigner{privateKeyError: true} _, err = env.Sign(signRequest) expected := errors.New("signing key is not supported") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) t.Run("errorLocalSigner: when getSigner has keySpecError", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Signer = &errorLocalSigner{keySpecError: true} _, err = env.Sign(signRequest) expected := errors.New("intended KeySpec() error") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) t.Run("errorLocalSigner: when getSigner has algError", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Signer = &errorLocalSigner{algError: true} _, err = env.Sign(signRequest) expected := errors.New("RSA: key size 0 not supported") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) t.Run("errorRemoteSigner: when getSigner has keySpecError", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Signer = &errorRemoteSigner{keySpecError: true} _, err = env.Sign(signRequest) expected := errors.New("intended KeySpec() error") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) t.Run("errorRemoteSigner: when getSigner has algError", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Signer = &errorRemoteSigner{algError: true} _, err = env.Sign(signRequest) expected := errors.New("RSA: key size 0 not supported") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) t.Run("errorRemoteSigner: when getSigner has algError and wantEC", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Signer = &errorRemoteSigner{algError: true, wantEC: true} _, err = env.Sign(signRequest) expected := errors.New("EC: key size 0 not supported") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) t.Run("errorRemoteSigner: when getSigner has keyTypeError", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Signer = &errorRemoteSigner{keyTypeError: true} _, err = env.Sign(signRequest) expected := errors.New("key type not supported") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) // Testing generateProtectedHeaders t.Run("when signingScheme is absent", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.SigningScheme = "" _, err = env.Sign(signRequest) expected := errors.New("signing scheme: require notary.x509 or notary.x509.signingAuthority") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) t.Run("when an extended signed attribute already exists in the protected header", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.ExtendedSignedAttributes = []signature.Attribute{ {Key: headerLabelSigningScheme, Value: "notary.x509", Critical: true}, } _, err = env.Sign(signRequest) expected := errors.New("\"io.cncf.notary.signingScheme\" already exists in the protected header") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) // Testing core sign process t.Run("when signer has signError", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Signer = &errorRemoteSigner{signError: true} _, err = env.Sign(signRequest) expected := errors.New("intended Sign() Error") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) t.Run("when signer returns empty signature", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Signer = &errorRemoteSigner{} _, err = env.Sign(signRequest) expected := errors.New("empty signature") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) // Testing generateUnprotectedHeaders t.Run("errorLocalSigner: when signer has certificateChainError", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Signer = &errorLocalSigner{certificateChainError: true} _, err = env.Sign(signRequest) expected := errors.New("intended CertificateChain() error") if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } }) t.Run("when invalid tsa url is provided", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %v", err) } signRequest.Timestamper, err = tspclient.NewHTTPTimestamper(nil, "invalid") if err != nil { t.Fatal(err) } expected := errors.New("timestamp: Post \"invalid\": unsupported protocol scheme \"\"") encoded, err := env.Sign(signRequest) if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } var timestampErr *signature.TimestampError if !errors.As(err, ×tampErr) { t.Fatal("expected signature.TimestampError") } if encoded != nil { t.Fatal("expected nil signature envelope") } }) } func TestVerifyErrors(t *testing.T) { t.Run("when signature envelope is not present", func(t *testing.T) { env := createNewEnv(nil) _, err := env.Verify() expected := errors.New("signature envelope is not present") if !isErrEqual(expected, err) { t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope has invalid certificate chain", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = []any{} _, err = env.Verify() expected := errors.New("certificate chain is not present") if !isErrEqual(expected, err) { t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope leaf certificate has wrong type", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = []any{0} _, err = env.Verify() expected := errors.New("COSE envelope malformed leaf certificate") if !isErrEqual(expected, err) { t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope has malformed leaf certificate", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } certs, ok := env.base.Headers.Unprotected[cose.HeaderLabelX5Chain].([]any) if !ok || len(certs) == 0 { t.Fatalf("certificate chain is not present") } certRaw, ok := certs[0].([]byte) if !ok { t.Fatalf("COSE envelope malformed leaf certificate") } // Manipulate the leaf certificate certRaw[0] += 'A' certs[0] = certRaw env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = certs _, err = env.Verify() expected := errors.New("malformed leaf certificate") if !isErrEqual(expected, err) { t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) } }) t.Run("when getSignatureAlgorithm fails due to unsupported public key", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } certs := []*x509.Certificate{testhelper.GetUnsupportedRSACert().Cert} certChain := make([]any, len(certs)) for i, c := range certs { certChain[i] = c.Raw } env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = certChain _, err = env.Verify() expected := errors.New("rsa key size 1024 bits is not supported") if !isErrEqual(expected, err) { t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) } }) // Testing core verify process t.Run("when tempered signature envelope is provided", func(t *testing.T) { signRequest, err := getSignRequest() if err != nil { t.Fatalf("getSignRequest() failed. Error = %s", err) } env := NewEnvelope() encoded, err := env.Sign(signRequest) if err != nil { t.Fatalf("Sign() failed. Error = %s", err) } encoded[len(encoded)-10] += 'A' newEnv, err := ParseEnvelope(encoded) if err != nil { t.Fatalf("ParseEnvelope() failed. Error = %s", err) } _, err = newEnv.Verify() expected := errors.New("signature is invalid. Error: verification error") if !isErrEqual(expected, err) { t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) } }) t.Run("when get payload fails", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } delete(env.base.Headers.Protected, cose.HeaderLabelContentType) _, err = env.Verify() expected := errors.New("missing content type") if !isErrEqual(expected, err) { t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) } }) t.Run("when get signerInfo fails", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Protected[cose.HeaderLabelCritical] = []any{} _, err = env.Verify() expected := errors.New("empty crit header") if !isErrEqual(expected, err) { t.Fatalf("Verify() expects error: %v, but got: %v.", expected, err) } }) } func TestPayloadErrors(t *testing.T) { t.Run("when env.base is nil", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base = nil _, err = env.Content() expected := errors.New("signature envelope is not present") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when missing content type", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } delete(env.base.Headers.Protected, cose.HeaderLabelContentType) _, err = env.Content() expected := errors.New("missing content type") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when content type has wrong type", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Protected[cose.HeaderLabelContentType] = 0 _, err = env.Content() expected := errors.New("content type should be of 'tstr' type") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) } func TestSignerInfoErrors(t *testing.T) { t.Run("when signature missing in COSE envelope", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Signature = []byte{} _, err = env.Content() expected := errors.New("signature missing in COSE envelope") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) // Testing parseProtectedHeaders t.Run("when COSE envelope protected header has empty crit", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Protected[cose.HeaderLabelCritical] = []any{} _, err = env.Content() expected := errors.New("empty crit header") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope protected header has invalid crit", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Protected[cose.HeaderLabelCritical] = "invalid" _, err = env.Content() expected := errors.New("invalid crit header") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope protected header signingScheme has wrong type", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Protected[headerLabelSigningScheme] = 0 _, err = env.Content() expected := errors.New("invalid signingScheme") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope has required headers that are not marked as critical", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Protected[cose.HeaderLabelCritical] = []any{"io.cncf.notary.expiry"} _, err = env.Content() expected := errors.New("these required headers are not marked as critical: [io.cncf.notary.signingScheme]") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope protected header missing algorithm", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } delete(env.base.Headers.Protected, cose.HeaderLabelAlgorithm) _, err = env.Content() expected := errors.New("algorithm not found") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope protected header has unsupported algorithm", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Protected.SetAlgorithm(cose.AlgorithmEd25519) _, err = env.Content() expected := errors.New("signature algorithm not supported: -8") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope protected header has unsupported signingScheme", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Protected[headerLabelSigningScheme] = "unsupported" _, err = env.Content() expected := errors.New("unsupported signingScheme: unsupported") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when parseTime has Tag0 signingTime", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } raw := generateTestRawMessage(env.base.Headers.RawProtected, headerLabelSigningTime, false, false) env.base.Headers.RawProtected = raw _, err = env.Content() expected := errors.New("invalid signingTime: only Tag `1` Datetime CBOR object is supported") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when parseTime has Tag0 expiry", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } raw := generateTestRawMessage(env.base.Headers.RawProtected, headerLabelExpiry, false, false) env.base.Headers.RawProtected = raw _, err = env.Content() expected := errors.New("invalid expiry: only Tag `1` Datetime CBOR object is supported") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when parseTime fails at headerMap missing signgingTime", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } raw := generateTestRawMessage(env.base.Headers.RawProtected, headerLabelSigningTime, true, true) env.base.Headers.RawProtected = raw _, err = env.Content() expected := errors.New("invalid signingTime: headerMap is missing label \"io.cncf.notary.signingTime\"") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when parseTime fails at signgingTime tag validation", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } raw := generateTestRawMessage(env.base.Headers.RawProtected, headerLabelSigningTime, true, false) env.base.Headers.RawProtected = raw _, err = env.Content() expected := errors.New("invalid signingTime: header \"io.cncf.notary.signingTime\" time value does not have a tag") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when parseTime fails at expiry tag validation", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } raw := generateTestRawMessage(env.base.Headers.RawProtected, headerLabelExpiry, true, false) env.base.Headers.RawProtected = raw _, err = env.Content() expected := errors.New("invalid expiry: header \"io.cncf.notary.expiry\" time value does not have a tag") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when decodeTime fails", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } raw := cbor.RawMessage{} env.base.Headers.Protected[headerLabelSigningTime] = raw raw = nil _, err = env.Content() expected := errors.New("invalid signingTime: EOF") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope protected header has invalid signingTime", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Protected[headerLabelSigningTime] = "invalid" _, err = env.Content() expected := errors.New("invalid signingTime: invalid timeValue type") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope protected header missing signingTime", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } delete(env.base.Headers.Protected, headerLabelSigningTime) _, err = env.Content() expected := errors.New("invalid signingTime: protected header \"io.cncf.notary.signingTime\" is missing") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope protected header has invalid expiry", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Protected[headerLabelExpiry] = "invalid" _, err = env.Content() expected := errors.New("invalid expiry: invalid timeValue type") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) // Testing unprotected headers t.Run("when COSE envelope has invalid certificate chain", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } delete(env.base.Headers.Unprotected, cose.HeaderLabelX5Chain) _, err = env.Content() expected := errors.New("certificate chain is not present") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope leaf certificate has wrong type", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = []any{0} _, err = env.Content() expected := errors.New("certificate chain is not present") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) t.Run("when COSE envelope has malformed leaf certificate", func(t *testing.T) { env, err := getVerifyCOSE("notary.x509", signature.KeyTypeRSA, 3072) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } certs, ok := env.base.Headers.Unprotected[cose.HeaderLabelX5Chain].([]any) if !ok || len(certs) == 0 { t.Fatalf("certificate chain is not present") } certRaw, ok := certs[0].([]byte) if !ok { t.Fatalf("COSE envelope malformed leaf certificate") } // Manipulate the leaf certificate certRaw[0] += 'A' certs[0] = certRaw env.base.Headers.Unprotected[cose.HeaderLabelX5Chain] = certs _, err = env.Content() expected := errors.New("x509: malformed certificate") if !isErrEqual(expected, err) { t.Fatalf("Content() expects error: %v, but got: %v.", expected, err) } }) } func TestSignAndVerify(t *testing.T) { env := createNewEnv(nil) for _, signingScheme := range signingSchemeString { for _, keyType := range signaturetest.KeyTypes { for _, size := range signaturetest.GetKeySizes(keyType) { t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize", signingScheme, keyType, size), func(t *testing.T) { // Sign signRequest, err := newSignRequest(signingScheme, keyType, size) if err != nil { t.Fatalf("newSignRequest() failed. Error = %s", err) } encoded, err := env.Sign(signRequest) if err != nil || len(encoded) == 0 { t.Fatalf("Sign() faild. Error = %s", err) } // Verify using the same envelope struct // (Verify with UnmarshalCBOR is covered in the // TestSignAndParseVerify() part) _, err = env.Verify() if err != nil { t.Fatalf("Verify() failed. Error = %s", err) } }) } } } } func TestSignAndParseVerify(t *testing.T) { for _, signingScheme := range signingSchemeString { for _, keyType := range signaturetest.KeyTypes { for _, size := range signaturetest.GetKeySizes(keyType) { t.Run(fmt.Sprintf("with %s scheme, %v keyType, %v keySize", signingScheme, keyType, size), func(t *testing.T) { //Verify after UnmarshalCBOR env, err := getVerifyCOSE(signingScheme, keyType, size) if err != nil { t.Fatalf("getVerifyCOSE() failed. Error = %s", err) } _, err = env.Verify() if err != nil { t.Fatalf("Verify() failed. Error = %s", err) } }) } } } } func TestGenerateExtendedAttributesError(t *testing.T) { var extendedAttributeKeys []any var protected cose.ProtectedHeader _, err := generateExtendedAttributes(extendedAttributeKeys, protected) expected := errors.New("invalid critical headers") if !isErrEqual(expected, err) { t.Fatalf("TestgenerateExtendedAttributesError() expects error: %v, but got: %v.", expected, err) } } func TestHashFunc(t *testing.T) { hash, err := hashFromCOSEAlgorithm(cose.AlgorithmPS256) if err != nil || hash.String() != "SHA-256" { t.Fatalf("expected SHA-256, but got %s", hash) } hash, err = hashFromCOSEAlgorithm(cose.AlgorithmPS384) if err != nil || hash.String() != "SHA-384" { t.Fatalf("expected SHA-384, but got %s", hash) } hash, err = hashFromCOSEAlgorithm(cose.AlgorithmPS512) if err != nil || hash.String() != "SHA-512" { t.Fatalf("expected SHA-512, but got %s", hash) } hash, err = hashFromCOSEAlgorithm(cose.AlgorithmES256) if err != nil || hash.String() != "SHA-256" { t.Fatalf("expected SHA-256, but got %s", hash) } hash, err = hashFromCOSEAlgorithm(cose.AlgorithmES384) if err != nil || hash.String() != "SHA-384" { t.Fatalf("expected SHA-384, but got %s", hash) } hash, err = hashFromCOSEAlgorithm(cose.AlgorithmES512) if err != nil || hash.String() != "SHA-512" { t.Fatalf("expected SHA-512, but got %s", hash) } _, err = hashFromCOSEAlgorithm(cose.AlgorithmEd25519) expectedErrMsg := "unsupported cose algorithm EdDSA" if err == nil || err.Error() != expectedErrMsg { t.Fatalf("expected %s, but got %s", expectedErrMsg, err) } } func newSignRequest(signingScheme string, keyType signature.KeyType, size int) (*signature.SignRequest, error) { signer, err := signaturetest.GetTestLocalSigner(keyType, size) if err != nil { return nil, err } return &signature.SignRequest{ Payload: signature.Payload{ ContentType: "application/vnd.cncf.notary.payload.v1+json", Content: []byte(payloadString), }, Signer: signer, SigningTime: time.Now().Truncate(time.Second), Expiry: time.Now().AddDate(0, 0, 1).Truncate(time.Second), ExtendedSignedAttributes: []signature.Attribute{ {Key: "signedCritKey1", Value: "signedCritValue1", Critical: true}, {Key: "signedKey1", Value: "signedValue1", Critical: false}, }, SigningAgent: "NotationUnitTest/1.0.0", SigningScheme: signature.SigningScheme(signingScheme), }, nil } func getSignRequest() (*signature.SignRequest, error) { return newSignRequest("notary.x509", signature.KeyTypeRSA, 3072) } func getVerifyCOSE(signingScheme string, keyType signature.KeyType, size int) (envelope, error) { signRequest, err := newSignRequest(signingScheme, keyType, size) if err != nil { return createNewEnv(nil), err } env := NewEnvelope() encoded, err := env.Sign(signRequest) if err != nil { return createNewEnv(nil), err } var msg cose.Sign1Message if err := msg.UnmarshalCBOR(encoded); err != nil { return createNewEnv(nil), err } newEnv := createNewEnv(&msg) return newEnv, nil } // errorLocalSigner implements signature.LocalSigner interface. type errorLocalSigner struct { signature.LocalSigner privateKeyError bool keySpecError bool algError bool certificateChainError bool } // Sign signs the digest and returns the raw signature func (s *errorLocalSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { return nil, nil, fmt.Errorf("local signer doesn't support Sign with digest") } // CertificateChain returns the certificate chain func (s *errorLocalSigner) CertificateChain() ([]*x509.Certificate, error) { if s.certificateChainError { return nil, fmt.Errorf("intended CertificateChain() error") } return []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert}, nil } // KeySpec returns the key specification func (s *errorLocalSigner) KeySpec() (signature.KeySpec, error) { if s.keySpecError { return signature.KeySpec{}, fmt.Errorf("intended KeySpec() error") } if s.algError { return signature.KeySpec{ Type: signature.KeyTypeRSA, Size: 0, }, nil } return signature.KeySpec{ Type: signature.KeyTypeRSA, Size: 3072, }, nil } // PrivateKey returns the private key func (s *errorLocalSigner) PrivateKey() crypto.PrivateKey { if s.privateKeyError { return fmt.Errorf("intended PrivateKey() Error") } return testhelper.GetRSALeafCertificate().PrivateKey } // errorRemoteSigner implements signature.Signer interface. type errorRemoteSigner struct { signError bool keySpecError bool algError bool wantEC bool keyTypeError bool } // Sign signs the digest and returns the raw signature func (s *errorRemoteSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { if s.signError { return nil, nil, fmt.Errorf("intended Sign() Error") } return nil, nil, nil } // KeySpec returns the key specification func (s *errorRemoteSigner) KeySpec() (signature.KeySpec, error) { if s.keySpecError { return signature.KeySpec{}, fmt.Errorf("intended KeySpec() error") } if s.algError { if s.wantEC { return signature.KeySpec{ Type: signature.KeyTypeEC, Size: 0, }, nil } return signature.KeySpec{ Type: signature.KeyTypeRSA, Size: 0, }, nil } if s.keyTypeError { return signature.KeySpec{ Type: 3, Size: 3072, }, nil } if s.wantEC { return signature.KeySpec{ Type: signature.KeyTypeEC, Size: 384, }, nil } return signature.KeySpec{ Type: signature.KeyTypeRSA, Size: 3072, }, nil } func isErrEqual(wanted, got error) bool { if wanted == nil && got == nil { return true } if wanted != nil && got != nil { return wanted.Error() == got.Error() } return false } func createNewEnv(msg *cose.Sign1Message) envelope { return envelope{ base: msg, } } func generateTestRawMessage(raw cbor.RawMessage, label string, unmarshalError bool, mapError bool) cbor.RawMessage { var decoded []byte decMode.Unmarshal(raw, &decoded) cborMap := make(map[string]cbor.RawMessage) cbor.Unmarshal(decoded, &cborMap) if unmarshalError { // "invalid" cborMap[label] = cbor.RawMessage([]byte{103, 105, 110, 118, 97, 108, 105, 100}) if mapError { delete(cborMap, label) } } else { // construct Tag0 Datetime CBOR object encOptsTag0 := cbor.EncOptions{ Time: cbor.TimeRFC3339, TimeTag: cbor.EncTagRequired, } encModeTag0, _ := encOptsTag0.EncMode() timeCBOR, _ := encModeTag0.Marshal(time.Now()) cborMap[label] = timeCBOR } encoded, err := cbor.Marshal(cborMap) if err != nil { fmt.Println("err1:", err) } resRaw, err := encMode.Marshal(encoded) if err != nil { fmt.Println("err2:", err) } return resRaw } notation-core-go-1.1.0/signature/cose/testdata/000077500000000000000000000000001466321423000214415ustar00rootroot00000000000000notation-core-go-1.1.0/signature/cose/testdata/conformance.json000066400000000000000000000105611466321423000246310ustar00rootroot00000000000000{ "title": "Sign1 - RSASSA-PSS w/ SHA-384 (sign)", "description": "Sign with one signer using RSASSA-PSS w/ SHA-384", "signingTime": 1661321924, "expiry": 1661408324, "payload": "68656C6C6F20434F5345", "protectedHeaders": { "cborHex": "A80138250283781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D6575696F2E636E63662E6E6F746172792E6578706972796E7369676E6564437269744B65793103782B6170706C69636174696F6E2F766E642E636E63662E6E6F746172792E7061796C6F61642E76312B6A736F6E6A7369676E65644B6579316C7369676E656456616C7565316E7369676E6564437269744B657931707369676E65644372697456616C75653175696F2E636E63662E6E6F746172792E657870697279C11A63071444781A696F2E636E63662E6E6F746172792E7369676E696E6754696D65C11A6305C2C4781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D656B6E6F746172792E78353039", "cborDiag": "{1: -38, 2: [\"io.cncf.notary.signingScheme\", \"io.cncf.notary.expiry\", \"signedCritKey1\"], 3: \"application/vnd.cncf.notary.payload.v1+json\", \"signedKey1\": \"signedValue1\", \"signedCritKey1\": \"signedCritValue1\", \"io.cncf.notary.expiry\": 1(1661408324), \"io.cncf.notary.signingTime\": 1(1661321924), \"io.cncf.notary.signingScheme\": \"notary.x509\"}" }, "unprotectedHeaders": { "cborHex": "A1781B696F2E636E63662E6E6F746172792E7369676E696E674167656E74781D4E6F746174696F6E436F6E666F726D616E6365546573742F312E302E30", "cborDiag": "{\"io.cncf.notary.signingAgent\":\"NotationConformanceTest/1.0.0\"}" }, "expectedOutput": { "cborHex": "D284590117A80138250283781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D6575696F2E636E63662E6E6F746172792E6578706972796E7369676E6564437269744B65793103782B6170706C69636174696F6E2F766E642E636E63662E6E6F746172792E7061796C6F61642E76312B6A736F6E6A7369676E65644B6579316C7369676E656456616C7565316E7369676E6564437269744B657931707369676E65644372697456616C75653175696F2E636E63662E6E6F746172792E657870697279C11A63071444781A696F2E636E63662E6E6F746172792E7369676E696E6754696D65C11A6305C2C4781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D656B6E6F746172792E78353039A1781B696F2E636E63662E6E6F746172792E7369676E696E674167656E74781D4E6F746174696F6E436F6E666F726D616E6365546573742F312E302E304A68656C6C6F20434F53455901805BFEC0A345098B9B9B6FB7358FACE7AB76D191B648CCD19E36FB03C2085EA072EC050D9C6E4FA4845478386D0831A2360D343A1FF027BDD56D496F996B90AC2DB9DA2460BAFFEC21DB7C0CA759BA83AB35CDF521C0926138681BDE05277E2976CEDBEB4040C930908EF2B113D935378BD3C5E7740119B2B81C59E9C6C24411ABDF699547864F68F2E0F6346EEFF627BF0D971ABDF94E67E12A10134CCBBADFA0AB4031B18705696A9567A0F1F061247FDD00D343EA3A45F63DA7F80771612B38FC9877375BCBCE28AEF1F3EE2B25869722C24737C49D8C6711376DD62B3D32B24D489746E2BA5D25FA76FEBCC6ABF9D2BAEE67221C85A7A8F8763DADC5E20BB8C5C03A75C68211557813D2D6ADEA56EC5526F78C18460B1944C8307A4B0ED64A6D6B4ABED5067DE5A5AD38948A2EA140B01A7762C15B3E63D7D7BDC8962E6C4BFF18B34D2A19FC627F02EBF88DAF7FB25C55CE1B9CA06ADE02F9D60AD16CB306F433F692E598132D67B5D0A02193191D5C9CD52AD81F4E31917E5B5D40EF5CE7", "cborDiag": "18([h'A80138250283781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D6575696F2E636E63662E6E6F746172792E6578706972796E7369676E6564437269744B65793103782B6170706C69636174696F6E2F766E642E636E63662E6E6F746172792E7061796C6F61642E76312B6A736F6E6A7369676E65644B6579316C7369676E656456616C7565316E7369676E6564437269744B657931707369676E65644372697456616C75653175696F2E636E63662E6E6F746172792E657870697279C11A63071444781A696F2E636E63662E6E6F746172792E7369676E696E6754696D65C11A6305C2C4781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D656B6E6F746172792E78353039', {\"io.cncf.notary.signingAgent\":\"NotationConformanceTest/1.0.0\"}, h'68656C6C6F20434F5345', h'5bfec0a345098b9b9b6fb7358face7ab76d191b648ccd19e36fb03c2085ea072ec050d9c6e4fa4845478386d0831a2360d343a1ff027bdd56d496f996b90ac2db9da2460baffec21db7c0ca759ba83ab35cdf521c0926138681bde05277e2976cedbeb4040c930908ef2b113d935378bd3c5e7740119b2b81c59e9c6c24411abdf699547864f68f2e0f6346eeff627bf0d971abdf94e67e12a10134ccbbadfa0ab4031b18705696a9567a0f1f061247fdd00d343ea3a45f63da7f80771612b38fc9877375bcbce28aef1f3ee2b25869722c24737c49d8c6711376dd62b3d32b24d489746e2ba5d25fa76febcc6abf9d2baee67221c85a7a8f8763dadc5e20bb8c5c03a75c68211557813d2d6adea56ec5526f78c18460b1944c8307a4b0ed64a6d6b4abed5067de5a5ad38948a2ea140b01a7762c15b3e63d7d7bdc8962e6c4bff18b34d2a19fc627f02ebf88daf7fb25c55ce1b9ca06ade02f9d60ad16cb306f433f692e598132d67b5d0a02193191d5c9cd52ad81f4e31917e5b5d40ef5ce7'])" } } notation-core-go-1.1.0/signature/envelope.go000066400000000000000000000071151466321423000210470ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package signature provides operations for types that implement // signature.Envelope or signature.Signer. // // An Envelope is a structure that creates and verifies a signature using the // specified signing algorithm with required validation. To register a new // envelope, call RegisterEnvelopeType first during the initialization. // // A Signer is a structure used to sign payload generated after signature // envelope created. The underlying signing logic is provided by the underlying // local crypto library or the external signing plugin. package signature import ( "fmt" "sync" ) // Envelope provides basic functions to manipulate signatures. type Envelope interface { // Sign generates and sign the envelope according to the sign request. Sign(req *SignRequest) ([]byte, error) // Verify verifies the envelope and returns its enclosed payload and signer // info. Verify() (*EnvelopeContent, error) // Content returns the payload and signer information of the envelope. // Content is trusted only after the successful call to `Verify()`. Content() (*EnvelopeContent, error) } // NewEnvelopeFunc defines a function to create a new Envelope. type NewEnvelopeFunc func() Envelope // ParseEnvelopeFunc defines a function that takes envelope bytes to create // an Envelope. type ParseEnvelopeFunc func([]byte) (Envelope, error) // envelopeFunc wraps functions to create and parsenew envelopes. type envelopeFunc struct { newFunc NewEnvelopeFunc parseFunc ParseEnvelopeFunc } // envelopeFuncs maps envelope media type to corresponding constructors and // parsers. var envelopeFuncs sync.Map // map[string]envelopeFunc // RegisterEnvelopeType registers newFunc and parseFunc for the given mediaType. // Those functions are intended to be called when creating a new envelope. // It will be called while inializing the built-in envelopes(JWS/COSE). func RegisterEnvelopeType(mediaType string, newFunc NewEnvelopeFunc, parseFunc ParseEnvelopeFunc) error { if newFunc == nil || parseFunc == nil { return fmt.Errorf("required functions not provided") } envelopeFuncs.Store(mediaType, envelopeFunc{ newFunc: newFunc, parseFunc: parseFunc, }) return nil } // RegisteredEnvelopeTypes lists registered envelope media types. func RegisteredEnvelopeTypes() []string { var types []string envelopeFuncs.Range(func(k, v any) bool { types = append(types, k.(string)) return true }) return types } // NewEnvelope generates an envelope of given media type. func NewEnvelope(mediaType string) (Envelope, error) { val, ok := envelopeFuncs.Load(mediaType) if !ok { return nil, &UnsupportedSignatureFormatError{MediaType: mediaType} } return val.(envelopeFunc).newFunc(), nil } // ParseEnvelope generates an envelope for given envelope bytes with specified // media type. func ParseEnvelope(mediaType string, envelopeBytes []byte) (Envelope, error) { val, ok := envelopeFuncs.Load(mediaType) if !ok { return nil, &UnsupportedSignatureFormatError{MediaType: mediaType} } return val.(envelopeFunc).parseFunc(envelopeBytes) } notation-core-go-1.1.0/signature/envelope_test.go000066400000000000000000000111311466321423000220770ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signature import ( "reflect" "sync" "testing" ) var ( emptyFuncs sync.Map validFuncs sync.Map ) func init() { validFuncs.Store(testMediaType, envelopeFunc{ newFunc: testNewFunc, parseFunc: testParseFunc, }) } // mock an envelope that implements signature.Envelope. type testEnvelope struct { } // Sign implements Sign of signature.Envelope. func (e testEnvelope) Sign(req *SignRequest) ([]byte, error) { return nil, nil } // Verify implements Verify of signature.Envelope. func (e testEnvelope) Verify() (*EnvelopeContent, error) { return nil, nil } // Content implements Content of signature.Envelope. func (e testEnvelope) Content() (*EnvelopeContent, error) { return nil, nil } var ( testNewFunc = func() Envelope { return testEnvelope{} } testParseFunc = func([]byte) (Envelope, error) { return testEnvelope{}, nil } ) func TestRegisterEnvelopeType(t *testing.T) { tests := []struct { name string mediaType string newFunc NewEnvelopeFunc parseFunc ParseEnvelopeFunc expectErr bool }{ { name: "nil newFunc", mediaType: testMediaType, newFunc: nil, parseFunc: testParseFunc, expectErr: true, }, { name: "nil newParseFunc", mediaType: testMediaType, newFunc: testNewFunc, parseFunc: nil, expectErr: true, }, { name: "valid funcs", mediaType: testMediaType, newFunc: testNewFunc, parseFunc: testParseFunc, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := RegisterEnvelopeType(tt.mediaType, tt.newFunc, tt.parseFunc) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } }) } } func TestRegisteredEnvelopeTypes(t *testing.T) { tests := []struct { name string envelopeFuncs sync.Map expect []string }{ { name: "empty map", envelopeFuncs: emptyFuncs, expect: nil, }, { name: "nonempty map", envelopeFuncs: validFuncs, expect: []string{testMediaType}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { envelopeFuncs = tt.envelopeFuncs types := RegisteredEnvelopeTypes() if !reflect.DeepEqual(types, tt.expect) { t.Errorf("got types: %v, expect types: %v", types, tt.expect) } }) } } func TestNewEnvelope(t *testing.T) { tests := []struct { name string mediaType string envelopeFuncs sync.Map expect Envelope expectErr bool }{ { name: "unsupported media type", mediaType: testMediaType, envelopeFuncs: emptyFuncs, expect: nil, expectErr: true, }, { name: "valid media type", mediaType: testMediaType, envelopeFuncs: validFuncs, expect: testEnvelope{}, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { envelopeFuncs = tt.envelopeFuncs envelope, err := NewEnvelope(tt.mediaType) if (err != nil) != tt.expectErr { t.Errorf("got error: %v, expected error? %v", err, tt.expectErr) } if envelope != tt.expect { t.Errorf("got envelope: %v, expected envelope? %v", envelope, tt.expect) } }) } } func TestParseEnvelope(t *testing.T) { tests := []struct { name string mediaType string envelopeFuncs sync.Map expect Envelope expectErr bool }{ { name: "unsupported media type", mediaType: testMediaType, envelopeFuncs: emptyFuncs, expect: nil, expectErr: true, }, { name: "valid media type", mediaType: testMediaType, envelopeFuncs: validFuncs, expect: testEnvelope{}, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { envelopeFuncs = tt.envelopeFuncs envelope, err := ParseEnvelope(tt.mediaType, nil) if (err != nil) != tt.expectErr { t.Errorf("got error: %v, expected error? %v", err, tt.expectErr) } if envelope != tt.expect { t.Errorf("got envelope: %v, expected envelope? %v", envelope, tt.expect) } }) } } notation-core-go-1.1.0/signature/errors.go000066400000000000000000000113231466321423000205420ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signature import "fmt" // SignatureIntegrityError is used when the signature associated is no longer // valid. type SignatureIntegrityError struct { Err error } // Error returns the formatted error message. func (e *SignatureIntegrityError) Error() string { return fmt.Sprintf("signature is invalid. Error: %s", e.Err.Error()) } // Unwrap unwraps the internal error. func (e *SignatureIntegrityError) Unwrap() error { return e.Err } // InvalidSignatureError is used when Signature envelope is invalid. type InvalidSignatureError struct { Msg string } // Error returns the error message or the default message if not provided. func (e InvalidSignatureError) Error() string { if e.Msg != "" { return e.Msg } return "signature envelope format is invalid" } // UnsupportedSignatureFormatError is used when Signature envelope is not supported. type UnsupportedSignatureFormatError struct { MediaType string } // Error returns the formatted error message. func (e *UnsupportedSignatureFormatError) Error() string { return fmt.Sprintf("signature envelope format with media type %q is not supported", e.MediaType) } // SignatureNotFoundError is used when signature envelope is not present. type SignatureNotFoundError struct{} func (e SignatureNotFoundError) Error() string { return "signature envelope is not present" } // SignatureAuthenticityError is used when signature is not generated using // trusted certificates. type SignatureAuthenticityError struct{} // Error returns the default error message. func (e *SignatureAuthenticityError) Error() string { return "signature is not produced by a trusted signer" } // UnsupportedSigningKeyError is used when a signing key is not supported. type UnsupportedSigningKeyError struct { Msg string } // Error returns the error message or the default message if not provided. func (e UnsupportedSigningKeyError) Error() string { if e.Msg != "" { return e.Msg } return "signing key is not supported" } // InvalidArgumentError is used when an argument to a function is invalid. type InvalidArgumentError struct { Param string Err error } // Error returns the error message. func (e *InvalidArgumentError) Error() string { if e.Err != nil { return fmt.Sprintf("%q param is invalid. Error: %s", e.Param, e.Err.Error()) } return fmt.Sprintf("%q param is invalid", e.Param) } // Unwrap returns the unwrapped error. func (e *InvalidArgumentError) Unwrap() error { return e.Err } // InvalidSignRequestError is used when SignRequest is invalid. type InvalidSignRequestError struct { Msg string } // Error returns the error message or the default message if not provided. func (e *InvalidSignRequestError) Error() string { if e.Msg != "" { return e.Msg } return "SignRequest is invalid" } // UnsupportedSignatureAlgoError is used when signing algo is not supported. type UnsupportedSignatureAlgoError struct { Alg string } // Error returns the formatted error message. func (e *UnsupportedSignatureAlgoError) Error() string { return fmt.Sprintf("signature algorithm %q is not supported", e.Alg) } // SignatureEnvelopeNotFoundError is used when signature envelope is not present. type SignatureEnvelopeNotFoundError struct{} // Error returns the default error message. func (e *SignatureEnvelopeNotFoundError) Error() string { return "signature envelope is not present" } // DuplicateKeyError is used when repeated key name found. type DuplicateKeyError struct { Key string } // Error returns the formatted error message. func (e *DuplicateKeyError) Error() string { return fmt.Sprintf("repeated key: %q exists.", e.Key) } // TimestampError is any error related to RFC3161 Timestamp. type TimestampError struct { Msg string Detail error } // Error returns the formatted error message. func (e *TimestampError) Error() string { if e.Msg != "" && e.Detail != nil { return fmt.Sprintf("timestamp: %s. Error: %s", e.Msg, e.Detail.Error()) } if e.Msg != "" { return fmt.Sprintf("timestamp: %s", e.Msg) } if e.Detail != nil { return fmt.Sprintf("timestamp: %s", e.Detail.Error()) } return "timestamp error" } // Unwrap returns the detail error of e. func (e *TimestampError) Unwrap() error { return e.Detail } notation-core-go-1.1.0/signature/errors_test.go000066400000000000000000000132121466321423000216000ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signature import ( "errors" "fmt" "testing" ) const ( errMsg = "error msg" testParam = "test param" testAlg = "test algorithm" testMediaType = "test media type" ) func TestSignatureIntegrityError(t *testing.T) { unwrappedErr := errors.New(errMsg) err := &SignatureIntegrityError{ Err: unwrappedErr, } expectMsg := fmt.Sprintf("signature is invalid. Error: %s", errMsg) if err.Error() != expectMsg { t.Errorf("Expected %s but got %s", expectMsg, err.Error()) } if err.Unwrap() != unwrappedErr { t.Errorf("Expected %v but got %v", unwrappedErr, err.Unwrap()) } } func TestInvalidSignatureError(t *testing.T) { tests := []struct { name string err *InvalidSignatureError expect string }{ { name: "err msg set", err: &InvalidSignatureError{Msg: errMsg}, expect: errMsg, }, { name: "err msg not set", err: &InvalidSignatureError{}, expect: "signature envelope format is invalid", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { msg := tt.err.Error() if msg != tt.expect { t.Errorf("Expected %s but got %s", tt.expect, msg) } }) } } func TestUnsupportedSignatureFormatError(t *testing.T) { err := &UnsupportedSignatureFormatError{MediaType: testMediaType} expectMsg := fmt.Sprintf("signature envelope format with media type %q is not supported", testMediaType) if err.Error() != expectMsg { t.Errorf("Expected %v but got %v", expectMsg, err.Error()) } } func TestUnsupportedSigningKeyError(t *testing.T) { tests := []struct { name string err *UnsupportedSigningKeyError expect string }{ { name: "err msg set", err: &UnsupportedSigningKeyError{Msg: errMsg}, expect: errMsg, }, { name: "err msg not set", err: &UnsupportedSigningKeyError{}, expect: "signing key is not supported", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { msg := tt.err.Error() if msg != tt.expect { t.Errorf("Expected %s but got %s", tt.expect, msg) } }) } } func TestInvalidArgumentError(t *testing.T) { expectedMsg := "\"hola\" param is invalid" validateErrorMsg(&InvalidArgumentError{Param: "hola"}, expectedMsg, t) expectedMsg = "\"hola\" param is invalid. Error: se produjo un error" validateErrorMsg(&InvalidArgumentError{Param: "hola", Err: fmt.Errorf("se produjo un error")}, expectedMsg, t) } func TestUnsupportedSignatureAlgoError(t *testing.T) { err := &UnsupportedSignatureAlgoError{ Alg: testAlg, } expectMsg := fmt.Sprintf("signature algorithm %q is not supported", testAlg) if err.Error() != expectMsg { t.Errorf("Expected %s but got %s", expectMsg, err.Error()) } } func TestInvalidSignRequestError(t *testing.T) { expectedMsg := "SignRequest is invalid" validateErrorMsg(&InvalidSignRequestError{}, expectedMsg, t) expectedMsg = "Se produjo un error" validateErrorMsg(&InvalidSignRequestError{Msg: expectedMsg}, expectedMsg, t) } func validateErrorMsg(err error, expectedMsg string, t *testing.T) { foundMsg := err.Error() if expectedMsg != foundMsg { t.Errorf("Expected %q but found %q", expectedMsg, foundMsg) } } func TestInvalidArgumentError_Unwrap(t *testing.T) { err := &InvalidArgumentError{ Param: testParam, Err: errors.New(errMsg), } unwrappedErr := err.Unwrap() if unwrappedErr.Error() != errMsg { t.Errorf("Expected %s but got %s", errMsg, unwrappedErr.Error()) } } func TestSignatureEnvelopeNotFoundError(t *testing.T) { err := &SignatureEnvelopeNotFoundError{} expectMsg := "signature envelope is not present" if err.Error() != expectMsg { t.Errorf("Expected %v but got %v", expectMsg, err.Error()) } } func TestSignatureAuthenticityError(t *testing.T) { err := &SignatureAuthenticityError{} expectMsg := "signature is not produced by a trusted signer" if err.Error() != expectMsg { t.Errorf("Expected %v but got %v", expectMsg, err.Error()) } } func TestEnvelopeKeyRepeatedError(t *testing.T) { err := &DuplicateKeyError{Key: errMsg} expectMsg := fmt.Sprintf("repeated key: %q exists.", errMsg) if err.Error() != expectMsg { t.Errorf("Expected %v but got %v", expectMsg, err.Error()) } } func TestTimestampError(t *testing.T) { err := &TimestampError{Msg: "test error", Detail: errors.New("test inner error")} expectMsg := "timestamp: test error. Error: test inner error" if err.Error() != expectMsg { t.Errorf("Expected %v but got %v", expectMsg, err.Error()) } err = &TimestampError{Msg: "test error"} expectMsg = "timestamp: test error" if err.Error() != expectMsg { t.Errorf("Expected %v but got %v", expectMsg, err.Error()) } err = &TimestampError{Detail: errors.New("test inner error")} expectMsg = "timestamp: test inner error" if err.Error() != expectMsg { t.Errorf("Expected %v but got %v", expectMsg, err.Error()) } unwrappedErr := err.Unwrap() expectMsg = "test inner error" if unwrappedErr.Error() != expectMsg { t.Errorf("Expected %s but got %s", errMsg, unwrappedErr.Error()) } err = &TimestampError{} expectMsg = "timestamp error" if err.Error() != expectMsg { t.Errorf("Expected %v but got %v", expectMsg, err.Error()) } } notation-core-go-1.1.0/signature/internal/000077500000000000000000000000001466321423000205135ustar00rootroot00000000000000notation-core-go-1.1.0/signature/internal/base/000077500000000000000000000000001466321423000214255ustar00rootroot00000000000000notation-core-go-1.1.0/signature/internal/base/envelope.go000066400000000000000000000155751466321423000236060ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package base import ( "crypto/x509" "errors" "fmt" "time" "github.com/notaryproject/notation-core-go/signature" nx509 "github.com/notaryproject/notation-core-go/x509" ) // Envelope represents a general envelope wrapping a raw signature and envelope // in specific format. // Envelope manipulates the common validation shared by internal envelopes. type Envelope struct { signature.Envelope // internal envelope in a specific format (e.g. COSE, JWS) Raw []byte // raw signature } // Sign generates signature in terms of given SignRequest. // // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signing-and-verification-workflow.md#signing-steps func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) { // Canonicalize request. req.SigningTime = req.SigningTime.Truncate(time.Second) req.Expiry = req.Expiry.Truncate(time.Second) err := validateSignRequest(req) if err != nil { return nil, err } raw, err := e.Envelope.Sign(req) if err != nil { return nil, err } // validate certificate chain content, err := e.Envelope.Content() if err != nil { return nil, err } if err := validateCertificateChain( content.SignerInfo.CertificateChain, &content.SignerInfo.SignedAttributes.SigningTime, content.SignerInfo.SignatureAlgorithm, ); err != nil { return nil, err } // store the raw signature e.Raw = raw return e.Raw, nil } // Verify performs integrity and other signature specification related // validations. // It returns envelope content containing the payload to be signed and // SignerInfo object containing the information about the signature. // // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#steps func (e *Envelope) Verify() (*signature.EnvelopeContent, error) { // validation before the core verify process. if len(e.Raw) == 0 { return nil, &signature.SignatureNotFoundError{} } // core verify process. content, err := e.Envelope.Verify() if err != nil { return nil, err } // validation after the core verify process. if err = validateEnvelopeContent(content); err != nil { return nil, err } return content, nil } // Content returns the validated signature information and payload. func (e *Envelope) Content() (*signature.EnvelopeContent, error) { if len(e.Raw) == 0 { return nil, &signature.SignatureNotFoundError{} } content, err := e.Envelope.Content() if err != nil { return nil, err } if err = validateEnvelopeContent(content); err != nil { return nil, err } return content, nil } // validateSignRequest performs basic set of validations on SignRequest struct. func validateSignRequest(req *signature.SignRequest) error { if err := validatePayload(&req.Payload); err != nil { return &signature.InvalidSignRequestError{Msg: err.Error()} } if err := validateSigningAndExpiryTime(req.SigningTime, req.Expiry); err != nil { return err } if req.Signer == nil { return &signature.InvalidSignRequestError{Msg: "signer is nil"} } if _, err := req.Signer.KeySpec(); err != nil { return err } return validateSigningSchema(req.SigningScheme) } // validateSigningSchema validates the schema. func validateSigningSchema(schema signature.SigningScheme) error { if schema == "" { return &signature.InvalidSignRequestError{Msg: "SigningScheme not present"} } return nil } // validateEnvelopeContent validates the content which includes signerInfo and // payload. func validateEnvelopeContent(content *signature.EnvelopeContent) error { if err := validatePayload(&content.Payload); err != nil { return &signature.InvalidSignatureError{Msg: err.Error()} } return validateSignerInfo(&content.SignerInfo) } // validateSignerInfo performs basic set of validations on SignerInfo struct. func validateSignerInfo(info *signature.SignerInfo) error { if len(info.Signature) == 0 { return &signature.InvalidSignatureError{Msg: "signature not present or is empty"} } if info.SignatureAlgorithm == 0 { return &signature.InvalidSignatureError{Msg: "SignatureAlgorithm is not present"} } signingTime := info.SignedAttributes.SigningTime if err := validateSigningAndExpiryTime(signingTime, info.SignedAttributes.Expiry); err != nil { return err } if err := validateSigningSchema(info.SignedAttributes.SigningScheme); err != nil { return err } return validateCertificateChain( info.CertificateChain, nil, info.SignatureAlgorithm, ) } // validateSigningAndExpiryTime checks that signing time is within the valid // range of time duration and expire time is valid. func validateSigningAndExpiryTime(signingTime, expireTime time.Time) error { if signingTime.IsZero() { return &signature.InvalidSignatureError{Msg: "signing-time not present"} } if !expireTime.IsZero() && (expireTime.Before(signingTime) || expireTime.Equal(signingTime)) { return &signature.InvalidSignatureError{Msg: "expiry cannot be equal or before the signing time"} } return nil } // validatePayload performs validation of the payload. func validatePayload(payload *signature.Payload) error { if len(payload.Content) == 0 { return errors.New("content not present") } return nil } // validateCertificateChain performs the validation of the certificate chain. func validateCertificateChain(certChain []*x509.Certificate, signTime *time.Time, expectedAlg signature.Algorithm) error { if len(certChain) == 0 { return &signature.InvalidSignatureError{Msg: "certificate-chain not present or is empty"} } err := nx509.ValidateCodeSigningCertChain(certChain, signTime) if err != nil { return &signature.InvalidSignatureError{ Msg: fmt.Sprintf("certificate-chain is invalid, %s", err), } } signingAlg, err := getSignatureAlgorithm(certChain[0]) if err != nil { return &signature.InvalidSignatureError{Msg: err.Error()} } if signingAlg != expectedAlg { return &signature.InvalidSignatureError{ Msg: fmt.Sprintf("mismatch between signature algorithm derived from signing certificate (%v) and signing algorithm specified (%vs)", signingAlg, expectedAlg), } } return nil } // getSignatureAlgorithm picks up a recommended signing algorithm for given // certificate. func getSignatureAlgorithm(signingCert *x509.Certificate) (signature.Algorithm, error) { keySpec, err := signature.ExtractKeySpec(signingCert) if err != nil { return 0, err } return keySpec.SignatureAlgorithm(), nil } notation-core-go-1.1.0/signature/internal/base/envelope_test.go000066400000000000000000000443651466321423000246440ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package base import ( "crypto/x509" "errors" "reflect" "testing" "time" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/testhelper" "github.com/notaryproject/tspclient-go" ) var ( errMsg = "error msg" invalidTimestamper tspclient.Timestamper invalidSigningAgent = "test/1" validSigningAgent = "test/0" invalidContentType = "text/plain" validContentType = "application/vnd.cncf.notary.payload.v1+json" validContent = "test content" validBytes = []byte(validContent) time08_02 time.Time time08_03 time.Time timeLayout = "2006-01-02" signiningSchema = signature.SigningScheme("notary.x509") validSignerInfo = &signature.SignerInfo{ Signature: validBytes, SignatureAlgorithm: signature.AlgorithmPS384, SignedAttributes: signature.SignedAttributes{ SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, Expiry: testhelper.GetECLeafCertificate().Cert.NotAfter, SigningScheme: signiningSchema, }, CertificateChain: []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert, }, } validPayload = &signature.Payload{ ContentType: validContentType, Content: validBytes, } validEnvelopeContent = &signature.EnvelopeContent{ SignerInfo: *validSignerInfo, Payload: *validPayload, } validReq = &signature.SignRequest{ Payload: signature.Payload{ ContentType: validContentType, Content: validBytes, }, SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, SigningScheme: signiningSchema, Signer: &mockSigner{ keySpec: signature.KeySpec{ Type: signature.KeyTypeRSA, Size: 3072, }, certs: []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert, }, }, SigningAgent: validSigningAgent, } signReq1 = &signature.SignRequest{ Payload: signature.Payload{ ContentType: validContentType, Content: validBytes, }, SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, SigningScheme: signiningSchema, Signer: &mockSigner{ keySpec: signature.KeySpec{ Type: signature.KeyTypeRSA, Size: 3072, }, certs: []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert, }, }, SigningAgent: invalidSigningAgent, } reqWithInvalidTSAurl = &signature.SignRequest{ Payload: signature.Payload{ ContentType: validContentType, Content: validBytes, }, SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter, SigningScheme: signiningSchema, Signer: &mockSigner{ keySpec: signature.KeySpec{ Type: signature.KeyTypeRSA, Size: 3072, }, certs: []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert, }, }, SigningAgent: validSigningAgent, Timestamper: invalidTimestamper, } ) func init() { time08_02, _ = time.Parse(timeLayout, "2020-08-02") time08_03, _ = time.Parse(timeLayout, "2020-08-03") invalidTimestamper, _ = tspclient.NewHTTPTimestamper(nil, "invalid") } // Mock an internal envelope that implements signature.Envelope. type mockEnvelope struct { payload *signature.Payload signerInfo *signature.SignerInfo content *signature.EnvelopeContent failTimestamp bool failVerify bool } // Sign implements Sign of signature.Envelope. func (e mockEnvelope) Sign(req *signature.SignRequest) ([]byte, error) { switch req.SigningAgent { case invalidSigningAgent: return nil, errors.New(errMsg) case validSigningAgent: if e.failTimestamp { return validBytes, &signature.TimestampError{} } return validBytes, nil } return nil, nil } // Verify implements Verify of signature.Envelope. func (e mockEnvelope) Verify() (*signature.EnvelopeContent, error) { if e.failVerify { return nil, errors.New(errMsg) } return e.content, nil } // SignerInfo implements SignerInfo of signature.Envelope. func (e mockEnvelope) Content() (*signature.EnvelopeContent, error) { if e.content == nil { return nil, errors.New(errMsg) } return e.content, nil } // Mock a signer implements signature.Signer. type mockSigner struct { certs []*x509.Certificate keySpec signature.KeySpec } // CertificateChain implements CertificateChain of signature.Signer. func (s *mockSigner) CertificateChain() ([]*x509.Certificate, error) { if len(s.certs) == 0 { return nil, errors.New(errMsg) } return s.certs, nil } // Sign implements Sign of signature.Signer. func (s *mockSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { return nil, nil, nil } // KeySpec implements KeySpec of signature.Signer. func (s *mockSigner) KeySpec() (signature.KeySpec, error) { var emptyKeySpec signature.KeySpec if s.keySpec == emptyKeySpec { return s.keySpec, errors.New(errMsg) } return s.keySpec, nil } func TestSign(t *testing.T) { tests := []struct { name string req *signature.SignRequest env *Envelope expect []byte expectErr bool }{ { name: "invalid request", req: &signature.SignRequest{ SigningTime: time08_02, Expiry: time08_02, }, env: &Envelope{ Raw: nil, Envelope: mockEnvelope{}, }, expect: nil, expectErr: true, }, { name: "internal envelope fails to sign", req: signReq1, env: &Envelope{ Raw: nil, Envelope: mockEnvelope{}, }, expect: nil, expectErr: true, }, { name: "internal envelope fails to get content", req: validReq, env: &Envelope{ Raw: nil, Envelope: mockEnvelope{}, }, expect: nil, expectErr: true, }, { name: "invalid certificate chain", req: validReq, env: &Envelope{ Raw: nil, Envelope: mockEnvelope{ content: &signature.EnvelopeContent{}, }, }, expect: nil, expectErr: true, }, { name: "successfully signed", req: validReq, env: &Envelope{ Raw: validBytes, Envelope: &mockEnvelope{ content: &signature.EnvelopeContent{ SignerInfo: *validSignerInfo, }, }, }, expect: validBytes, expectErr: false, }, { name: "failed to timestamp", req: reqWithInvalidTSAurl, env: &Envelope{ Raw: validBytes, Envelope: &mockEnvelope{ content: &signature.EnvelopeContent{ SignerInfo: *validSignerInfo, }, failTimestamp: true, }, }, expect: nil, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sig, err := tt.env.Sign(tt.req) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } if !reflect.DeepEqual(sig, tt.expect) { t.Errorf("expect %+v, got %+v", tt.expect, sig) } if tt.name == "failed to timestamp" { var timestampErr *signature.TimestampError if !errors.As(err, ×tampErr) { t.Fatal("expecting error to be signature.TimestampError") } expectedErrMsg := "timestamp error" if timestampErr.Error() != expectedErrMsg { t.Fatalf("expected error %s, but got %v", expectedErrMsg, err) } } }) } } func TestVerify(t *testing.T) { tests := []struct { name string env *Envelope expectContent *signature.EnvelopeContent expectErr bool }{ { name: "empty raw", env: &Envelope{}, expectContent: nil, expectErr: true, }, { name: "err returned by internal envelope", env: &Envelope{ Raw: validBytes, Envelope: &mockEnvelope{ failVerify: true, payload: validPayload, }, }, expectContent: nil, expectErr: true, }, { name: "payload validation failed after internal envelope verfication", env: &Envelope{ Raw: validBytes, Envelope: &mockEnvelope{ content: &signature.EnvelopeContent{ Payload: signature.Payload{ ContentType: invalidContentType, }, }, }, }, expectContent: nil, expectErr: true, }, { name: "signerInfo validation failed after internal envelope verfication", env: &Envelope{ Raw: validBytes, Envelope: &mockEnvelope{ content: &signature.EnvelopeContent{ Payload: *validPayload, SignerInfo: signature.SignerInfo{}, }, }, }, expectContent: nil, expectErr: true, }, { name: "verify successfully", env: &Envelope{ Raw: validBytes, Envelope: &mockEnvelope{ content: &signature.EnvelopeContent{ Payload: *validPayload, SignerInfo: *validSignerInfo, }, }, }, expectContent: &signature.EnvelopeContent{ Payload: *validPayload, SignerInfo: *validSignerInfo, }, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { content, err := tt.env.Verify() if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } if !reflect.DeepEqual(content, tt.expectContent) { t.Errorf("expect content: %+v, got %+v", tt.expectContent, content) } }) } } func TestContent(t *testing.T) { tests := []struct { name string env *Envelope expect *signature.EnvelopeContent expectErr bool }{ { name: "empty raw", env: &Envelope{}, expect: nil, expectErr: true, }, { name: "err returned by internal envelope", env: &Envelope{ Raw: validBytes, Envelope: &mockEnvelope{}, }, expect: nil, expectErr: true, }, { name: "invalid payload", env: &Envelope{ Raw: validBytes, Envelope: &mockEnvelope{ content: &signature.EnvelopeContent{ Payload: signature.Payload{}, }, }, }, expect: nil, expectErr: true, }, { name: "valid payload and invalid signerInfo", env: &Envelope{ Raw: validBytes, Envelope: &mockEnvelope{ content: &signature.EnvelopeContent{ Payload: *validPayload, }, signerInfo: &signature.SignerInfo{}, }, }, expect: nil, expectErr: true, }, { name: "valid payload and valid signerInfo", env: &Envelope{ Raw: validBytes, Envelope: &mockEnvelope{ content: &signature.EnvelopeContent{ Payload: *validPayload, SignerInfo: *validSignerInfo, }, }, }, expect: validEnvelopeContent, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { content, err := tt.env.Content() if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } if !reflect.DeepEqual(content, tt.expect) { t.Errorf("expect %+v, got %+v", tt.expect, content) } }) } } func TestValidateSignRequest(t *testing.T) { tests := []struct { name string req *signature.SignRequest expectErr bool }{ { name: "invalid payload", req: &signature.SignRequest{}, expectErr: true, }, { name: "invalid signing time", req: &signature.SignRequest{ Payload: signature.Payload{ ContentType: validContentType, Content: validBytes, }, }, expectErr: true, }, { name: "signer is nil", req: &signature.SignRequest{ Payload: signature.Payload{ ContentType: validContentType, Content: validBytes, }, SigningTime: time08_02, Expiry: time08_03, }, expectErr: true, }, { name: "keySpec is empty", req: &signature.SignRequest{ Payload: signature.Payload{ ContentType: validContentType, Content: validBytes, }, SigningTime: time08_02, Expiry: time08_03, SigningScheme: signiningSchema, Signer: &mockSigner{ certs: []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert, }, keySpec: signature.KeySpec{}, }, }, expectErr: true, }, { name: "invalid signing schema", req: &signature.SignRequest{ Payload: signature.Payload{ ContentType: validContentType, Content: validBytes, }, SigningTime: time08_02, Expiry: time08_03, Signer: &mockSigner{ keySpec: signature.KeySpec{ Type: signature.KeyTypeRSA, Size: 3072, }, certs: []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert, }, }, }, expectErr: true, }, { name: "valid request", req: validReq, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateSignRequest(tt.req) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } }) } } func TestValidateSignerInfo(t *testing.T) { tests := []struct { name string info *signature.SignerInfo expectErr bool }{ { name: "empty signature", info: &signature.SignerInfo{}, expectErr: true, }, { name: "missing signature algorithm", info: &signature.SignerInfo{ Signature: validBytes, }, expectErr: true, }, { name: "invalid signing time", info: &signature.SignerInfo{ Signature: validBytes, SignatureAlgorithm: signature.AlgorithmPS256, }, expectErr: true, }, { name: "invalid signing schema", info: &signature.SignerInfo{ Signature: validBytes, SignatureAlgorithm: signature.AlgorithmPS384, SignedAttributes: signature.SignedAttributes{ SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, Expiry: testhelper.GetECLeafCertificate().Cert.NotAfter, }, }, expectErr: true, }, { name: "valid signerInfo", info: validSignerInfo, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateSignerInfo(tt.info) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } }) } } func TestValidateSigningTime(t *testing.T) { tests := []struct { name string signingTime time.Time expireTime time.Time expectErr bool }{ { name: "zero signing time", signingTime: time.Time{}, expireTime: time.Now(), expectErr: true, }, { name: "no expire time", signingTime: time.Now(), expireTime: time.Time{}, expectErr: false, }, { name: "expireTime set but invalid", signingTime: time08_03, expireTime: time08_02, expectErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateSigningAndExpiryTime(tt.signingTime, tt.expireTime) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } }) } } func TestValidatePayload(t *testing.T) { tests := []struct { name string payload *signature.Payload expectErr bool }{ { name: "invalid payload content type", payload: &signature.Payload{ ContentType: invalidContentType, }, expectErr: true, }, { name: "payload content is empty", payload: &signature.Payload{ ContentType: validContentType, Content: []byte{}, }, expectErr: true, }, { name: "valid payload", payload: validPayload, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validatePayload(tt.payload) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } }) } } func TestValidateCertificateChain(t *testing.T) { tests := []struct { name string certs []*x509.Certificate signTime time.Time alg signature.Algorithm expectErr bool }{ { name: "empty certs", certs: []*x509.Certificate{}, signTime: time.Now(), alg: signature.AlgorithmES256, expectErr: true, }, { name: "invalid certificates", certs: []*x509.Certificate{ testhelper.GetECLeafCertificate().Cert, }, signTime: time.Now(), alg: signature.AlgorithmES256, expectErr: true, }, { name: "unmatched signing algorithm", certs: []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert, }, signTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, alg: signature.AlgorithmPS256, expectErr: true, }, { name: "valid certificate chain", certs: []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert, }, signTime: testhelper.GetRSALeafCertificate().Cert.NotBefore, alg: signature.AlgorithmPS384, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateCertificateChain(tt.certs, &tt.signTime, tt.alg) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } }) } } func TestGetSignatureAlgorithm(t *testing.T) { tests := []struct { name string cert *x509.Certificate expect signature.Algorithm expectErr bool }{ { name: "unsupported cert", cert: testhelper.GetUnsupportedRSACert().Cert, expect: 0, expectErr: true, }, { name: "valid cert", cert: testhelper.GetRSALeafCertificate().Cert, expect: signature.AlgorithmPS384, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { alg, err := getSignatureAlgorithm(tt.cert) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } if !reflect.DeepEqual(alg, tt.expect) { t.Errorf("expect %+v, got %+v", tt.expect, alg) } }) } } notation-core-go-1.1.0/signature/internal/signaturetest/000077500000000000000000000000001466321423000234145ustar00rootroot00000000000000notation-core-go-1.1.0/signature/internal/signaturetest/algorithm.go000066400000000000000000000022171466321423000257330ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package signaturetest includes variables and functions for signature // unit test. package signaturetest import "github.com/notaryproject/notation-core-go/signature" // KeyTypes contains supported key type var KeyTypes = []signature.KeyType{signature.KeyTypeRSA, signature.KeyTypeEC} // GetKeySizes returns the supported key size for the named keyType func GetKeySizes(keyType signature.KeyType) []int { switch keyType { case signature.KeyTypeRSA: return []int{2048, 3072, 4096} case signature.KeyTypeEC: return []int{256, 384, 521} default: return nil } } notation-core-go-1.1.0/signature/internal/signaturetest/algorithm_test.go000066400000000000000000000025651466321423000270000ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package signaturetest includes variables and functions for signature // unit test. package signaturetest import ( "reflect" "testing" "github.com/notaryproject/notation-core-go/signature" ) func TestGetKeySizes(t *testing.T) { type args struct { keyType signature.KeyType } tests := []struct { name string args args want []int }{ {name: "RSA", args: args{keyType: signature.KeyTypeRSA}, want: []int{2048, 3072, 4096}}, {name: "EC", args: args{keyType: signature.KeyTypeEC}, want: []int{256, 384, 521}}, {name: "others", args: args{keyType: -1}, want: nil}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := GetKeySizes(tt.args.keyType); !reflect.DeepEqual(got, tt.want) { t.Errorf("GetKeySizes() = %v, want %v", got, tt.want) } }) } } notation-core-go-1.1.0/signature/internal/signaturetest/signer.go000066400000000000000000000051271466321423000252370ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signaturetest import ( "crypto/elliptic" "crypto/x509" "fmt" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/testhelper" ) // GetTestLocalSigner returns the local signer with given keyType and size for testing func GetTestLocalSigner(keyType signature.KeyType, size int) (signature.Signer, error) { switch keyType { case signature.KeyTypeEC: switch size { case 256: leafCertTuple := testhelper.GetECCertTuple(elliptic.P256()) certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetECRootCertificate().Cert} return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) case 384: leafCertTuple := testhelper.GetECCertTuple(elliptic.P384()) certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetECRootCertificate().Cert} return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) case 521: leafCertTuple := testhelper.GetECCertTuple(elliptic.P521()) certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetECRootCertificate().Cert} return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) default: return nil, fmt.Errorf("key size not supported") } case signature.KeyTypeRSA: switch size { case 2048: leafCertTuple := testhelper.GetRSACertTuple(2048) certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetRSARootCertificate().Cert} return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) case 3072: leafCertTuple := testhelper.GetRSACertTuple(3072) certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetRSARootCertificate().Cert} return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) case 4096: leafCertTuple := testhelper.GetRSACertTuple(4096) certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetRSARootCertificate().Cert} return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) default: return nil, fmt.Errorf("key size not supported") } default: return nil, fmt.Errorf("keyType not supported") } } notation-core-go-1.1.0/signature/internal/signaturetest/signer_test.go000066400000000000000000000036251466321423000262770ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signaturetest import ( "testing" "github.com/notaryproject/notation-core-go/signature" ) func TestGetTestLocalSigner(t *testing.T) { type args struct { keyType signature.KeyType size int } tests := []struct { name string args args wantErr bool }{ {name: "EC256", args: args{keyType: signature.KeyTypeEC, size: 256}, wantErr: false}, {name: "EC384", args: args{keyType: signature.KeyTypeEC, size: 384}, wantErr: false}, {name: "EC521", args: args{keyType: signature.KeyTypeEC, size: 521}, wantErr: false}, {name: "ECOthers", args: args{keyType: signature.KeyTypeEC, size: 520}, wantErr: true}, {name: "RSA2048", args: args{keyType: signature.KeyTypeRSA, size: 2048}, wantErr: false}, {name: "RSA3072", args: args{keyType: signature.KeyTypeRSA, size: 3072}, wantErr: false}, {name: "RSA4096", args: args{keyType: signature.KeyTypeRSA, size: 4096}, wantErr: false}, {name: "RSAOthers", args: args{keyType: signature.KeyTypeRSA, size: 4097}, wantErr: true}, {name: "Others", args: args{keyType: -1, size: 4097}, wantErr: true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := GetTestLocalSigner(tt.args.keyType, tt.args.size) if (err != nil) != tt.wantErr { t.Errorf("GetTestLocalSigner() error = %v, wantErr %v", err, tt.wantErr) return } }) } } notation-core-go-1.1.0/signature/jws/000077500000000000000000000000001466321423000175025ustar00rootroot00000000000000notation-core-go-1.1.0/signature/jws/conformance_test.go000066400000000000000000000150161466321423000233650ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package jws import ( "crypto/elliptic" "crypto/x509" "encoding/json" "os" "reflect" "sort" "strings" "testing" "time" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/testhelper" ) var ( // prepare signing time signingTime, _ = time.Parse("2006-01-02 15:04:05", "2022-08-29 13:50:00") expiry, _ = time.Parse("2006-01-02 15:04:05", "2099-08-29 13:50:00") // signedAttributes for signing request signedAttributes = signature.SignedAttributes{ SigningScheme: "notary.x509", SigningTime: signingTime, Expiry: expiry.Add(time.Hour * 24), ExtendedAttributes: sortAttributes([]signature.Attribute{ {Key: "signedCritKey1", Value: "signedCritValue1", Critical: true}, {Key: "signedKey1", Value: "signedValue1", Critical: false}, {Key: "signedKey2", Value: "signedValue1", Critical: false}, {Key: "signedKey3", Value: "signedValue1", Critical: false}, {Key: "signedKey4", Value: "signedValue1", Critical: false}, }), } // unsignedAttributes for signing request unsignedAttributes = signature.UnsignedAttributes{ SigningAgent: "NotationConformanceTest/1.0.0", } // payload to be signed payload = signature.Payload{ ContentType: "application/vnd.cncf.notary.payload.v1+json", Content: []byte(`{"key":"hello JWS"}`), } // certificate chain for signer leafCertTuple = testhelper.GetECCertTuple(elliptic.P256()) certs = []*x509.Certificate{leafCertTuple.Cert, testhelper.GetECRootCertificate().Cert} ) func conformanceTestSignReq() *signature.SignRequest { signer, err := signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) if err != nil { panic(err) } return &signature.SignRequest{ Payload: payload, Signer: signer, SigningTime: signedAttributes.SigningTime, Expiry: signedAttributes.Expiry, ExtendedSignedAttributes: signedAttributes.ExtendedAttributes, SigningAgent: unsignedAttributes.SigningAgent, SigningScheme: signedAttributes.SigningScheme, } } // TestSignedMessageConformance check the conformance between the encoded message // and the valid encoded message in conformance.json // // check payload, protected and signingAgent section func TestSignedMessageConformance(t *testing.T) { // get encoded message env := envelope{} signReq := conformanceTestSignReq() encoded, err := env.Sign(signReq) checkNoError(t, err) // parse encoded message to be a map envMap, err := unmarshalEncodedMessage(encoded) checkNoError(t, err) // load validation encoded message validEnvMap, err := getValidEnvelopeMap() checkNoError(t, err) // check payload section conformance if !reflect.DeepEqual(envMap["payload"], validEnvMap["payload"]) { t.Fatal("signed message payload test failed.") } // check protected section conformance if !reflect.DeepEqual(envMap["protected"], validEnvMap["protected"]) { t.Fatal("signed message protected test failed.") } // prepare header header, ok := envMap["header"].(map[string]interface{}) if !ok { t.Fatal("signed message header format error.") } validHeader, ok := validEnvMap["header"].(map[string]interface{}) if !ok { t.Fatal("conformance.json header format error.") } // check io.cncf.notary.signingAgent conformance if !reflect.DeepEqual(header["io.cncf.notary.signingAgent"], validHeader["io.cncf.notary.signingAgent"]) { t.Fatal("signed message signingAgent test failed.") } } func getValidEnvelopeMap() (map[string]interface{}, error) { encoded, err := os.ReadFile("./testdata/conformance.json") if err != nil { return nil, err } return unmarshalEncodedMessage(encoded) } func unmarshalEncodedMessage(encoded []byte) (envelopeMap map[string]interface{}, err error) { err = json.Unmarshal(encoded, &envelopeMap) return } // TestVerifyConformance generates JWS encoded message, parses the encoded message and // verify the payload, signed/unsigned attributes conformance. func TestVerifyConformance(t *testing.T) { env := envelope{} signReq := conformanceTestSignReq() encoded, err := env.Sign(signReq) checkNoError(t, err) // parse envelope var e jwsEnvelope err = json.Unmarshal(encoded, &e) checkNoError(t, err) newEnv := envelope{base: &e} // verify validity content, err := newEnv.Verify() checkNoError(t, err) // check payload conformance verifyPayload(t, &content.Payload) // check signed/unsigned attributes conformance verifyAttributes(t, &content.SignerInfo) } func verifyPayload(t *testing.T, gotPayload *signature.Payload) { if !reflect.DeepEqual(&payload, gotPayload) { t.Fatalf("verify payload failed. want: %+v got: %+v\n", &payload, gotPayload) } } func verifyAttributes(t *testing.T, signerInfo *signature.SignerInfo) { // check unsigned attributes if !reflect.DeepEqual(&unsignedAttributes, &signerInfo.UnsignedAttributes) { t.Fatalf("verify UnsignedAttributes failed. want: %+v got: %+v\n", &unsignedAttributes, &signerInfo.UnsignedAttributes) } // check signed attributes sortAttributes(signerInfo.SignedAttributes.ExtendedAttributes) if !reflect.DeepEqual(&signedAttributes, &signerInfo.SignedAttributes) { t.Fatalf("verify SignedAttributes failed. want: %+v got: %+v\n", &signedAttributes, &signerInfo.SignedAttributes) } // check signature algorithm keySpec, err := signature.ExtractKeySpec(certs[0]) checkNoError(t, err) if keySpec.SignatureAlgorithm() != signerInfo.SignatureAlgorithm { t.Fatalf("verify signature algorithm failed. want: %d got: %d\n", keySpec.SignatureAlgorithm(), signerInfo.SignatureAlgorithm) } // check certificate chain if !reflect.DeepEqual(signerInfo.CertificateChain, certs) { t.Fatalf("verify certificate chain failed. want: %+v got: %+v\n", &signerInfo.CertificateChain, certs) } } func sortAttributes(attributes []signature.Attribute) []signature.Attribute { sort.Slice(attributes, func(i, j int) bool { key1, key2 := attributes[i].Key.(string), attributes[j].Key.(string) return strings.Compare(key1, key2) < 0 }) return attributes } notation-core-go-1.1.0/signature/jws/envelope.go000066400000000000000000000170731466321423000216560ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package jws import ( "crypto/x509" "encoding/base64" "encoding/json" "fmt" "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-core-go/internal/timestamp" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/base" "github.com/notaryproject/tspclient-go" ) // MediaTypeEnvelope defines the media type name of JWS envelope. const MediaTypeEnvelope = "application/jose+json" func init() { if err := signature.RegisterEnvelopeType(MediaTypeEnvelope, NewEnvelope, ParseEnvelope); err != nil { panic(err) } } type envelope struct { base *jwsEnvelope } // NewEnvelope generates an JWS envelope. func NewEnvelope() signature.Envelope { return &base.Envelope{ Envelope: &envelope{}, } } // ParseEnvelope parses the envelope bytes and return a JWS envelope. func ParseEnvelope(envelopeBytes []byte) (signature.Envelope, error) { var e jwsEnvelope err := json.Unmarshal(envelopeBytes, &e) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } return &base.Envelope{ Envelope: &envelope{base: &e}, Raw: envelopeBytes, }, nil } // Sign generates and sign the envelope according to the sign request. func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) { // get signingMethod for JWT package method, err := getSigningMethod(req.Signer) if err != nil { return nil, &signature.InvalidSignRequestError{Msg: err.Error()} } // get all attributes ready to be signed signedAttrs, err := getSignedAttributes(req, method.Alg()) if err != nil { return nil, &signature.InvalidSignRequestError{Msg: err.Error()} } // parse payload as jwt.MapClaims // [jwt-go]: https://pkg.go.dev/github.com/dgrijalva/jwt-go#MapClaims var payload jwt.MapClaims if err = json.Unmarshal(req.Payload.Content, &payload); err != nil { return nil, &signature.InvalidSignRequestError{ Msg: fmt.Sprintf("payload format error: %v", err.Error())} } // JWT sign and get certificate chain compact, certs, err := sign(payload, signedAttrs, method) if err != nil { return nil, &signature.InvalidSignRequestError{Msg: err.Error()} } // generate envelope env, err := generateJWS(compact, req, certs) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } // timestamping if err := timestampJWS(env, req, signedAttrs[headerKeySigningScheme].(string)); err != nil { return nil, err } encoded, err := json.Marshal(env) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } e.base = env return encoded, nil } // Verify verifies the envelope and returns its enclosed payload and signer info. func (e *envelope) Verify() (*signature.EnvelopeContent, error) { if e.base == nil { return nil, &signature.SignatureEnvelopeNotFoundError{} } if len(e.base.Header.CertChain) == 0 { return nil, &signature.InvalidSignatureError{Msg: "certificate chain is not present"} } cert, err := x509.ParseCertificate(e.base.Header.CertChain[0]) if err != nil { return nil, &signature.InvalidSignatureError{Msg: "malformed leaf certificate"} } // verify JWT compact := compactJWS(e.base) if err = verifyJWT(compact, cert.PublicKey); err != nil { return nil, err } return e.Content() } // Content returns the payload and signer information of the envelope. // Content is trusted only after the successful call to `Verify()`. func (e *envelope) Content() (*signature.EnvelopeContent, error) { if e.base == nil { return nil, &signature.SignatureEnvelopeNotFoundError{} } // parse protected headers protected, err := parseProtectedHeaders(e.base.Protected) if err != nil { return nil, err } // extract payload payload, err := e.payload(protected) if err != nil { return nil, err } // extract signer info signerInfo, err := e.signerInfo(protected) if err != nil { return nil, err } return &signature.EnvelopeContent{ SignerInfo: *signerInfo, Payload: *payload, }, nil } // payload returns the payload of JWS envelope. func (e *envelope) payload(protected *jwsProtectedHeader) (*signature.Payload, error) { payload, err := base64.RawURLEncoding.DecodeString(e.base.Payload) if err != nil { return nil, &signature.InvalidSignatureError{ Msg: fmt.Sprintf("payload error: %v", err)} } return &signature.Payload{ Content: payload, ContentType: protected.ContentType, }, nil } // signerInfo returns the SignerInfo of JWS envelope. func (e *envelope) signerInfo(protected *jwsProtectedHeader) (*signature.SignerInfo, error) { var signerInfo signature.SignerInfo // populate protected header to signerInfo if err := populateProtectedHeaders(protected, &signerInfo); err != nil { return nil, err } // parse signature sig, err := base64.RawURLEncoding.DecodeString(e.base.Signature) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } if len(sig) == 0 { return nil, &signature.InvalidSignatureError{Msg: "signature missing in jws-json envelope"} } signerInfo.Signature = sig // parse headers var certs []*x509.Certificate for _, certBytes := range e.base.Header.CertChain { cert, err := x509.ParseCertificate(certBytes) if err != nil { return nil, &signature.InvalidSignatureError{Msg: err.Error()} } certs = append(certs, cert) } signerInfo.CertificateChain = certs signerInfo.UnsignedAttributes.SigningAgent = e.base.Header.SigningAgent signerInfo.UnsignedAttributes.TimestampSignature = e.base.Header.TimestampSignature return &signerInfo, nil } // sign the given payload and headers using the given signature provider. func sign(payload jwt.MapClaims, headers map[string]interface{}, method signingMethod) (string, []*x509.Certificate, error) { // generate token token := jwt.NewWithClaims(method, payload) token.Header = headers // sign and return compact JWS compact, err := token.SignedString(method.PrivateKey()) if err != nil { return "", nil, err } // access certificate chain after sign certs, err := method.CertificateChain() if err != nil { return "", nil, err } return compact, certs, nil } // timestampJWS timestamps a JWS envelope func timestampJWS(env *jwsEnvelope, req *signature.SignRequest, signingScheme string) error { if signingScheme != string(signature.SigningSchemeX509) || req.Timestamper == nil { return nil } primitiveSignature, err := base64.RawURLEncoding.DecodeString(env.Signature) if err != nil { return &signature.TimestampError{Detail: err} } ks, err := req.Signer.KeySpec() if err != nil { return &signature.TimestampError{Detail: err} } hash := ks.SignatureAlgorithm().Hash() if hash == 0 { return &signature.TimestampError{Msg: fmt.Sprintf("got hash value 0 from key spec %+v", ks)} } timestampOpts := tspclient.RequestOptions{ Content: primitiveSignature, HashAlgorithm: hash, } timestampToken, err := timestamp.Timestamp(req, timestampOpts) if err != nil { return &signature.TimestampError{Detail: err} } // on success, embed the timestamp token to TimestampSignature env.Header.TimestampSignature = timestampToken return nil } notation-core-go-1.1.0/signature/jws/envelope_test.go000066400000000000000000000471761466321423000227240ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package jws import ( "crypto" "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" "math" "reflect" "strings" "testing" "time" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature/internal/signaturetest" "github.com/notaryproject/notation-core-go/testhelper" nx509 "github.com/notaryproject/notation-core-go/x509" "github.com/notaryproject/tspclient-go" ) const rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" // remoteMockSigner is used to mock remote signer type remoteMockSigner struct { privateKey crypto.PrivateKey certs []*x509.Certificate } // Sign signs the digest and returns the raw signature func (signer *remoteMockSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { // calculate hash keySpec, err := signer.KeySpec() if err != nil { return nil, nil, err } // calculate hash hasher := keySpec.SignatureAlgorithm().Hash().HashFunc() h := hasher.New() h.Write(payload) hash := h.Sum(nil) // sign switch key := signer.privateKey.(type) { case *rsa.PrivateKey: sig, err := rsa.SignPSS(rand.Reader, key, hasher.HashFunc(), hash, &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}) if err != nil { return nil, nil, err } return sig, signer.certs, nil case *ecdsa.PrivateKey: r, s, err := ecdsa.Sign(rand.Reader, key, hash) if err != nil { return nil, nil, err } curveBits := key.Curve.Params().BitSize keyBytes := curveBits / 8 if curveBits%8 > 0 { keyBytes += 1 } out := make([]byte, 2*keyBytes) r.FillBytes(out[0:keyBytes]) // r is assigned to the first half of output. s.FillBytes(out[keyBytes:]) // s is assigned to the second half of output. return out, signer.certs, nil } return nil, nil, &signature.UnsupportedSigningKeyError{} } // KeySpec returns the key specification func (signer *remoteMockSigner) KeySpec() (signature.KeySpec, error) { return signature.ExtractKeySpec(signer.certs[0]) } func checkNoError(t *testing.T, err error) { if err != nil { t.Fatal(err) } } func checkErrorEqual(t *testing.T, want, got string) { if !strings.Contains(got, want) { t.Fatalf("want: %v got: %v\n", want, got) } } var ( extSignedAttr = []signature.Attribute{ { Key: "testKey", Critical: true, Value: "testValue", }, { Key: "testKey2", Critical: false, Value: "testValue2", }, } extSignedAttrRepeated = []signature.Attribute{ { Key: "cty", Critical: false, Value: "testValue2", }, } extSignedAttrErrorValue = []signature.Attribute{ { Key: "add", Critical: false, Value: math.Inf(1), }, } ) func getSigningCerts() []*x509.Certificate { return []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert} } func getSignReq(signingScheme signature.SigningScheme, signer signature.Signer, extendedSignedAttribute []signature.Attribute) (*signature.SignRequest, error) { payloadBytes := []byte(`{ "subject": { "mediaType": "application/vnd.oci.image.manifest.v1+json", "digest": "sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333", "size": 16724, "annotations": { "io.wabbit-networks.buildId": "123" } } }`) return &signature.SignRequest{ Payload: signature.Payload{ ContentType: "application/vnd.cncf.notary.payload.v1+json", Content: payloadBytes, }, Signer: signer, SigningTime: time.Now(), Expiry: time.Now().Add(time.Hour), ExtendedSignedAttributes: extendedSignedAttribute, SigningAgent: "Notation/1.0.0", SigningScheme: signingScheme, }, nil } func getSigner(isLocal bool, certs []*x509.Certificate, privateKey *rsa.PrivateKey) (signature.Signer, error) { if certs == nil { certs = getSigningCerts() } if privateKey == nil { privateKey = testhelper.GetRSALeafCertificate().PrivateKey } if isLocal { return signature.NewLocalSigner(certs, privateKey) } return &remoteMockSigner{ certs: certs, privateKey: privateKey, }, nil } func getEncodedMessage(signingScheme signature.SigningScheme, isLocal bool, extendedSignedAttribute []signature.Attribute) ([]byte, error) { signer, err := getSigner(isLocal, nil, nil) if err != nil { return nil, err } signReq, err := getSignReq(signingScheme, signer, extendedSignedAttribute) if err != nil { return nil, err } e := envelope{} return e.Sign(signReq) } func getSignedEnvelope(signingScheme signature.SigningScheme, isLocal bool, extendedSignedAttribute []signature.Attribute) (*jwsEnvelope, error) { encoded, err := getEncodedMessage(signingScheme, isLocal, extendedSignedAttribute) if err != nil { return nil, err } var env jwsEnvelope err = json.Unmarshal(encoded, &env) if err != nil { return nil, err } return &env, nil } func verifyEnvelope(env *jwsEnvelope) error { newEncoded, err := json.Marshal(env) if err != nil { return err } _, err = verifyCore(newEncoded) return err } func verifyCore(encoded []byte) (*signature.EnvelopeContent, error) { env, err := ParseEnvelope(encoded) if err != nil { return nil, err } return env.Verify() } func TestNewEnvelope(t *testing.T) { env := NewEnvelope() if env == nil { t.Fatal("should get an JWS envelope") } } func TestSignFailed(t *testing.T) { // Test the same key exists both in extended signed attributes and protected header t.Run("extended attribute conflict with protected header keys", func(t *testing.T) { _, err := getEncodedMessage(signature.SigningSchemeX509, true, extSignedAttrRepeated) checkErrorEqual(t, "attribute key:cty repeated", err.Error()) }) t.Run("extended attribute error value", func(t *testing.T) { _, err := getEncodedMessage(signature.SigningSchemeX509, true, extSignedAttrErrorValue) checkErrorEqual(t, "json: unsupported value: +Inf", err.Error()) }) t.Run("unsupported sign algorithm", func(t *testing.T) { signer := errorLocalSigner{ algType: signature.KeyTypeRSA, size: 222, } _, err := getEncodedMessage(signature.SigningSchemeX509, true, nil) checkNoError(t, err) signReq, err := getSignReq(signature.SigningSchemeX509, &signer, nil) checkNoError(t, err) e := envelope{} _, err = e.Sign(signReq) checkErrorEqual(t, `signature algorithm "#0" is not supported`, err.Error()) }) t.Run("invalid tsa url", func(t *testing.T) { env := envelope{} signer, err := signaturetest.GetTestLocalSigner(signature.KeyTypeRSA, 3072) checkNoError(t, err) signReq, err := getSignReq(signature.SigningSchemeX509, signer, nil) checkNoError(t, err) signReq.Timestamper, err = tspclient.NewHTTPTimestamper(nil, "invalid") if err != nil { t.Fatal(err) } expected := errors.New("timestamp: Post \"invalid\": unsupported protocol scheme \"\"") encoded, err := env.Sign(signReq) if !isErrEqual(expected, err) { t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) } var timestampErr *signature.TimestampError if !errors.As(err, ×tampErr) { t.Fatal("expected signature.TimestampError") } if encoded != nil { t.Fatal("expected nil signature envelope") } }) } func TestSigningScheme(t *testing.T) { var signParams = []struct { isLocal bool signingScheme signature.SigningScheme }{ {true, signature.SigningSchemeX509}, {true, signature.SigningSchemeX509SigningAuthority}, {false, signature.SigningSchemeX509}, {false, signature.SigningSchemeX509SigningAuthority}, } for _, tt := range signParams { t.Run(fmt.Sprintf("verify_isLocal=%v_signingScheme=%v", tt.isLocal, tt.signingScheme), func(t *testing.T) { encoded, err := getEncodedMessage(tt.signingScheme, tt.isLocal, extSignedAttr) checkNoError(t, err) _, err = verifyCore(encoded) checkNoError(t, err) }) } } func TestSignVerify(t *testing.T) { for _, keyType := range signaturetest.KeyTypes { keyName := map[signature.KeyType]string{ signature.KeyTypeEC: "ECDSA", signature.KeyTypeRSA: "RSA", }[keyType] for _, size := range signaturetest.GetKeySizes(keyType) { t.Run(fmt.Sprintf("%s %d", keyName, size), func(t *testing.T) { signer, err := signaturetest.GetTestLocalSigner(keyType, size) checkNoError(t, err) signReq, err := getSignReq(signature.SigningSchemeX509, signer, nil) checkNoError(t, err) e := envelope{} encoded, err := e.Sign(signReq) checkNoError(t, err) _, err = verifyCore(encoded) checkNoError(t, err) }) } } } func TestSignWithTimestamp(t *testing.T) { signer, err := signaturetest.GetTestLocalSigner(signature.KeyTypeRSA, 3072) checkNoError(t, err) signReq, err := getSignReq(signature.SigningSchemeX509, signer, nil) checkNoError(t, err) signReq.Timestamper, err = tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl) if err != nil { t.Fatal(err) } rootCerts, err := nx509.ReadCertificateFile("../../internal/timestamp/testdata/tsaRootCert.crt") if err != nil || len(rootCerts) == 0 { t.Fatal("failed to read root CA certificate:", err) } rootCert := rootCerts[0] rootCAs := x509.NewCertPool() rootCAs.AddCert(rootCert) signReq.TSARootCAs = rootCAs env := envelope{} encoded, err := env.Sign(signReq) if err != nil || encoded == nil { t.Fatalf("Sign() failed. Error = %s", err) } content, err := env.Content() if err != nil { t.Fatal(err) } timestampToken := content.SignerInfo.UnsignedAttributes.TimestampSignature if len(timestampToken) == 0 { t.Fatal("expected timestamp token to be present") } signedToken, err := tspclient.ParseSignedToken(timestampToken) if err != nil { t.Fatal(err) } info, err := signedToken.Info() if err != nil { t.Fatal(err) } _, err = info.Validate(content.SignerInfo.Signature) if err != nil { t.Fatal(err) } } func TestVerify(t *testing.T) { t.Run("break json format", func(t *testing.T) { encoded, err := getEncodedMessage(signature.SigningSchemeX509, true, extSignedAttr) checkNoError(t, err) encoded[0] = '}' _, err = verifyCore(encoded) checkErrorEqual(t, "invalid character '}' looking for beginning of value", err.Error()) }) t.Run("tamper signature", func(t *testing.T) { // get envelope env, err := getSignedEnvelope(signature.SigningSchemeX509, true, extSignedAttr) checkNoError(t, err) // temper envelope env.Signature = "" err = verifyEnvelope(env) checkErrorEqual(t, "signature is invalid. Error: crypto/rsa: verification error", err.Error()) }) t.Run("empty certificate", func(t *testing.T) { // get envelope env, err := getSignedEnvelope(signature.SigningSchemeX509, true, extSignedAttr) checkNoError(t, err) // temper envelope env.Header.CertChain = [][]byte{} err = verifyEnvelope(env) checkErrorEqual(t, "certificate chain is not present", err.Error()) }) t.Run("tamper certificate", func(t *testing.T) { // get envelope env, err := getSignedEnvelope(signature.SigningSchemeX509, true, extSignedAttr) checkNoError(t, err) // temper envelope env.Header.CertChain[0][0] = 'C' err = verifyEnvelope(env) checkErrorEqual(t, "malformed leaf certificate", err.Error()) }) t.Run("malformed protected header base64 encoded", func(t *testing.T) { // get envelope env, err := getSignedEnvelope(signature.SigningSchemeX509, true, extSignedAttr) checkNoError(t, err) // temper envelope env.Protected = "$" + env.Protected err = verifyEnvelope(env) checkErrorEqual(t, "signature is invalid. Error: illegal base64 data at input byte 0", err.Error()) }) t.Run("malformed protected header raw", func(t *testing.T) { // get envelope env, err := getSignedEnvelope(signature.SigningSchemeX509, true, extSignedAttr) checkNoError(t, err) // temper envelope rawProtected, err := base64.RawURLEncoding.DecodeString(env.Protected) checkNoError(t, err) rawProtected[0] = '}' env.Protected = base64.RawURLEncoding.EncodeToString(rawProtected) err = verifyEnvelope(env) checkErrorEqual(t, "signature is invalid. Error: invalid character '}' looking for beginning of value", err.Error()) }) } func TestSignerInfo(t *testing.T) { getEnvelopeAndHeader := func(signingScheme signature.SigningScheme) (*jwsEnvelope, *jwsProtectedHeader) { // get envelope env, err := getSignedEnvelope(signingScheme, true, extSignedAttr) checkNoError(t, err) // get protected header header, err := parseProtectedHeaders(env.Protected) checkNoError(t, err) return env, header } updateProtectedHeader := func(env *jwsEnvelope, protected *jwsProtectedHeader) { // generate protected header headerMap := make(map[string]interface{}) valueOf := reflect.ValueOf(*protected) for i := 0; i < valueOf.NumField(); i++ { var key string tags := strings.Split(valueOf.Type().Field(i).Tag.Get("json"), ",") if len(tags) > 0 { key = tags[0] } if key == "-" { continue } headerMap[key] = valueOf.Field(i).Interface() } // extract extended attribute for key, value := range protected.ExtendedAttributes { headerMap[key] = value } // marshal and write back to envelope rawProtected, err := json.Marshal(headerMap) checkNoError(t, err) env.Protected = base64.RawURLEncoding.EncodeToString(rawProtected) } getSignerInfo := func(env *jwsEnvelope, protected *jwsProtectedHeader) (*signature.SignerInfo, error) { updateProtectedHeader(env, protected) // marshal tampered envelope newEncoded, err := json.Marshal(env) checkNoError(t, err) // parse tampered envelope newEnv, err := ParseEnvelope(newEncoded) checkNoError(t, err) content, err := newEnv.Content() if err != nil { return nil, err } return &content.SignerInfo, nil } t.Run("tamper protected header signing scheme X509", func(t *testing.T) { env, header := getEnvelopeAndHeader(signature.SigningSchemeX509) // temper protected header signingTime := time.Now() header.AuthenticSigningTime = &signingTime _, err := getSignerInfo(env, header) checkErrorEqual(t, `"io.cncf.notary.authenticSigningTime" header must not be present for notary.x509 signing scheme`, err.Error()) }) t.Run("tamper protected header signing scheme X509 Signing Authority", func(t *testing.T) { env, header := getEnvelopeAndHeader(signature.SigningSchemeX509SigningAuthority) // temper protected header signingTime := time.Now() header.SigningTime = &signingTime _, err := getSignerInfo(env, header) checkErrorEqual(t, `"io.cncf.notary.signingTime" header must not be present for notary.x509.signingAuthority signing scheme`, err.Error()) }) t.Run("tamper protected header signing scheme X509 Signing Authority 2", func(t *testing.T) { env, header := getEnvelopeAndHeader(signature.SigningSchemeX509SigningAuthority) // temper protected header header.AuthenticSigningTime = nil _, err := getSignerInfo(env, header) checkErrorEqual(t, `"io.cncf.notary.authenticSigningTime" header must be present for notary.x509 signing scheme`, err.Error()) }) t.Run("tamper protected header extended attributes", func(t *testing.T) { env, header := getEnvelopeAndHeader(signature.SigningSchemeX509) // temper protected header header.ExtendedAttributes = make(map[string]interface{}) _, err := getSignerInfo(env, header) checkErrorEqual(t, `"testKey" header is marked critical but not present`, err.Error()) }) t.Run("add protected header critical key", func(t *testing.T) { env, header := getEnvelopeAndHeader(signature.SigningSchemeX509) // temper protected header header.Critical = header.Critical[:len(header.Critical)-2] _, err := getSignerInfo(env, header) checkErrorEqual(t, `these required headers are not marked as critical: [io.cncf.notary.expiry]`, err.Error()) }) t.Run("empty critical section", func(t *testing.T) { env, header := getEnvelopeAndHeader(signature.SigningSchemeX509) // temper protected header header.Critical = []string{} _, err := getSignerInfo(env, header) checkErrorEqual(t, `missing "crit" header`, err.Error()) }) t.Run("unsupported algorithm", func(t *testing.T) { env, header := getEnvelopeAndHeader(signature.SigningSchemeX509) // temper protected header header.Algorithm = "ES222" _, err := getSignerInfo(env, header) checkErrorEqual(t, `signature algorithm "ES222" is not supported`, err.Error()) }) t.Run("tamper raw protected header json format", func(t *testing.T) { // get envelope env, err := getSignedEnvelope(signature.SigningSchemeX509, true, extSignedAttr) checkNoError(t, err) rawProtected, err := base64.RawURLEncoding.DecodeString(env.Protected) checkNoError(t, err) // temper envelope rawProtected[0] = '}' env.Protected = base64.RawURLEncoding.EncodeToString(rawProtected) newEncoded, err := json.Marshal(env) checkNoError(t, err) // parse tampered envelope newEnv, err := ParseEnvelope(newEncoded) checkNoError(t, err) _, err = newEnv.Content() checkErrorEqual(t, "jws envelope protected header can't be decoded: invalid character '}' looking for beginning of value", err.Error()) }) t.Run("tamper signature base64 encoding", func(t *testing.T) { env, header := getEnvelopeAndHeader(signature.SigningSchemeX509) // temper protected header env.Signature = "{" + env.Signature _, err := getSignerInfo(env, header) checkErrorEqual(t, `illegal base64 data at input byte 0`, err.Error()) }) t.Run("tamper empty signature", func(t *testing.T) { env, header := getEnvelopeAndHeader(signature.SigningSchemeX509) // temper protected header env.Signature = "" _, err := getSignerInfo(env, header) checkErrorEqual(t, `signature missing in jws-json envelope`, err.Error()) }) t.Run("tamper cert chain", func(t *testing.T) { env, header := getEnvelopeAndHeader(signature.SigningSchemeX509) // temper protected header env.Header.CertChain[0] = append(env.Header.CertChain[0], 'v') _, err := getSignerInfo(env, header) checkErrorEqual(t, `x509: trailing data`, err.Error()) }) } func TestPayload(t *testing.T) { t.Run("tamper envelope cause JWT parse failed", func(t *testing.T) { // get envelope env, err := getSignedEnvelope(signature.SigningSchemeX509, true, extSignedAttr) checkNoError(t, err) // tamper payload env.Payload = env.Payload[1:] // marshal tampered envelope newEncoded, err := json.Marshal(env) checkNoError(t, err) // parse tampered envelope newEnv, err := ParseEnvelope(newEncoded) checkNoError(t, err) _, err = newEnv.Content() checkErrorEqual(t, "payload error: illegal base64 data at input byte", err.Error()) }) } func TestEmptyEnvelope(t *testing.T) { wantErr := &signature.SignatureEnvelopeNotFoundError{} env := envelope{} t.Run("Verify()_with_empty_envelope", func(t *testing.T) { _, err := env.Verify() if !errors.Is(err, wantErr) { t.Fatalf("want: %v, got: %v", wantErr, err) } }) t.Run("Payload()_with_empty_envelope", func(t *testing.T) { _, err := env.Content() if !errors.Is(err, wantErr) { t.Fatalf("want: %v, got: %v", wantErr, err) } }) t.Run("SignerInfo()_with_empty_envelope", func(t *testing.T) { _, err := env.Content() if !errors.Is(err, wantErr) { t.Fatalf("want: %v, got: %v", wantErr, err) } }) } func isErrEqual(wanted, got error) bool { if wanted == nil && got == nil { return true } if wanted != nil && got != nil { return wanted.Error() == got.Error() } return false } notation-core-go-1.1.0/signature/jws/jws.go000066400000000000000000000225471466321423000206460ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package jws import ( "crypto/x509" "encoding/base64" "encoding/json" "fmt" "strings" "github.com/notaryproject/notation-core-go/signature" ) func parseProtectedHeaders(encoded string) (*jwsProtectedHeader, error) { rawProtected, err := base64.RawURLEncoding.DecodeString(encoded) if err != nil { return nil, &signature.InvalidSignatureError{ Msg: fmt.Sprintf("jws envelope protected header can't be decoded: %s", err.Error())} } // To Unmarshal JSON with some known(jwsProtectedHeader), and some unknown(jwsProtectedHeader.ExtendedAttributes) field names. // We unmarshal twice: once into a value of type jwsProtectedHeader and once into a value of type jwsProtectedHeader.ExtendedAttributes(map[string]interface{}) // and removing the keys are already been defined in jwsProtectedHeader. var protected jwsProtectedHeader if err = json.Unmarshal(rawProtected, &protected); err != nil { return nil, &signature.InvalidSignatureError{ Msg: fmt.Sprintf("jws envelope protected header can't be decoded: %s", err.Error())} } if err = json.Unmarshal(rawProtected, &protected.ExtendedAttributes); err != nil { return nil, &signature.InvalidSignatureError{ Msg: fmt.Sprintf("jws envelope protected header can't be decoded: %s", err.Error())} } // delete attributes that are already defined in jwsProtectedHeader. for _, headerKey := range headerKeys { delete(protected.ExtendedAttributes, headerKey) } return &protected, nil } func populateProtectedHeaders(protectedHeader *jwsProtectedHeader, signerInfo *signature.SignerInfo) error { err := validateProtectedHeaders(protectedHeader) if err != nil { return err } if signerInfo.SignatureAlgorithm, err = getSignatureAlgorithm(protectedHeader.Algorithm); err != nil { return err } signerInfo.SignedAttributes.ExtendedAttributes = getExtendedAttributes(protectedHeader.ExtendedAttributes, protectedHeader.Critical) signerInfo.SignedAttributes.SigningScheme = protectedHeader.SigningScheme if protectedHeader.Expiry != nil { signerInfo.SignedAttributes.Expiry = *protectedHeader.Expiry } switch protectedHeader.SigningScheme { case signature.SigningSchemeX509: if protectedHeader.SigningTime != nil { signerInfo.SignedAttributes.SigningTime = *protectedHeader.SigningTime } case signature.SigningSchemeX509SigningAuthority: if protectedHeader.AuthenticSigningTime != nil { signerInfo.SignedAttributes.SigningTime = *protectedHeader.AuthenticSigningTime } default: return &signature.InvalidSignatureError{ Msg: fmt.Sprintf("unsupported SigningScheme: `%v`", protectedHeader.SigningScheme), } } return nil } func validateProtectedHeaders(protectedHeader *jwsProtectedHeader) error { // validate headers that should not be present as per signing schemes switch protectedHeader.SigningScheme { case signature.SigningSchemeX509: if protectedHeader.AuthenticSigningTime != nil { return &signature.InvalidSignatureError{Msg: fmt.Sprintf("%q header must not be present for %s signing scheme", headerKeyAuthenticSigningTime, signature.SigningSchemeX509)} } case signature.SigningSchemeX509SigningAuthority: if protectedHeader.SigningTime != nil { return &signature.InvalidSignatureError{Msg: fmt.Sprintf("%q header must not be present for %s signing scheme", headerKeySigningTime, signature.SigningSchemeX509SigningAuthority)} } if protectedHeader.AuthenticSigningTime == nil { return &signature.InvalidSignatureError{Msg: fmt.Sprintf("%q header must be present for %s signing scheme", headerKeyAuthenticSigningTime, signature.SigningSchemeX509)} } default: return &signature.InvalidSignatureError{Msg: fmt.Sprintf("unsupported SigningScheme: `%v`", protectedHeader.SigningScheme)} } return validateCriticalHeaders(protectedHeader) } func validateCriticalHeaders(protectedHeader *jwsProtectedHeader) error { if len(protectedHeader.Critical) == 0 { return &signature.InvalidSignatureError{Msg: `missing "crit" header`} } mustMarkedCrit := map[string]bool{headerKeySigningScheme: true} if protectedHeader.Expiry != nil && !protectedHeader.Expiry.IsZero() { mustMarkedCrit[headerKeyExpiry] = true } if protectedHeader.SigningScheme == signature.SigningSchemeX509SigningAuthority { mustMarkedCrit[headerKeyAuthenticSigningTime] = true } for _, val := range protectedHeader.Critical { if _, ok := mustMarkedCrit[val]; ok { delete(mustMarkedCrit, val) } else { if _, ok := protectedHeader.ExtendedAttributes[val]; !ok { return &signature.InvalidSignatureError{ Msg: fmt.Sprintf("%q header is marked critical but not present", val)} } } } // validate all required critical headers headers(as per spec) are marked as critical. if len(mustMarkedCrit) != 0 { // This is not taken care by VerifySignerInfo method keys := make([]string, 0, len(mustMarkedCrit)) for k := range mustMarkedCrit { keys = append(keys, k) } return &signature.InvalidSignatureError{Msg: fmt.Sprintf("these required headers are not marked as critical: %v", keys)} } return nil } func getSignatureAlgorithm(alg string) (signature.Algorithm, error) { signatureAlg, ok := jwsAlgSignatureAlgMap[alg] if !ok { return 0, &signature.UnsupportedSignatureAlgoError{Alg: alg} } return signatureAlg, nil } func getExtendedAttributes(attrs map[string]interface{}, critical []string) []signature.Attribute { extendedAttr := make([]signature.Attribute, 0, len(attrs)) for key, value := range attrs { extendedAttr = append(extendedAttr, signature.Attribute{ Key: key, Critical: contains(critical, key), Value: value, }) } return extendedAttr } func contains(s []string, e string) bool { for _, a := range s { if a == e { return true } } return false } func generateJWS(compact string, req *signature.SignRequest, certs []*x509.Certificate) (*jwsEnvelope, error) { parts := strings.Split(compact, ".") if len(parts) != 3 { // this should never happen return nil, fmt.Errorf( "unexpected error occurred while generating a JWS-JSON serialization from compact serialization. want: len(parts) == 3, got: len(parts) == %d", len(parts)) } rawCerts := make([][]byte, len(certs)) for i, cert := range certs { rawCerts[i] = cert.Raw } return &jwsEnvelope{ Protected: parts[0], Payload: parts[1], Signature: parts[2], Header: jwsUnprotectedHeader{ CertChain: rawCerts, SigningAgent: req.SigningAgent, }, }, nil } // getSignerAttributes merge extended signed attributes and protected header to be signed attributes. func getSignedAttributes(req *signature.SignRequest, algorithm string) (map[string]interface{}, error) { extAttrs := make(map[string]interface{}) crit := []string{headerKeySigningScheme} // write extended signed attributes to the extAttrs map for _, elm := range req.ExtendedSignedAttributes { key, ok := elm.Key.(string) if !ok { return nil, &signature.InvalidSignRequestError{Msg: "JWS envelope format only supports key of type string"} } if _, ok := extAttrs[key]; ok { return nil, &signature.InvalidSignRequestError{Msg: fmt.Sprintf("%q already exists in the extAttrs", key)} } extAttrs[key] = elm.Value if elm.Critical { crit = append(crit, key) } } jwsProtectedHeader := jwsProtectedHeader{ Algorithm: algorithm, ContentType: req.Payload.ContentType, SigningScheme: req.SigningScheme, } switch req.SigningScheme { case signature.SigningSchemeX509: jwsProtectedHeader.SigningTime = &req.SigningTime case signature.SigningSchemeX509SigningAuthority: crit = append(crit, headerKeyAuthenticSigningTime) jwsProtectedHeader.AuthenticSigningTime = &req.SigningTime default: return nil, fmt.Errorf("unsupported SigningScheme: `%v`", req.SigningScheme) } if !req.Expiry.IsZero() { crit = append(crit, headerKeyExpiry) jwsProtectedHeader.Expiry = &req.Expiry } jwsProtectedHeader.Critical = crit m, err := convertToMap(jwsProtectedHeader) if err != nil { return nil, fmt.Errorf("unexpected error occurred while creating protected headers, Error: %s", err.Error()) } return mergeMaps(m, extAttrs) } func convertToMap(i interface{}) (map[string]interface{}, error) { s, err := json.Marshal(i) if err != nil { return nil, err } var m map[string]interface{} err = json.Unmarshal(s, &m) return m, err } func mergeMaps(maps ...map[string]interface{}) (map[string]interface{}, error) { result := make(map[string]interface{}) for _, m := range maps { for k, v := range m { if _, ok := result[k]; ok { return nil, fmt.Errorf("attribute key:%s repeated", k) } result[k] = v } } return result, nil } // compactJWS converts Flattened JWS JSON Serialization Syntax (section-7.2.2) to // JWS Compact Serialization (section-7.1) // // [RFC 7515]: https://www.rfc-editor.org/rfc/rfc7515.html func compactJWS(envelope *jwsEnvelope) string { return strings.Join([]string{ envelope.Protected, envelope.Payload, envelope.Signature}, ".") } notation-core-go-1.1.0/signature/jws/jws_test.go000066400000000000000000000033351466321423000216770ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package jws import ( "encoding/json" "math" "testing" ) func Test_convertToMap(t *testing.T) { type S struct { A string B int C float64 } t.Run("invalid value", func(t *testing.T) { _, err := convertToMap(math.Inf(1)) if err == nil { t.Fatal("should cause error") } }) t.Run("normal case", func(t *testing.T) { testStruct := S{ A: "test string", B: 1, C: 1.1, } // generate map m, err := convertToMap(&testStruct) checkNoError(t, err) // convert map to struct bytes, err := json.Marshal(m) checkNoError(t, err) var newStruct S err = json.Unmarshal(bytes, &newStruct) checkNoError(t, err) // check new struct equal with original struct if newStruct != testStruct { t.Fatal("convertToMap error") } }) } func Test_generateJWSError(t *testing.T) { _, err := generateJWS("", nil, nil) checkErrorEqual(t, "unexpected error occurred while generating a JWS-JSON serialization from compact serialization", err.Error()) } func Test_getSignatureAlgorithmError(t *testing.T) { _, err := getSignatureAlgorithm("ES222") checkErrorEqual(t, `signature algorithm "ES222" is not supported`, err.Error()) } notation-core-go-1.1.0/signature/jws/jwt.go000066400000000000000000000111041466321423000206320ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package jws import ( "crypto" "crypto/x509" "encoding/base64" "errors" "fmt" "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-core-go/signature" ) // signingMethod is the interface for jwt.SigingMethod with additional method to // access certificate chain after calling Sign(). type signingMethod interface { jwt.SigningMethod // CertificateChain returns the certificate chain. // // It should be called after calling Sign(). CertificateChain() ([]*x509.Certificate, error) // PrivateKey returns the private key. PrivateKey() crypto.PrivateKey } // remoteSigningMethod wraps the remote signer to be a SigningMethod. type remoteSigningMethod struct { signer signature.Signer certs []*x509.Certificate algorithm string } func newRemoteSigningMethod(signer signature.Signer) (signingMethod, error) { algorithm, err := extractJwtAlgorithm(signer) if err != nil { return nil, err } return &remoteSigningMethod{ signer: signer, algorithm: algorithm, }, nil } // Verify doesn't need to be implemented. func (s *remoteSigningMethod) Verify(signingString, signature string, key interface{}) error { return errors.New("not implemented") } // Sign hashes the signingString and call the remote signer to sign the digest. func (s *remoteSigningMethod) Sign(signingString string, key interface{}) (string, error) { // sign by external signer sig, certs, err := s.signer.Sign([]byte(signingString)) if err != nil { return "", err } s.certs = certs return base64.RawURLEncoding.EncodeToString(sig), nil } // Alg implements jwt.SigningMethod interface. func (s *remoteSigningMethod) Alg() string { return s.algorithm } // CertificateChain returns the certificate chain. // // It should be called after Sign(). func (s *remoteSigningMethod) CertificateChain() ([]*x509.Certificate, error) { if s.certs == nil { return nil, &signature.InvalidSignRequestError{Msg: "certificate chain is not set"} } return s.certs, nil } // PrivateKey returns nil for remote signer. func (s *remoteSigningMethod) PrivateKey() crypto.PrivateKey { return nil } // localSigningMethod wraps the local signer to be a SigningMethod. type localSigningMethod struct { jwt.SigningMethod signer signature.LocalSigner } func newLocalSigningMethod(signer signature.LocalSigner) (signingMethod, error) { alg, err := extractJwtAlgorithm(signer) if err != nil { return nil, err } return &localSigningMethod{ SigningMethod: jwt.GetSigningMethod(alg), signer: signer, }, nil } // CertificateChain returns the certificate chain. func (s *localSigningMethod) CertificateChain() ([]*x509.Certificate, error) { return s.signer.CertificateChain() } // PrivateKey returns the private key. func (s *localSigningMethod) PrivateKey() crypto.PrivateKey { return s.signer.PrivateKey() } // getSigningMethod return signingMethod for the given signer. func getSigningMethod(signer signature.Signer) (signingMethod, error) { if localSigner, ok := signer.(signature.LocalSigner); ok { // for local signer return newLocalSigningMethod(localSigner) } // for remote signer return newRemoteSigningMethod(signer) } // verifyJWT verifies the JWT token against the specified verification key. func verifyJWT(tokenString string, publicKey interface{}) error { parser := jwt.NewParser( jwt.WithValidMethods(validMethods), jwt.WithJSONNumber(), jwt.WithoutClaimsValidation(), ) if _, err := parser.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { return publicKey, nil }); err != nil { return &signature.SignatureIntegrityError{Err: err} } return nil } func extractJwtAlgorithm(signer signature.Signer) (string, error) { // extract algorithm from signer keySpec, err := signer.KeySpec() if err != nil { return "", err } alg := keySpec.SignatureAlgorithm() // converts the signature.Algorithm to be jwt package defined // algorithm name. jwsAlg, ok := signatureAlgJWSAlgMap[alg] if !ok { return "", &signature.UnsupportedSignatureAlgoError{ Alg: fmt.Sprintf("#%d", alg)} } return jwsAlg, nil } notation-core-go-1.1.0/signature/jws/jwt_test.go000066400000000000000000000122031466321423000216720ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package jws import ( "crypto" "crypto/x509" "errors" "testing" "github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/testhelper" ) type errorLocalSigner struct { algType signature.KeyType size int keySpecError error } // Sign returns error func (s *errorLocalSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { return nil, nil, errors.New("sign error") } // KeySpec returns the key specification. func (s *errorLocalSigner) KeySpec() (signature.KeySpec, error) { return signature.KeySpec{ Type: s.algType, Size: s.size, }, s.keySpecError } // PrivateKey returns nil. func (s *errorLocalSigner) PrivateKey() crypto.PrivateKey { return nil } // CertificateChain returns nil. func (s *errorLocalSigner) CertificateChain() ([]*x509.Certificate, error) { return nil, nil } func Test_remoteSigningMethod_Verify(t *testing.T) { s := &remoteSigningMethod{} // Sign signs the payload and returns the raw signature and certificates. err := s.Verify("", "", nil) if err == nil { t.Fatalf("should panic") } } func Test_newLocalSigningMethod(t *testing.T) { signer := errorLocalSigner{} _, err := newLocalSigningMethod(&signer) checkErrorEqual(t, `signature algorithm "#0" is not supported`, err.Error()) } func Test_newRemoteSigningMethod(t *testing.T) { _, err := newRemoteSigningMethod(&errorLocalSigner{}) checkErrorEqual(t, `signature algorithm "#0" is not supported`, err.Error()) } func Test_remoteSigningMethod_CertificateChain(t *testing.T) { certs := []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, } signer, err := getSigner(false, certs, testhelper.GetRSALeafCertificate().PrivateKey) checkNoError(t, err) signingScheme, err := newRemoteSigningMethod(signer) checkNoError(t, err) _, err = signingScheme.CertificateChain() checkErrorEqual(t, "certificate chain is not set", err.Error()) } func Test_remoteSigningMethod_Sign(t *testing.T) { signer := errorLocalSigner{ algType: signature.KeyTypeRSA, size: 2048, keySpecError: nil, } signingScheme, err := newRemoteSigningMethod(&signer) checkNoError(t, err) _, err = signingScheme.Sign("", nil) checkErrorEqual(t, "sign error", err.Error()) } func Test_extractJwtAlgorithm(t *testing.T) { _, err := extractJwtAlgorithm(&errorLocalSigner{}) checkErrorEqual(t, `signature algorithm "#0" is not supported`, err.Error()) _, err = extractJwtAlgorithm(&errorLocalSigner{ keySpecError: errors.New("get key spec error"), }) checkErrorEqual(t, `get key spec error`, err.Error()) } func Test_verifyJWT(t *testing.T) { type args struct { tokenString string publicKey interface{} } tests := []struct { name string args args wantErr bool }{ { name: "invalid signature", args: args{ tokenString: "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsInRlc3RLZXkiLCJpby5jbmNmLm5vdGFyeS5leHBpcnkiXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LmV4cGlyeSI6IjIwMjItMDgtMjRUMTc6MTg6MTUuNDkxNzQ1ODQ1KzA4OjAwIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDIyLTA4LTI0VDE2OjE4OjE1LjQ5MTc0NTgwNCswODowMCIsInRlc3RLZXkiOiJ0ZXN0VmFsdWUiLCJ0ZXN0S2V5MiI6InRlc3RWYWx1ZTIifQ.ImV3b2dJQ0p6ZFdKcVpXTjBJam9nZXdvZ0lDQWdJbTFsWkdsaFZIbHdaU0k2SUNKaGNIQnNhV05oZEdsdmJpOTJibVF1YjJOcExtbHRZV2RsTG0xaGJtbG1aWE4wTG5ZeEsycHpiMjRpTEFvZ0lDQWdJbVJwWjJWemRDSTZJQ0p6YUdFeU5UWTZOek5qT0RBek9UTXdaV0V6WW1FeFpUVTBZbU15TldNeVltUmpOVE5sWkdRd01qZzBZell5WldRMk5URm1aVGRpTURBek5qbGtZVFV4T1dFell6TXpNeUlzQ2lBZ0lDQWljMmw2WlNJNklERTJOekkwTEFvZ0lDQWdJbUZ1Ym05MFlYUnBiMjV6SWpvZ2V3b2dJQ0FnSUNBZ0lDSnBieTUzWVdKaWFYUXRibVYwZDI5eWEzTXVZblZwYkdSSlpDSTZJQ0l4TWpNaUNpQWdJQ0I5Q2lBZ2ZRcDlDZ2s9Ig.YmF1_5dMW4YWK2fzct1dp25lTy8p0qdSmR-O2fZsf29ohiLYGUVXfvRjEgERzZvDd49aOYQvrEgGvoU9FfK2KIqHrJ8kliI00wd4kuK57aE83pszBMOOrZqAjqkdyoj7dswmwJSyjMC9fhwh_AwrrOnrBjw4U0vGTrImMQEwHfVq0MWLCuw9YpFkytLPeCl8n825EtqMzwYYTUzdQfQJO_ZZrS34n8tK0IRZrX2LjrYz9HqR_UFgVqf_G9qwJpekYyd9Aacl9y4x7zzI-R-bADFgztyAYeWRmE75qI26OgG-ss4wfG-ZbchEm6FYU8py64bsLmJtK9muPd9ZU7SXQOEVzxtXoQFnUhT9AgaNNoxnSnU25mMjAeuGDj0Xn_Gv7f24PyDk9ZEE3WjrguJyzaP6P4jYugXr6Afq10HXRpI_cE8B-6USGpiRH9iJLE04xumWpjWup9p5fv3Fnt3Au1dhbgaDvrSGMHmmCSW4dk7_87Q4LGkGcbn0zNINydcg", publicKey: testhelper.GetRSALeafCertificate().Cert.PublicKey, }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := verifyJWT(tt.args.tokenString, tt.args.publicKey); (err != nil) != tt.wantErr { t.Errorf("verifyJWT() error = %v, wantErr %v", err, tt.wantErr) } }) } } notation-core-go-1.1.0/signature/jws/testdata/000077500000000000000000000000001466321423000213135ustar00rootroot00000000000000notation-core-go-1.1.0/signature/jws/testdata/conformance.json000066400000000000000000000015301466321423000244770ustar00rootroot00000000000000{ "payload": "eyJrZXkiOiJoZWxsbyBKV1MifQ", "protected": "eyJhbGciOiJFUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsInNpZ25lZENyaXRLZXkxIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5Il0sImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS5wYXlsb2FkLnYxK2pzb24iLCJpby5jbmNmLm5vdGFyeS5leHBpcnkiOiIyMDk5LTA4LTMwVDEzOjUwOjAwWiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMi0wOC0yOVQxMzo1MDowMFoiLCJzaWduZWRDcml0S2V5MSI6InNpZ25lZENyaXRWYWx1ZTEiLCJzaWduZWRLZXkxIjoic2lnbmVkVmFsdWUxIiwic2lnbmVkS2V5MiI6InNpZ25lZFZhbHVlMSIsInNpZ25lZEtleTMiOiJzaWduZWRWYWx1ZTEiLCJzaWduZWRLZXk0Ijoic2lnbmVkVmFsdWUxIn0", "header": { "io.cncf.notary.signingAgent": "NotationConformanceTest/1.0.0" }, "signature": "zWcjEO1QYe1aNIcfIv_nHjCwb-5gjjHTkL8aUjBq3kU_nP6YtDfKX6Sjl90DLG7NR0-uB1Tn6oj_6Ws1LhCuIQ" }notation-core-go-1.1.0/signature/jws/types.go000066400000000000000000000104001466321423000211700ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package jws import ( "time" "github.com/golang-jwt/jwt/v4" "github.com/notaryproject/notation-core-go/signature" ) const ( headerKeyAlg = "alg" headerKeyCty = "cty" headerKeyCrit = "crit" headerKeyExpiry = "io.cncf.notary.expiry" headerKeySigningTime = "io.cncf.notary.signingTime" headerKeySigningScheme = "io.cncf.notary.signingScheme" headerKeyAuthenticSigningTime = "io.cncf.notary.authenticSigningTime" ) // headerKeys includes all system aware keys for JWS protected header // [JWS envelope]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-envelope-jws.md#protected-headers var headerKeys = []string{ headerKeyAlg, headerKeyCty, headerKeyCrit, headerKeyExpiry, headerKeySigningTime, headerKeySigningScheme, headerKeyAuthenticSigningTime, } // jwsProtectedHeader contains the set of protected headers. type jwsProtectedHeader struct { // Defines which algorithm was used to generate the signature. Algorithm string `json:"alg"` // Media type of the secured content (the payload). ContentType string `json:"cty"` // Lists the headers that implementation MUST understand and process. Critical []string `json:"crit,omitempty"` // The "best by use" time for the artifact, as defined by the signer. Expiry *time.Time `json:"io.cncf.notary.expiry,omitempty"` // Specifies the Notary Project Signing Scheme used by the signature. SigningScheme signature.SigningScheme `json:"io.cncf.notary.signingScheme"` // The time at which the signature was generated. only valid when signing // scheme is `notary.x509`. SigningTime *time.Time `json:"io.cncf.notary.signingTime,omitempty"` // The time at which the signature was generated. only valid when signing // scheme is `notary.x509.signingAuthority`. AuthenticSigningTime *time.Time `json:"io.cncf.notary.authenticSigningTime,omitempty"` // The user defined attributes. ExtendedAttributes map[string]interface{} `json:"-"` } // jwsUnprotectedHeader contains the set of unprotected headers. type jwsUnprotectedHeader struct { // RFC3161 timestamp token Base64-encoded. TimestampSignature []byte `json:"io.cncf.notary.timestampSignature,omitempty"` // List of X.509 Base64-DER-encoded certificates // as defined at https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6. CertChain [][]byte `json:"x5c"` // SigningAgent used for signing. SigningAgent string `json:"io.cncf.notary.signingAgent,omitempty"` } // jwsEnvelope is the final Signature envelope. type jwsEnvelope struct { // JWSPayload Base64URL-encoded. Raw data should be JSON format. Payload string `json:"payload"` // jwsProtectedHeader Base64URL-encoded. Protected string `json:"protected"` // Signature metadata that is not integrity Protected Header jwsUnprotectedHeader `json:"header"` // Base64URL-encoded Signature. Signature string `json:"signature"` } var ( ps256 = jwt.SigningMethodPS256.Name ps384 = jwt.SigningMethodPS384.Name ps512 = jwt.SigningMethodPS512.Name es256 = jwt.SigningMethodES256.Name es384 = jwt.SigningMethodES384.Name es512 = jwt.SigningMethodES512.Name ) var validMethods = []string{ps256, ps384, ps512, es256, es384, es512} var signatureAlgJWSAlgMap = map[signature.Algorithm]string{ signature.AlgorithmPS256: ps256, signature.AlgorithmPS384: ps384, signature.AlgorithmPS512: ps512, signature.AlgorithmES256: es256, signature.AlgorithmES384: es384, signature.AlgorithmES512: es512, } var jwsAlgSignatureAlgMap = reverseMap(signatureAlgJWSAlgMap) func reverseMap(m map[signature.Algorithm]string) map[string]signature.Algorithm { n := make(map[string]signature.Algorithm, len(m)) for k, v := range m { n[v] = k } return n } notation-core-go-1.1.0/signature/signer.go000066400000000000000000000102341466321423000205150ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signature import ( "crypto" "crypto/ecdsa" "crypto/rsa" "crypto/x509" "errors" "fmt" ) // Signer is used to sign bytes generated after signature envelope created. type Signer interface { // Sign signs the payload and returns the raw signature and certificates. Sign(payload []byte) ([]byte, []*x509.Certificate, error) // KeySpec returns the key specification. KeySpec() (KeySpec, error) } // LocalSigner is only used by built-in signers to sign. type LocalSigner interface { Signer // CertificateChain returns the certificate chain. CertificateChain() ([]*x509.Certificate, error) // PrivateKey returns the private key. PrivateKey() crypto.PrivateKey } // localSigner implements LocalSigner interface. // // Note that localSigner only holds the signing key, keySpec and certificate // chain. The underlying signing implementation is provided by the underlying // crypto library for the specific signature format like go-jwt or go-cose. type localSigner struct { keySpec KeySpec key crypto.PrivateKey certs []*x509.Certificate } // NewLocalSigner returns a new signer with given certificates and private key. func NewLocalSigner(certs []*x509.Certificate, key crypto.PrivateKey) (LocalSigner, error) { if len(certs) == 0 { return nil, &InvalidArgumentError{ Param: "certs", Err: errors.New("empty certs"), } } keySpec, err := ExtractKeySpec(certs[0]) if err != nil { return nil, err } if !isKeyPair(key, certs[0].PublicKey, keySpec) { return nil, &InvalidArgumentError{ Param: "key and certs", Err: errors.New("key not matches certificate"), } } return &localSigner{ keySpec: keySpec, key: key, certs: certs, }, nil } // isKeyPair checks if the private key matches the provided public key. func isKeyPair(priv crypto.PrivateKey, pub crypto.PublicKey, keySpec KeySpec) bool { switch keySpec.Type { case KeyTypeRSA: privateKey, ok := priv.(*rsa.PrivateKey) if !ok { return false } return privateKey.PublicKey.Equal(pub) case KeyTypeEC: privateKey, ok := priv.(*ecdsa.PrivateKey) if !ok { return false } return privateKey.PublicKey.Equal(pub) default: return false } } // Sign signs the content and returns the raw signature and certificates. // This implementation should never be used by built-in signers. func (s *localSigner) Sign(content []byte) ([]byte, []*x509.Certificate, error) { return nil, nil, fmt.Errorf("local signer doesn't support sign") } // KeySpec returns the key specification. func (s *localSigner) KeySpec() (KeySpec, error) { return s.keySpec, nil } // CertificateChain returns the certificate chain. func (s *localSigner) CertificateChain() ([]*x509.Certificate, error) { return s.certs, nil } // PrivateKey returns the private key. func (s *localSigner) PrivateKey() crypto.PrivateKey { return s.key } // VerifyAuthenticity verifies the certificate chain in the given SignerInfo // with one of the trusted certificates and returns a certificate that matches // with one of the certificates in the SignerInfo. // // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#steps func VerifyAuthenticity(signerInfo *SignerInfo, trustedCerts []*x509.Certificate) (*x509.Certificate, error) { if len(trustedCerts) == 0 { return nil, &InvalidArgumentError{Param: "trustedCerts"} } if signerInfo == nil { return nil, &InvalidArgumentError{Param: "signerInfo"} } for _, trust := range trustedCerts { for _, cert := range signerInfo.CertificateChain { if trust.Equal(cert) { return trust, nil } } } return nil, &SignatureAuthenticityError{} } notation-core-go-1.1.0/signature/signer_test.go000066400000000000000000000127371466321423000215660ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signature import ( "crypto" "crypto/ed25519" "crypto/x509" "reflect" "testing" "github.com/notaryproject/notation-core-go/testhelper" ) func TestNewLocalSigner(t *testing.T) { tests := []struct { name string certs []*x509.Certificate key crypto.PrivateKey expect LocalSigner expectErr bool }{ { name: "empty certs", certs: []*x509.Certificate{}, key: nil, expect: nil, expectErr: true, }, { name: "unsupported leaf cert", certs: []*x509.Certificate{ {PublicKey: ed25519.PublicKey{}}, }, key: nil, expect: nil, expectErr: true, }, { name: "keys not match", certs: []*x509.Certificate{ testhelper.GetECLeafCertificate().Cert, }, key: testhelper.GetRSARootCertificate().PrivateKey, expect: nil, expectErr: true, }, { name: "keys not match", certs: []*x509.Certificate{ testhelper.GetRSARootCertificate().Cert, }, key: testhelper.GetECLeafCertificate().PrivateKey, expect: nil, expectErr: true, }, { name: "RSA keys match", certs: []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, }, key: testhelper.GetRSALeafCertificate().PrivateKey, expect: &localSigner{ keySpec: KeySpec{ Type: KeyTypeRSA, Size: 3072, }, key: testhelper.GetRSALeafCertificate().PrivateKey, certs: []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, }, }, expectErr: false, }, { name: "EC keys match", certs: []*x509.Certificate{ testhelper.GetECLeafCertificate().Cert, }, key: testhelper.GetECLeafCertificate().PrivateKey, expect: &localSigner{ keySpec: KeySpec{ Type: KeyTypeEC, Size: 384, }, key: testhelper.GetECLeafCertificate().PrivateKey, certs: []*x509.Certificate{ testhelper.GetECLeafCertificate().Cert, }, }, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { signer, err := NewLocalSigner(tt.certs, tt.key) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } if !reflect.DeepEqual(signer, tt.expect) { t.Errorf("expect %+v, got %+v", tt.expect, signer) } }) } } func TestSign(t *testing.T) { signer := &localSigner{} raw, certs, err := signer.Sign([]byte{}) if err == nil { t.Errorf("expect error but got nil") } if raw != nil { t.Errorf("expect nil raw signature but got %v", raw) } if certs != nil { t.Errorf("expect nil certs but got %v", certs) } } func TestKeySpec(t *testing.T) { expectKeySpec := KeySpec{ Type: KeyTypeRSA, Size: 256, } signer := &localSigner{keySpec: expectKeySpec} keySpec, err := signer.KeySpec() if err != nil { t.Errorf("expect no error but got %v", err) } if !reflect.DeepEqual(keySpec, expectKeySpec) { t.Errorf("expect keySpec %+v, got %+v", expectKeySpec, keySpec) } } func TestCertificateChain(t *testing.T) { expectCerts := []*x509.Certificate{ testhelper.GetRSALeafCertificate().Cert, } signer := &localSigner{certs: expectCerts} certs, err := signer.CertificateChain() if err != nil { t.Errorf("expect no error but got %v", err) } if !reflect.DeepEqual(certs, expectCerts) { t.Errorf("expect certs %+v, got %+v", expectCerts, certs) } } func TestPrivateKey(t *testing.T) { expectKey := testhelper.GetRSALeafCertificate().PrivateKey signer := &localSigner{key: expectKey} key := signer.PrivateKey() if !reflect.DeepEqual(key, expectKey) { t.Errorf("expect key %+v, got %+v", expectKey, key) } } func TestVerifyAuthenticity(t *testing.T) { tests := []struct { name string signerInfo *SignerInfo certs []*x509.Certificate expect *x509.Certificate expectErr bool }{ { name: "empty certs", signerInfo: nil, certs: make([]*x509.Certificate, 0), expect: nil, expectErr: true, }, { name: "nil signerInfo", signerInfo: nil, certs: []*x509.Certificate{ testhelper.GetECLeafCertificate().Cert, }, expect: nil, expectErr: true, }, { name: "no cert matches", signerInfo: &SignerInfo{}, certs: []*x509.Certificate{ testhelper.GetECLeafCertificate().Cert, }, expect: nil, expectErr: true, }, { name: "cert matches", signerInfo: &SignerInfo{ CertificateChain: []*x509.Certificate{ testhelper.GetECLeafCertificate().Cert, }, }, certs: []*x509.Certificate{ testhelper.GetECLeafCertificate().Cert, }, expect: testhelper.GetECLeafCertificate().Cert, expectErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cert, err := VerifyAuthenticity(tt.signerInfo, tt.certs) if (err != nil) != tt.expectErr { t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) } if !reflect.DeepEqual(cert, tt.expect) { t.Errorf("expect cert %+v, got %+v", tt.expect, cert) } }) } } notation-core-go-1.1.0/signature/types.go000066400000000000000000000162341466321423000204000ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signature import ( "context" "crypto/x509" "errors" "fmt" "time" "github.com/notaryproject/tspclient-go" ) // SignatureMediaType list the supported media-type for signatures. type SignatureMediaType string // SigningScheme formalizes the feature set (guarantees) provided by // the signature. // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signing-scheme.md type SigningScheme string // SigningSchemes supported by notation. const ( // notary.x509 signing scheme. SigningSchemeX509 SigningScheme = "notary.x509" // notary.x509.signingAuthority schema. SigningSchemeX509SigningAuthority SigningScheme = "notary.x509.signingAuthority" ) // SignedAttributes represents signed metadata in the signature envelope. // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#signed-attributes type SignedAttributes struct { // SigningScheme defines the Notary Project Signing Scheme used by the signature. SigningScheme SigningScheme // SigningTime indicates the time at which the signature was generated. SigningTime time.Time // Expiry provides a “best by use” time for the artifact. Expiry time.Time // additional signed attributes in the signature envelope. ExtendedAttributes []Attribute } // UnsignedAttributes represents unsigned metadata in the Signature envelope. // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#unsigned-attributes type UnsignedAttributes struct { // TimestampSignature is a counter signature providing authentic timestamp. TimestampSignature []byte // SigningAgent provides the identifier of the software (e.g. Notation) that // produces the signature on behalf of the user. SigningAgent string } // Attribute represents metadata in the Signature envelope. type Attribute struct { // Key is the key name of the attribute. Key any // Critical marks the attribute that MUST be processed by a verifier. Critical bool // Value is the value of the attribute. Value any } // SignRequest is used to generate Signature. type SignRequest struct { // Payload is the payload to be signed. // // For JWS envelope, Payload.Content is limited to be JSON format. Payload Payload // Signer is the signer used to sign the digest. Signer Signer // SigningTime is the time at which the signature was generated. SigningTime time.Time // Expiry provides a “best by use” time for the artifact. Expiry time.Time // ExtendedSignedAttributes is additional signed attributes in the // signature envelope. ExtendedSignedAttributes []Attribute // SigningAgent provides the identifier of the software (e.g. Notation) // that produced the signature on behalf of the user. SigningAgent string // SigningScheme defines the Notary Project Signing Scheme used by the signature. SigningScheme SigningScheme // Timestamper denotes the timestamper for RFC 3161 timestamping Timestamper tspclient.Timestamper // TSARootCAs is the set of caller trusted TSA root certificates TSARootCAs *x509.CertPool // ctx is the caller context. It should only be modified via WithContext. // It is unexported to prevent people from using Context wrong // and mutating the contexts held by callers of the same request. ctx context.Context } // Context returns the SignRequest's context. To change the context, use // [SignRequest.WithContext]. // // The returned context is always non-nil; it defaults to the // background context. func (r *SignRequest) Context() context.Context { if r.ctx != nil { return r.ctx } return context.Background() } // WithContext returns a shallow copy of r with its context changed // to ctx. The provided ctx must be non-nil. func (r *SignRequest) WithContext(ctx context.Context) *SignRequest { if ctx == nil { panic("nil context") } r2 := new(SignRequest) *r2 = *r r2.ctx = ctx return r2 } // EnvelopeContent represents a combination of payload to be signed and a parsed // signature envelope. type EnvelopeContent struct { // SignerInfo is a parsed signature envelope. SignerInfo SignerInfo // Payload is payload to be signed. Payload Payload } // SignerInfo represents a parsed signature envelope that is agnostic to // signature envelope format. type SignerInfo struct { // SignedAttributes are additional metadata required to support the // signature verification process. SignedAttributes SignedAttributes // UnsignedAttributes are considered unsigned with respect to the signing // key that generates the signature. UnsignedAttributes UnsignedAttributes // SignatureAlgorithm defines the signature algorithm. SignatureAlgorithm Algorithm // CertificateChain is an ordered list of X.509 public certificates // associated with the signing key used to generate the signature. // The ordered list starts with the signing certificates, any intermediate // certificates and ends with the root certificate. CertificateChain []*x509.Certificate // Signature is the bytes generated from the signature. Signature []byte } // Payload represents payload in bytes and its content type. type Payload struct { // ContentType specifies the content type of payload. ContentType string // Content contains the raw bytes of the payload. // // For JWS envelope, Content is limited to be JSON format. Content []byte } // ExtendedAttribute fetches the specified Attribute with provided key from // signerInfo.SignedAttributes.ExtendedAttributes. func (signerInfo *SignerInfo) ExtendedAttribute(key string) (Attribute, error) { for _, attr := range signerInfo.SignedAttributes.ExtendedAttributes { if attr.Key == key { return attr, nil } } return Attribute{}, errors.New("key not in ExtendedAttributes") } // AuthenticSigningTime returns the authentic signing time under signing scheme // notary.x509.signingAuthority. // For signing scheme notary.x509, since it only supports authentic timestamp, // an error is returned. // // Reference: https://github.com/notaryproject/specifications/blob/3b0743cd9bb99faee60600dc31d706149775fd49/specs/signature-specification.md#signing-time--authentic-signing-time func (signerInfo *SignerInfo) AuthenticSigningTime() (time.Time, error) { switch signingScheme := signerInfo.SignedAttributes.SigningScheme; signingScheme { case SigningSchemeX509SigningAuthority: signingTime := signerInfo.SignedAttributes.SigningTime if signingTime.IsZero() { return time.Time{}, fmt.Errorf("authentic signing time must be present under signing scheme %q", signingScheme) } return signingTime, nil default: return time.Time{}, fmt.Errorf("authentic signing time not supported under signing scheme %q", signingScheme) } } notation-core-go-1.1.0/signature/types_test.go000066400000000000000000000050041466321423000214300ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package signature import ( "context" "fmt" "testing" "time" ) func TestSignRequestContext(t *testing.T) { r := &SignRequest{ ctx: context.WithValue(context.Background(), "k1", "v1"), } ctx := r.Context() if ctx.Value("k1") != "v1" { t.Fatal("expected k1:v1 in ctx") } r = &SignRequest{} ctx = r.Context() if fmt.Sprint(ctx) != "context.Background" { t.Fatal("expected context.Background") } } func TestSignRequestWithContext(t *testing.T) { r := &SignRequest{} ctx := context.WithValue(context.Background(), "k1", "v1") r = r.WithContext(ctx) if r.ctx.Value("k1") != "v1" { t.Fatal("expected k1:v1 in request ctx") } defer func() { if rc := recover(); rc == nil { t.Errorf("expected to be panic") } }() r.WithContext(nil) // should panic } func TestAuthenticSigningTime(t *testing.T) { testTime := time.Now() signerInfo := SignerInfo{ SignedAttributes: SignedAttributes{ SigningScheme: "notary.x509.signingAuthority", SigningTime: testTime, }, } authenticSigningTime, err := signerInfo.AuthenticSigningTime() if err != nil { t.Fatal(err) } if !authenticSigningTime.Equal(testTime) { t.Fatalf("expected %s, but got %s", testTime, authenticSigningTime) } signerInfo = SignerInfo{ SignedAttributes: SignedAttributes{ SigningScheme: "notary.x509.signingAuthority", }, } expectedErrMsg := "authentic signing time must be present under signing scheme \"notary.x509.signingAuthority\"" _, err = signerInfo.AuthenticSigningTime() if err == nil || err.Error() != expectedErrMsg { t.Fatalf("expected %s, but got %s", expectedErrMsg, err) } signerInfo = SignerInfo{ SignedAttributes: SignedAttributes{ SigningScheme: "notary.x509", }, } expectedErrMsg = "authentic signing time not supported under signing scheme \"notary.x509\"" _, err = signerInfo.AuthenticSigningTime() if err == nil || err.Error() != expectedErrMsg { t.Fatalf("expected %s, but got %s", expectedErrMsg, err) } } notation-core-go-1.1.0/testhelper/000077500000000000000000000000001466321423000170555ustar00rootroot00000000000000notation-core-go-1.1.0/testhelper/certificatetest.go000066400000000000000000000264411466321423000225750ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package testhelper implements utility routines required for writing unit tests. // The testhelper should only be used in unit tests. package testhelper import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "fmt" "math/big" mrand "math/rand" "strconv" "sync" "time" "github.com/notaryproject/notation-core-go/internal/oid" ) var ( rsaRoot RSACertTuple rsaLeaf RSACertTuple revokableRSALeaf RSACertTuple rsaLeafWithoutEKU RSACertTuple ecdsaRoot ECCertTuple ecdsaLeaf ECCertTuple unsupportedECDSARoot ECCertTuple unsupportedRSARoot RSACertTuple rsaSelfSignedSigningCert RSACertTuple ) var setupCertificatesOnce sync.Once type RSACertTuple struct { Cert *x509.Certificate PrivateKey *rsa.PrivateKey } type ECCertTuple struct { Cert *x509.Certificate PrivateKey *ecdsa.PrivateKey } // GetRSARootCertificate returns root certificate signed using RSA algorithm func GetRSARootCertificate() RSACertTuple { setupCertificates() return rsaRoot } // GetRSALeafCertificate returns leaf certificate signed using RSA algorithm func GetRSALeafCertificate() RSACertTuple { setupCertificates() return rsaLeaf } // GetRevokableRSALeafCertificate returns leaf certificate that specifies a local OCSP server signed using RSA algorithm func GetRevokableRSALeafCertificate() RSACertTuple { setupCertificates() return revokableRSALeaf } // GetRevokableRSAChain returns a chain of certificates that specify a local OCSP server signed using RSA algorithm func GetRevokableRSAChain(size int) []RSACertTuple { setupCertificates() chain := make([]RSACertTuple, size) chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) for i := size - 2; i > 0; i-- { chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) } if size > 1 { chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false) } return chain } // GetRevokableRSATimestampChain returns a chain of certificates that specify a local OCSP server signed using RSA algorithm. // The leaf certificate is a timestamp certificate. func GetRevokableRSATimestampChain(size int) []RSACertTuple { setupCertificates() chain := make([]RSACertTuple, size) chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1) for i := size - 2; i > 0; i-- { chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i) } if size > 1 { chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, false, true) } return chain } // GetRSALeafCertificateWithoutEKU returns leaf certificate without EKU signed using RSA algorithm func GetRSALeafCertificateWithoutEKU() RSACertTuple { setupCertificates() return rsaLeafWithoutEKU } // GetECRootCertificate returns root certificate signed using EC algorithm func GetECRootCertificate() ECCertTuple { setupCertificates() return ecdsaRoot } // GetECLeafCertificate returns leaf certificate signed using EC algorithm func GetECLeafCertificate() ECCertTuple { setupCertificates() return ecdsaLeaf } // GetUnsupportedRSACert returns certificate signed using RSA algorithm with key // size of 1024 bits which is not supported by notary. func GetUnsupportedRSACert() RSACertTuple { setupCertificates() return unsupportedRSARoot } // GetUnsupportedECCert returns certificate signed using EC algorithm with P-224 // curve which is not supported by notary. func GetUnsupportedECCert() ECCertTuple { setupCertificates() return unsupportedECDSARoot } // GetRSASelfSignedSigningCertificate returns a self-signed certificate created which can be used for signing func GetRSASelfSignedSigningCertificate() RSACertTuple { setupCertificates() return rsaSelfSignedSigningCert } func setupCertificates() { setupCertificatesOnce.Do(func() { rsaRoot = getRSACertTuple("Notation Test RSA Root", nil) rsaLeaf = getRSACertTuple("Notation Test RSA Leaf Cert", &rsaRoot) revokableRSALeaf = getRevokableRSACertTuple("Notation Test Revokable RSA Leaf Cert", &rsaRoot) rsaLeafWithoutEKU = getRSACertWithoutEKUTuple("Notation Test RSA Leaf without EKU Cert", &rsaRoot) ecdsaRoot = getECCertTuple("Notation Test EC Root", nil) ecdsaLeaf = getECCertTuple("Notation Test EC Leaf Cert", &ecdsaRoot) unsupportedECDSARoot = getECCertTupleWithCurve("Notation Test Invalid ECDSA Cert", nil, elliptic.P224()) // This will be flagged by the static code analyzer as 'Use of a weak cryptographic key' but its intentional // and is used only for testing. k, _ := rsa.GenerateKey(rand.Reader, 1024) unsupportedRSARoot = GetRSACertTupleWithPK(k, "Notation Unsupported Root", nil) rsaSelfSignedSigningCert = GetRSASelfSignedSigningCertTuple("Notation Signing Test Root") }) } func getRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple { pk, _ := rsa.GenerateKey(rand.Reader, 3072) return GetRSACertTupleWithPK(pk, cn, issuer) } func getRevokableRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple { template := getCertTemplate(issuer == nil, true, false, cn) template.OCSPServer = []string{"http://example.com/ocsp"} return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int) RSACertTuple { template := getCertTemplate(previous == nil, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} return getRSACertTupleWithTemplate(template, previous.PrivateKey, previous) } func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple { pk, _ := rsa.GenerateKey(rand.Reader, 3072) template := getCertTemplate(true, true, false, cn) template.BasicConstraintsValid = true template.IsCA = true template.KeyUsage = x509.KeyUsageCertSign template.MaxPathLen = pathLen return getRSACertTupleWithTemplate(template, pk, nil) } func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp bool) RSACertTuple { template := getCertTemplate(false, codesign, timestamp, cn) template.BasicConstraintsValid = true template.IsCA = false template.KeyUsage = x509.KeyUsageDigitalSignature template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) } func getRSACertWithoutEKUTuple(cn string, issuer *RSACertTuple) RSACertTuple { pk, _ := rsa.GenerateKey(rand.Reader, 3072) template := getCertTemplate(issuer == nil, false, false, cn) return getRSACertTupleWithTemplate(template, pk, issuer) } func getECCertTupleWithCurve(cn string, issuer *ECCertTuple, curve elliptic.Curve) ECCertTuple { k, _ := ecdsa.GenerateKey(curve, rand.Reader) return GetECDSACertTupleWithPK(k, cn, issuer) } func getECCertTuple(cn string, issuer *ECCertTuple) ECCertTuple { k, _ := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) return GetECDSACertTupleWithPK(k, cn, issuer) } func GetRSASelfSignedSigningCertTuple(cn string) RSACertTuple { // Even though we are creating self-signed root, we are using false for 'isRoot' to not // add root CA's basic constraint, KU and EKU. template := getCertTemplate(false, true, false, cn) privKey, _ := rsa.GenerateKey(rand.Reader, 3072) return getRSACertTupleWithTemplate(template, privKey, nil) } func GetRSACertTupleWithPK(privKey *rsa.PrivateKey, cn string, issuer *RSACertTuple) RSACertTuple { template := getCertTemplate(issuer == nil, true, false, cn) return getRSACertTupleWithTemplate(template, privKey, issuer) } func GetRSASelfSignedCertTupleWithPK(privKey *rsa.PrivateKey, cn string) RSACertTuple { template := getCertTemplate(false, true, false, cn) return getRSACertTupleWithTemplate(template, privKey, nil) } func getRSACertTupleWithTemplate(template *x509.Certificate, privKey *rsa.PrivateKey, issuer *RSACertTuple) RSACertTuple { var certBytes []byte if issuer != nil { certBytes, _ = x509.CreateCertificate(rand.Reader, template, issuer.Cert, &privKey.PublicKey, issuer.PrivateKey) } else { certBytes, _ = x509.CreateCertificate(rand.Reader, template, template, &privKey.PublicKey, privKey) } cert, _ := x509.ParseCertificate(certBytes) return RSACertTuple{ Cert: cert, PrivateKey: privKey, } } func GetECDSACertTupleWithPK(privKey *ecdsa.PrivateKey, cn string, issuer *ECCertTuple) ECCertTuple { template := getCertTemplate(issuer == nil, true, false, cn) var certBytes []byte if issuer != nil { certBytes, _ = x509.CreateCertificate(rand.Reader, template, issuer.Cert, &privKey.PublicKey, issuer.PrivateKey) } else { certBytes, _ = x509.CreateCertificate(rand.Reader, template, template, &privKey.PublicKey, privKey) } cert, _ := x509.ParseCertificate(certBytes) return ECCertTuple{ Cert: cert, PrivateKey: privKey, } } func getCertTemplate(isRoot bool, setCodeSignEKU, setTimestampEKU bool, cn string) *x509.Certificate { template := &x509.Certificate{ Subject: pkix.Name{ Organization: []string{"Notary"}, Country: []string{"US"}, Province: []string{"WA"}, Locality: []string{"Seattle"}, CommonName: cn, }, NotBefore: time.Now(), KeyUsage: x509.KeyUsageDigitalSignature, } if setCodeSignEKU { template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning} } else if setTimestampEKU { ekuValue, _ := asn1.Marshal([]asn1.ObjectIdentifier{oid.Timestamping}) template.ExtraExtensions = []pkix.Extension{ { Id: oid.ExtKeyUsage, Critical: true, Value: ekuValue, }, } } if isRoot { template.SerialNumber = big.NewInt(1) template.NotAfter = time.Now().AddDate(0, 1, 0) template.KeyUsage = x509.KeyUsageCertSign template.BasicConstraintsValid = true template.MaxPathLen = 1 template.IsCA = true } else { template.SerialNumber = big.NewInt(int64(mrand.Intn(200))) template.NotAfter = time.Now().AddDate(0, 0, 1) } return template } func GetRSACertTuple(size int) RSACertTuple { rsaRoot := GetRSARootCertificate() priv, _ := rsa.GenerateKey(rand.Reader, size) certTuple := GetRSACertTupleWithPK( priv, "Test RSA_"+strconv.Itoa(priv.Size()), &rsaRoot, ) return certTuple } func GetECCertTuple(curve elliptic.Curve) ECCertTuple { ecdsaRoot := GetECRootCertificate() priv, _ := ecdsa.GenerateKey(curve, rand.Reader) bitSize := priv.Params().BitSize certTuple := GetECDSACertTupleWithPK( priv, "Test EC_"+strconv.Itoa(bitSize), &ecdsaRoot, ) return certTuple } notation-core-go-1.1.0/testhelper/httptest.go000066400000000000000000000106061466321423000212660ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package testhelper implements utility routines required for writing unit tests. // The testhelper should only be used in unit tests. package testhelper import ( "bytes" "crypto/x509/pkix" "encoding/asn1" "errors" "fmt" "io" "net/http" "regexp" "strconv" "strings" "time" "golang.org/x/crypto/ocsp" ) type spyRoundTripper struct { backupTransport http.RoundTripper certChain []RSACertTuple desiredOCSPStatuses []ocsp.ResponseStatus revokedTime *time.Time validPKIXNoCheck bool } func (s spyRoundTripper) roundTripResponse(index int, expired bool) (*http.Response, error) { // Verify index of cert in chain if index == (len(s.certChain) - 1) { return nil, errors.New("OCSP cannot be performed on root") } else if index > (len(s.certChain) - 1) { return nil, errors.New("index exceeded chain size") } // Get desired status for index var status ocsp.ResponseStatus if index < len(s.desiredOCSPStatuses) { status = s.desiredOCSPStatuses[index] } else if len(s.desiredOCSPStatuses) == 0 { status = ocsp.Good } else { // Use last status from passed statuses status = s.desiredOCSPStatuses[len(s.desiredOCSPStatuses)-1] } // Create template for ocsp response var nextUpdate time.Time if expired { nextUpdate = time.Now().Add(-1 * time.Hour) } else { nextUpdate = time.Now().Add(time.Hour) } template := ocsp.Response{ Status: int(status), SerialNumber: s.certChain[index].Cert.SerialNumber, NextUpdate: nextUpdate, } if status == ocsp.Revoked { if s.revokedTime != nil { template.RevokedAt = *s.revokedTime generalizedTime, _ := asn1.MarshalWithParams(*s.revokedTime, "generalized") template.ExtraExtensions = []pkix.Extension{{Id: asn1.ObjectIdentifier{2, 5, 29, 24}, Critical: false, Value: generalizedTime}} } else { template.RevokedAt = time.Now().Add(-1 * time.Hour) } template.RevocationReason = ocsp.Unspecified } if s.validPKIXNoCheck { template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 5}, Critical: false, Value: nil}) } // Create ocsp response response, err := ocsp.CreateResponse(s.certChain[index].Cert, s.certChain[index+1].Cert, template, s.certChain[index].PrivateKey) if err == nil { return &http.Response{ Body: io.NopCloser(bytes.NewBuffer(response)), StatusCode: http.StatusOK, }, nil } else { return nil, err } } func (s spyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { if match, _ := regexp.MatchString("^\\/ocsp.*", req.URL.Path); match { return s.roundTripResponse(0, false) } else if match, _ := regexp.MatchString("^\\/expired_ocsp.*", req.URL.Path); match { return s.roundTripResponse(0, true) } else if match, _ := regexp.MatchString("^\\/chain_ocsp.*", req.URL.Path); match { // this url works with the revokable chain, which has url structure /chain_ocsp/ or /chain_ocsp// index, err := strconv.Atoi(strings.Split(req.URL.Path, "/")[2]) if err != nil { return nil, err } return s.roundTripResponse(index, false) } else { fmt.Printf("%s did not match a specified path, using default transport", req.URL.Path) return s.backupTransport.RoundTrip(req) } } // Creates a mock HTTP Client (more accurately, a spy) that intercepts requests to /issuer and /ocsp endpoints // The client's responses are dependent on the cert and desiredOCSPStatus func MockClient(certChain []RSACertTuple, desiredOCSPStatuses []ocsp.ResponseStatus, revokedTime *time.Time, validPKIXNoCheck bool) *http.Client { return &http.Client{ Transport: spyRoundTripper{ backupTransport: http.DefaultTransport, certChain: certChain, desiredOCSPStatuses: desiredOCSPStatuses, revokedTime: revokedTime, validPKIXNoCheck: validPKIXNoCheck, }, } } notation-core-go-1.1.0/x509/000077500000000000000000000000001466321423000154035ustar00rootroot00000000000000notation-core-go-1.1.0/x509/cert.go000066400000000000000000000030371466321423000166720ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package x509 import ( "crypto/x509" "encoding/pem" "os" ) // ReadCertificateFile reads a certificate PEM or DER file. func ReadCertificateFile(path string) ([]*x509.Certificate, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } return parseCertificates(data) } // parseCertificates parses certificates from either PEM or DER data // returns an empty list if no certificates are found func parseCertificates(data []byte) ([]*x509.Certificate, error) { var certs []*x509.Certificate block, rest := pem.Decode(data) if block == nil { // data may be in DER format derCerts, err := x509.ParseCertificates(data) if err != nil { return nil, err } certs = append(certs, derCerts...) } else { // data is in PEM format for block != nil { cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, err } certs = append(certs, cert) block, rest = pem.Decode(rest) } } return certs, nil } notation-core-go-1.1.0/x509/cert_test.go000066400000000000000000000032651466321423000177340ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package x509 import ( "crypto/x509" "testing" ) func TestLoadPemFile(t *testing.T) { certs, err := ReadCertificateFile("testdata/pem.crt") verifyNoError(t, err) verifyNumCerts(t, certs, 1) } func TestLoadMultiPemFile(t *testing.T) { certs, err := ReadCertificateFile("testdata/multi-pem.crt") verifyNoError(t, err) verifyNumCerts(t, certs, 2) } func TestLoadDerFile(t *testing.T) { certs, err := ReadCertificateFile("testdata/der.der") verifyNoError(t, err) verifyNumCerts(t, certs, 1) } func TestLoadMultiDerFile(t *testing.T) { certs, err := ReadCertificateFile("testdata/multi-der.der") verifyNoError(t, err) verifyNumCerts(t, certs, 2) } func TestLoadInvalidFile(t *testing.T) { certs, err := ReadCertificateFile("testdata/invalid") if err == nil { t.Fatalf("invalid file should throw an error") } verifyNumCerts(t, certs, 0) } func verifyNoError(t *testing.T, err error) { if err != nil { t.Fatalf("Error : %q", err) } } func verifyNumCerts(t *testing.T, certs []*x509.Certificate, num int) { if len(certs) != num { t.Fatalf("test case should return only %d certificate/s", num) } } notation-core-go-1.1.0/x509/codesigning_cert_validations.go000066400000000000000000000137221466321423000236420ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package x509 import ( "crypto/x509" "errors" "fmt" "time" "github.com/notaryproject/notation-core-go/internal/oid" ) // ValidateCodeSigningCertChain takes an ordered code signing certificate chain // and validates issuance from leaf to root // Validates certificates according to this spec: // https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements func ValidateCodeSigningCertChain(certChain []*x509.Certificate, signingTime *time.Time) error { if len(certChain) < 1 { return errors.New("certificate chain must contain at least one certificate") } // For self-signed signing certificate (not a CA) if len(certChain) == 1 { cert := certChain[0] if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { return signedTimeError } if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: %w", cert.Subject, err) } if err := validateCodeSigningLeafCertificate(cert); err != nil { return fmt.Errorf("invalid self-signed certificate. Error: %w", err) } return nil } for i, cert := range certChain { if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { return signedTimeError } if i == len(certChain)-1 { selfSigned, selfSignedError := isSelfSigned(cert) if selfSignedError != nil { return fmt.Errorf("root certificate with subject %q is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: %v", cert.Subject, selfSignedError) } if !selfSigned { return fmt.Errorf("root certificate with subject %q is not self-signed. Certificate chain must end with a valid self-signed root certificate", cert.Subject) } } else { // This is to avoid extra/redundant multiple root cert at the end // of certificate-chain selfSigned, selfSignedError := isSelfSigned(cert) // not checking selfSignedError != nil here because we expect // a non-nil err. For a non-root certificate, it shouldn't be // self-signed, hence CheckSignatureFrom would return a non-nil // error. if selfSignedError == nil && selfSigned { if i == 0 { return fmt.Errorf("leaf certificate with subject %q is self-signed. Certificate chain must not contain self-signed leaf certificate", cert.Subject) } return fmt.Errorf("intermediate certificate with subject %q is self-signed. Certificate chain must not contain self-signed intermediate certificate", cert.Subject) } parentCert := certChain[i+1] issuedBy, issuedByError := isIssuedBy(cert, parentCert) if issuedByError != nil { return fmt.Errorf("invalid certificates or certificate with subject %q is not issued by %q. Error: %v", cert.Subject, parentCert.Subject, issuedByError) } if !issuedBy { return fmt.Errorf("certificate with subject %q is not issued by %q", cert.Subject, parentCert.Subject) } } if i == 0 { if err := validateCodeSigningLeafCertificate(cert); err != nil { return err } } else { if err := validateCodeSigningCACertificate(cert, i-1); err != nil { return err } } } return nil } func validateCodeSigningCACertificate(cert *x509.Certificate, expectedPathLen int) error { if err := validateCABasicConstraints(cert, expectedPathLen); err != nil { return err } return validateCodeSigningCAKeyUsage(cert) } func validateCodeSigningLeafCertificate(cert *x509.Certificate) error { if err := validateLeafBasicConstraints(cert); err != nil { return err } if err := validateCodeSigningLeafKeyUsage(cert); err != nil { return err } if err := validateCodeSigningExtendedKeyUsage(cert); err != nil { return err } return validateSignatureAlgorithm(cert) } func validateCodeSigningCAKeyUsage(cert *x509.Certificate) error { if err := validateCodeSigningKeyUsagePresent(cert); err != nil { return err } if cert.KeyUsage&x509.KeyUsageCertSign == 0 { return fmt.Errorf("certificate with subject %q: key usage must have the bit positions for key cert sign set", cert.Subject) } return nil } func validateCodeSigningLeafKeyUsage(cert *x509.Certificate) error { if err := validateCodeSigningKeyUsagePresent(cert); err != nil { return err } return validateLeafKeyUsage(cert) } func validateCodeSigningKeyUsagePresent(cert *x509.Certificate) error { var hasKeyUsageExtension bool for _, ext := range cert.Extensions { if ext.Id.Equal(oid.KeyUsage) { if !ext.Critical { return fmt.Errorf("certificate with subject %q: key usage extension must be marked critical", cert.Subject) } hasKeyUsageExtension = true break } } if !hasKeyUsageExtension { return fmt.Errorf("certificate with subject %q: key usage extension must be present", cert.Subject) } return nil } func validateCodeSigningExtendedKeyUsage(cert *x509.Certificate) error { if len(cert.ExtKeyUsage) == 0 { return nil } excludedEkus := []x509.ExtKeyUsage{ x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageEmailProtection, x509.ExtKeyUsageTimeStamping, x509.ExtKeyUsageOCSPSigning, } for _, certEku := range cert.ExtKeyUsage { for _, excludedEku := range excludedEkus { if certEku == excludedEku { return fmt.Errorf("certificate with subject %q: extended key usage must not contain %s eku", cert.Subject, ekuToString(excludedEku)) } } } return nil } notation-core-go-1.1.0/x509/codesigning_cert_validations_test.go000066400000000000000000001131721466321423000247010ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package x509 import ( "crypto/x509" "crypto/x509/pkix" _ "embed" "errors" "os" "testing" "time" "github.com/notaryproject/notation-core-go/internal/oid" "github.com/notaryproject/notation-core-go/testhelper" ) // ---------------- Chain Validations ---------------- var rootCertPem = "-----BEGIN CERTIFICATE-----\n" + "MIICyTCCAbGgAwIBAgIJAMKoxLbsiLVFMA0GCSqGSIb3DQEBCwUAMA8xDTALBgNV\n" + "BAMMBFJvb3QwIBcNMjIwNjMwMTkyMDAyWhgPMjEyMjA2MDYxOTIwMDJaMA8xDTAL\n" + "BgNVBAMMBFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC79rff\n" + "wcHY1g4Y3V89m8wmT9O5EuWzv2CXMRNuDHiAEzYtpCCZNXUzK2tDx0SMm7gSbL5R\n" + "sygeug1xo6B5ItcpS3Jr65sFd8XO/F2g8PRGZH5eZEBF+dogOjP1QgpkHtAtWuZh\n" + "Lc4O9Le6uqLHRm2bFOnyiqSSa/DbXdTXMIabOgVIHHOrDRM+uBYkPqV2PtUnGiNx\n" + "mVSatO/Gd8AMJ3QjuGxiArrMGPn5H0NrhaESbioFET2uHx337KNpSXjYOvI4zqbn\n" + "/E5XQrXk7WFvrrVytSNvoZKe2C3Rkx++LlMo6mGjnV4LmKptHRGEn+G4BxhFfYSF\n" + "cg8i2f/DPUEksEyvAgMBAAGjJjAkMBIGA1UdEwEB/wQIMAYBAf8CAQIwDgYDVR0P\n" + "AQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4IBAQB15AV+zNYU9H6CP4kT15yUaxE1\n" + "X1z5vt5K7yC0KTQgEuwouyhjK74FtGb7DRz1Irmncx9Ev109CCWfQIasJw1NaHCC\n" + "+TB0y7CVet4nawFTVTt3rJoLm3AAAh5EY0cOxSfF+kBSWQAPzBwK4XeeF10fqZde\n" + "r5ArNp1mk1X1GQPWr+bFzuAhOfbyo1rtX3JhTi9aPrH056mIVfnnS/6+jjqOYpeJ\n" + "EE2d/AqAytdgXIWq0Y/x/wymXgVINK2NEs1ajRyLPc9uGopZZFKyteqSbIk5H1PM\n" + "iVADu+Kjj+JocaQ4vRFSmR+5DGnLdBkP+woioprEIYD42nn7vW0yAZcuLnmo\n" + "-----END CERTIFICATE-----" var intermediateCertPem1 = "-----BEGIN CERTIFICATE-----\n" + "MIICyjCCAbKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDDARSb290\n" + "MCAXDTIyMDYzMDE5MjAwM1oYDzMwMjExMDMxMTkyMDAzWjAYMRYwFAYDVQQDDA1J\n" + "bnRlcm1lZGlhdGUxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1JTs\n" + "aiC/7+bho43kMVyHDwCsuocYp4PvYahB59NsKDR4QbrImU5ziaQ94D0DQqthe9pm\n" + "qOW0SxN/vSRJAZFELxacrB9hc1y4MjiDYaRSt/LVx7astylBV/QRpmxWSEqp0Avu\n" + "6nMJivIa1sD0WIEchizx6jG9BI5ULr9LbJICYvMgDalQR+0JGG+rKWnf1mPZyxEu\n" + "9zEh215LCg5K56P3W5kC8fKBXSdSgTqZAvHzp6u78qet9S8gARtOEfS03A/7y7MC\n" + "U0Sn2wdQyQdci0PBsR2sTZvUw179Cr93r5aRbb3I6jXgMWHAP2vvIndb9CM9ePyY\n" + "yEy4Je7oWVVfMQ3CWQIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/AgEBMA4GA1Ud\n" + "DwEB/wQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEALR0apUQVbWGmagLUz4Y/bRsl\n" + "mY9EJJXCiLuSxVWd3offjZfQTlGkQkCAW9FOQnm7JhEtaaHF1+AEVLo56/Gsd/hk\n" + "sXsrBagYGi72jun7QTb6j7iZ3X9zanrP3SjdkpjVnqxRfH83diSh0r68Xruq1NSK\n" + "qhUy1V+KQaXF0SSEutPqdTCoXUyxyXohVLU78uqZX/jx9Nc1XDuW9AZd+hMsLdk8\n" + "qGJqHYFvj2vOHGMTeYk8dWgMBthQeL0wdsg2AvKtAvn6FQXCN7mKCWjpFTtYsU8v\n" + "NsesS9M/i+geJjR/8/DDT3RP7S100BtCMm4XfHfmKcjXVaBh5evQVqGsa6TKLw==\n" + "-----END CERTIFICATE-----" var intermediateCertPem2 = "-----BEGIN CERTIFICATE-----\n" + "MIIC0zCCAbugAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1JbnRl\n" + "cm1lZGlhdGUxMCAXDTIyMDYzMDE5MjAwM1oYDzMwMjExMDMxMTkyMDAzWjAYMRYw\n" + "FAYDVQQDDA1JbnRlcm1lZGlhdGUyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\n" + "CgKCAQEAxH57OcIDpmuHgZ3y78HpyfNHVy0JwIpIp1quSBN5SHRkzouh+LcuVjic\n" + "/1DGwiut312XeIyKoeOLcNnsY1qfZgxtFxJCfZSArnyoHb6O0vRvUq/yY1cjOZea\n" + "J4U/ZsSPEt4S5oFApWLGFH6c7sRNmh3bPcPDsm1gNd+gM/UCSyCH62gmRn3r5nKA\n" + "4fkwrs46tBGDs+bwwj5/AupJETX4P+NaFE7XcAJP6ShMAGa/ykunyEvDsc8tdzhD\n" + "zvoyWRxMjrTZlAu+5THbz4ZgRZja2noQDGoV5g9QMzebLbAS/+YY+OJfGHtA0li8\n" + "THw5ZzButCmk+Us49FlN0MlyDC4oNwIDAQABoyYwJDASBgNVHRMBAf8ECDAGAQH/\n" + "AgEAMA4GA1UdDwEB/wQEAwICBDANBgkqhkiG9w0BAQsFAAOCAQEADbd56yUDfUCQ\n" + "pahXOS0OYBJ9GB+PRdp6lkvZTNPfu5cynZwla1juZFee71w+fcppSzpecGu8esLO\n" + "h9+1RooWKGqJpLoAvvUJMW6kZsGBTPjpSeH6xUP9HwxfWrZwg3FMMGMIzOs90jCZ\n" + "47U6CevxAGTtkvO8QMIQOx9VNcUDjX1utlkyCdAHccZLq2gw9wWHSfZWydKXpJVP\n" + "ffDPsF4LkjJb7XHFB8KOxYjvyomLXGTNlni1hRxadSKrRX9xeAztIZ1ReFgYVRQn\n" + "8TwCIeaN4N2TNJWeVmBSnYU7iuay6A/qkauuG2+Hc7eL834IzRejYpecoCjBwQFT\n" + "6OInMQCKnA==\n" + "-----END CERTIFICATE-----" var codeSigningLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIIC5DCCAcygAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1JbnRl\n" + "cm1lZGlhdGUyMCAXDTIyMDYzMDE5MjAwM1oYDzMwMjExMDMxMTkyMDAzWjAaMRgw\n" + "FgYDVQQDDA9Db2RlU2lnbmluZ0xlYWYwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\n" + "ggEKAoIBAQCfySlwm2lF1eMP8RZVjG1PAp6wJiqNfI1m4Oll5jZFBDPLFqUJFG2i\n" + "Zun5GecxJD8mz56AxB95vohQd1+AkPXE7bCpN085hQm3jMbbdg0N0HS+cAATGUDR\n" + "VEi/laHLSs8myuG9enJ1/EIGli8hZnOeSW46RaHtlawPbIXa8/8yV1McmrQjOOqj\n" + "K+m1Rra2J3apyqUL37K6MrydoLIy/ldvuGbfMDrsRZVu6GbtNMyV+6qwc91NL0aa\n" + "g67ge3LaQ4VcLXFSCYpbNzBMl+xBYGLFS4EgNe3VT0HOfOwYn7hcwRF7I0jmUBgH\n" + "BTP2yGYKuobDMslaK+FHisptT/qn29ihAgMBAAGjNTAzMA4GA1UdDwEB/wQEAwIH\n" + "gDAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMA0GCSqGSIb3DQEB\n" + "CwUAA4IBAQB8BAQTnqDkm4K4l0W6a26gl+usPmKzOsrFuKCbeAjUuNMOEcnignO0\n" + "URPXvXBEbQGMyNNmS7ix7JjU4BqbM4KSFfIXrCWvdHZTicWl+1+84HVktwmW2bIg\n" + "xJPo+m1ZLAsRLnBFmf27p7QBYVCYUvNKvbAqgP9rOPtTOkHe2WtiVNAGxDvWBdKr\n" + "gHcqUwRA3v7VfmW9EDoxLvkI9R0HolbiYQzp7GmA+KT5L/CMd50+2fUGaUnaacrU\n" + "v8kypIYx5OTOGTYisidXueUhhbp6RZYvpiQuX+O/bkIjSPMf+oXgbDcpRe18XeK4\n" + "cwtsQn/iENuvFcfRHcFhvRjEFrIP+Ugx\n" + "-----END CERTIFICATE-----" var unrelatedCertPem = "-----BEGIN CERTIFICATE-----\n" + "MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOZe\n" + "9zjKWNlFD/HGrkaAI9mh9Fw1gF8S2tphQD/aPd9IS4HJJEQRkKz5oeHj2g1Y6TEk\n" + "plODrKlnoLe+ZFNFFD4xMVV55aQSJDTljCLPwIZt2VewlaAhIImYihOJvJFST1zW\n" + "K2NW4eLxt0awbE/YzL6beH4A6UsrcXcnN0KKiu6YD1/d5TezJoTQBMo6fboltuce\n" + "P/+RMxyqpvip7nyFF3Yrmhumb7DKJrmSfSjdziI5QoUqzqVgqJ8pXMRb3ZOKb499\n" + "d9RRxGkox93iOdSSlaP3FEl8VK9KqnD+MNhjVZbeYTfjm9UVdp91VLP1E/yfMXz+\n" + "fZhYkublK6v3GWSEcb0CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + "MCgGCCsGAQUFBwMIBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + "SIb3DQEBCwUAA4IBAQCaQZ+ws93F1azT6SKBYvBRBCj07+2DtNI83Q53GxrVy2vU\n" + "rP1ULX7beY87amy6kQcqnQ0QSaoLK+CDL88pPxR2PBzCauz70rMRY8O/KrrLcfwd\n" + "D5HM9DcbneqXQyfh0ZQpt0wK5wux0MFh2sAEv76jgYBMHq2zc+19skAW/oBtTUty\n" + "i/IdOVeO589KXwJzEJmKiswN9zKo9KGgAlKS05zohjv40AOCAs+8Q2lOJjRMq4Ji\n" + "z21qor5e/5+NnGY+2p4A7PbN+QnDdRC3y16dESRN50o5x6CwUWQO74+uRjrAWYCm\n" + "f/Y7qdOf5zZbY21n8KnLcFOsKhwv4t40Y/LQqN/L\n" + "-----END CERTIFICATE-----" var intermediateCertInvalidPathLenPem = "-----BEGIN CERTIFICATE-----\n" + "MIIC1TCCAb2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1JbnRl\n" + "cm1lZGlhdGUyMCAXDTIyMDYzMDIwMDY0OVoYDzMwMjExMDMxMjAwNjQ5WjAaMRgw\n" + "FgYDVQQDDA9BbHRJbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\n" + "ggEKAoIBAQDDyujPemQdVtV5yoWk334MPwaj8kFcquvcNTQXX/Jx5F7IZD6E0E7F\n" + "2bgGdwxAwzhrZni+aMJtyT0YX9Kgi0Tm+86hBdN0gfNcQojr3qdB2CUnAwaNStn+\n" + "DaF5kw+Pg6WQE8k9yYMkavmbdegyvzSComtksyUYDtU6V/eBuHIIFviDrK0lrbhs\n" + "VTtuA0OWoUq3uv+TMEFKpak/XQ9vgor1CKORS16r/OgPTPpHzVibw/gjrwh5Ex1g\n" + "cNcsing92AMJxeT1UqOK6luVOwXmSD5Ixn8Ls2wWtgUtlszyA1E+5UHyfwbz6cdC\n" + "H4vdRJsFa8dEaA/+H50iHVslakL86YRrAgMBAAGjJjAkMBIGA1UdEwEB/wQIMAYB\n" + "Af8CAQAwDgYDVR0PAQH/BAQDAgIEMA0GCSqGSIb3DQEBCwUAA4IBAQBVZ8oMemPX\n" + "CegjTJJ302qmW1PlwkQ6w0CSIaw9lXbG8g8IsDCh/PJCJFwZN0LcW/wjPmMHWulS\n" + "xblyIQbUM6Y7jS7/YmmvHW2A7SiANHSGHKb0AwsAZRbuoOXYxyr0D3QIIktf65xC\n" + "qRpkoTuKpPdEer8wTvq1SN4B/4/VbdeIk7RFw2XAADnMDbcsqvU2O3SX+zl4yHev\n" + "vsoj63SWGIH4Sk8TUBo/s3WyuSWMEUs/wW71w9Yeo57K8+X/hTaC2GEUtcsdPSGL\n" + "E31rl9wvwouARTBAopy3ocw+hEwuwMIH8mhf5qih8TIwG10yV1tR2kgmvhgGICQN\n" + "dhS0BR1O8DCe\n" + "-----END CERTIFICATE-----" var codeSigningLeafInvalidPathLenPem = "-----BEGIN CERTIFICATE-----\n" + "MIIC6TCCAdGgAwIBAgIBATANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9BbHRJ\n" + "bnRlcm1lZGlhdGUwIBcNMjIwNjMwMjAwNjUwWhgPMzAyMTEwMzEyMDA2NTBaMB0x\n" + "GzAZBgNVBAMMEkFsdENvZGVTaWduaW5nTGVhZjCCASIwDQYJKoZIhvcNAQEBBQAD\n" + "ggEPADCCAQoCggEBAPAi9EWIguIIxXSIcJpU0+LZoKjB2ZGPJJOoY+0jzlMRdCLy\n" + "olU4Frmnke5+cVyTdHB/2f26eKZJe5iNCZi0EYjUUzuumnhDPvUxXkt1Qz46CGyE\n" + "X0oc0pfNz/jtCbK4gR8sm6qp5S/wXcrKStN06MKYbPARRdKuS4kQklylwDcyAXo6\n" + "Se47EdqLP+Vav8++Oj/L0fQbDs06s+N+mChgZA4VVClgxQkrslqM7wasJS2wLe1N\n" + "VFL3oDbmtAei+sV5PmLaesjb6LXi4VNu3MPMHbR3h3WzG19S590Ob81TlhvIcciH\n" + "FsWWU8PdKx1KWlwpht718r6Fzi+lS81bCwWtMEkCAwEAAaM1MDMwDgYDVR0PAQH/\n" + "BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZI\n" + "hvcNAQELBQADggEBAJ73158p7+5OIo5JNK7gW2e7EM/2659tuaKZ3HWTzkDrgw9O\n" + "YKQhpl1QizFCAgps/DTy+nBzrt5o4Rl9jUPKSvfAWGOGhb2MYNre9szKARhVg3ij\n" + "rGhhUtlpCUAHBZRdKUvXkBGbp+FTjZuYdsN1HBXJ5aZkwVi7P9JQjTn406DOoPgy\n" + "Fr4upDTYzskjFIF2tMwCkfy9Mc2KAV20wtGnr6j2PIqrhwE3DClMptUS5KfL0wp5\n" + "HasYvDIttHtgGR8lFTda4KvldG2u0t7E8glAb/n2oNQ1tsDojR/9Njnp+ZmmrJNi\n" + "L0Z9ZU3NwwFAh1wpSsoQR4pPN+ZkVj5Irr/KWFk=\n" + "-----END CERTIFICATE-----" //go:embed testdata/openssl-minimum-self-signed.pem var openSSLMinimumPem string var rootCert = parseCertificateFromString(rootCertPem) var intermediateCert1 = parseCertificateFromString(intermediateCertPem1) var intermediateCert2 = parseCertificateFromString(intermediateCertPem2) var codeSigningCert = parseCertificateFromString(codeSigningLeafPem) var unrelatedCert = parseCertificateFromString(unrelatedCertPem) var intermediateCertInvalidPathLen = parseCertificateFromString(intermediateCertInvalidPathLenPem) var codeSigningLeafInvalidPathLen = parseCertificateFromString(codeSigningLeafInvalidPathLenPem) var openSSLMinimumCert = parseCertificateFromString(openSSLMinimumPem) func TestValidCodeSigningChain(t *testing.T) { testCases := []struct { name string certChain []*x509.Certificate }{ {"cert-chain", []*x509.Certificate{codeSigningCert, intermediateCert2, intermediateCert1, rootCert}}, {"self-signed signing certificate", []*x509.Certificate{testhelper.GetRSASelfSignedSigningCertificate().Cert}}, {"RSA Leaf certificate without EKU", []*x509.Certificate{testhelper.GetRSALeafCertificateWithoutEKU().Cert, testhelper.GetRSARootCertificate().Cert}}, {"Open SSL minimum certificate", []*x509.Certificate{openSSLMinimumCert}}, } signingTime := time.Now() for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if err := ValidateCodeSigningCertChain(tc.certChain, &signingTime); err != nil { t.Error(err) } }) } } func TestFailEmptyChain(t *testing.T) { signingTime := time.Now() err := ValidateCodeSigningCertChain(nil, &signingTime) assertErrorEqual("certificate chain must contain at least one certificate", err, t) } func TestFailInvalidSigningTime(t *testing.T) { certChain := []*x509.Certificate{codeSigningCert, intermediateCert2, intermediateCert1, rootCert} st := time.Unix(1625690922, 0) err := ValidateCodeSigningCertChain(certChain, &st) assertErrorEqual("certificate with subject \"CN=CodeSigningLeaf\" was invalid at signing time of 2021-07-07 20:48:42 +0000 UTC. Certificate is valid from [2022-06-30 19:20:03 +0000 UTC] to [3021-10-31 19:20:03 +0000 UTC]", err, t) } func TestValidateSigningTime(t *testing.T) { // codeSigningCert is valid from 2022-06-30 19:20:03 +0000 UTC to 3021-10-31 19:20:03 +0000 UTC testCases := []struct { name string certChain *x509.Certificate signingTime time.Time expectErr string }{ {"invalid before certificate period", codeSigningCert, time.Date(2022, 6, 29, 0, 0, 0, 0, time.UTC), "certificate with subject \"CN=CodeSigningLeaf\" was invalid at signing time of 2022-06-29 00:00:00 +0000 UTC. Certificate is valid from [2022-06-30 19:20:03 +0000 UTC] to [3021-10-31 19:20:03 +0000 UTC]"}, {"invalid after certificate period", codeSigningCert, time.Date(3021, 11, 1, 0, 0, 0, 0, time.UTC), "certificate with subject \"CN=CodeSigningLeaf\" was invalid at signing time of 3021-11-01 00:00:00 +0000 UTC. Certificate is valid from [2022-06-30 19:20:03 +0000 UTC] to [3021-10-31 19:20:03 +0000 UTC]"}, {"valid in certificate period", codeSigningCert, time.Date(2023, 10, 10, 0, 0, 0, 0, time.UTC), ""}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if err := validateSigningTime(tc.certChain, &tc.signingTime); err != nil { assertErrorEqual(tc.expectErr, err, t) } }) } } func TestFailChainNotEndingInRoot(t *testing.T) { certChain := []*x509.Certificate{codeSigningCert, intermediateCert2, intermediateCert1} signingTime := time.Now() err := ValidateCodeSigningCertChain(certChain, &signingTime) expected := "root certificate with subject \"CN=Intermediate1\" is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: crypto/rsa: verification error" assertErrorEqual(expected, err, t) } func TestFailChainNotOrdered(t *testing.T) { certChain := []*x509.Certificate{codeSigningCert, intermediateCert1, intermediateCert2, rootCert} signingTime := time.Now() err := ValidateCodeSigningCertChain(certChain, &signingTime) expected := "invalid certificates or certificate with subject \"CN=CodeSigningLeaf\" is not issued by \"CN=Intermediate1\". Error: crypto/rsa: verification error" assertErrorEqual(expected, err, t) } func TestFailChainWithUnrelatedCert(t *testing.T) { certChain := []*x509.Certificate{codeSigningCert, unrelatedCert, intermediateCert1, rootCert} signingTime := time.Now() err := ValidateCodeSigningCertChain(certChain, &signingTime) expected := "invalid certificates or certificate with subject \"CN=CodeSigningLeaf\" is not issued by \"CN=Hello\". Error: x509: invalid signature: parent certificate cannot sign this kind of certificate" assertErrorEqual(expected, err, t) } func TestFailChainWithSelfSignedLeafCertificate(t *testing.T) { certChain := []*x509.Certificate{rootCert, rootCert, rootCert} signingTime := time.Now() err := ValidateCodeSigningCertChain(certChain, &signingTime) expected := "leaf certificate with subject \"CN=Root\" is self-signed. Certificate chain must not contain self-signed leaf certificate" assertErrorEqual(expected, err, t) } func TestFailChainWithSelfSignedIntermediateCertificate(t *testing.T) { certChain := []*x509.Certificate{codeSigningCert, intermediateCert2, intermediateCert1, rootCert, rootCert} signingTime := time.Now() err := ValidateCodeSigningCertChain(certChain, &signingTime) expected := "intermediate certificate with subject \"CN=Root\" is self-signed. Certificate chain must not contain self-signed intermediate certificate" assertErrorEqual(expected, err, t) } func TestFailInvalidPathLen(t *testing.T) { certChain := []*x509.Certificate{codeSigningLeafInvalidPathLen, intermediateCertInvalidPathLen, intermediateCert2, intermediateCert1, rootCert} signingTime := time.Now() err := ValidateCodeSigningCertChain(certChain, &signingTime) assertErrorEqual("certificate with subject \"CN=Intermediate2\": expected path length of 1 but certificate has path length 0 instead", err, t) } func TestRootCertIdentified(t *testing.T) { selfSignedCodeSigning, _ := isSelfSigned(codeSigningCert) selfSignedIntermediateCert1, _ := isSelfSigned(intermediateCert1) selfSignedIntermediateCert2, _ := isSelfSigned(intermediateCert2) selfSignedRootCert, _ := isSelfSigned(rootCert) if selfSignedCodeSigning || selfSignedIntermediateCert1 || selfSignedIntermediateCert2 || !selfSignedRootCert { t.Fatal("Root cert was not correctly identified") } } func TestInvalidSelfSignedSigningCertificate(t *testing.T) { certChain := []*x509.Certificate{testhelper.GetRSARootCertificate().Cert} signingTime := time.Now() err := ValidateCodeSigningCertChain(certChain, &signingTime) expected := "invalid self-signed certificate. Error: certificate with subject \"CN=Notation Test RSA Root,O=Notary,L=Seattle,ST=WA,C=US\": if the basic constraints extension is present, the ca field must be set to false" assertErrorEqual(expected, err, t) } // ---------------- CA Validations ---------------- func TestValidCa(t *testing.T) { if err := validateCodeSigningCACertificate(rootCert, 2); err != nil { t.Fatal(err) } } func TestFailInvalidPathLenCa(t *testing.T) { err := validateCodeSigningCACertificate(rootCert, 3) assertErrorEqual("certificate with subject \"CN=Root\": expected path length of 3 but certificate has path length 2 instead", err, t) } var noBasicConstraintsCaPem = "-----BEGIN CERTIFICATE-----\n" + "MIICtzCCAZ+gAwIBAgIJAOhRHhdRoeaNMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAxMzAxNVoYDzIxMjIwNjAxMDEzMDE1WjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN3s\n" + "ujhpbbJgOmy3bPdnE0JSSH/i3+TwwR0+/g2qTt3cOHC/X59/w03julru5HQYSUqG\n" + "Q4OlngVb2WoujuVPWwyYjP8nuhSUnWl6vhI8zrO300dhLtJvSa62bKOcmtseeWCo\n" + "KerJXHYqYWl+2nSyVI6x3yuXnJOnS68YAaZ69nqRgu8Cym/144DatzmY2TGuz7bm\n" + "TyP4CDWuUulxIOdp6GZg2raIRi//SJcA1UFjBx0n7H8XUcKV2xAO4e8xSK9KTMEm\n" + "QRA9H2pyT1QDaMeewbzhMEdWDwDnmkZPaWOXIIQ+x1eSvlr/6cQSIaBGKMkUMkq4\n" + "qX98M1dtth0BrhK22jkCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgIEMA0GCSqGSIb3\n" + "DQEBCwUAA4IBAQCyYgO+VA0+6VPz4cndEIsE1NZSFQwxNcKzC4esGivyBIadoH7U\n" + "lNMKw01UUpRUcIiehVCL11FGwMsJ08sDdMB7i/2UwCwodyT/6/6oFm08nUaiC/nV\n" + "ryMcDwyregNLSbK7GgHNidSBSXnFmvFAkUSvIUvoulGtH9B8MkdU3SmT7VFf9WQ1\n" + "lrh/nTAnUlTh48Mn4zpf96VLIw2d42Oq96pYYZy6k+/2h2zph9CuwBvK5cJjDLY3\n" + "3BCBV/ffCxM0v2mclr/CMXNvxVbtgGnQJVjoaccLRXS7C7+CbrP/bimszyyMRGcs\n" + "WkOh2T0tPw9Fr1zsyFzLeJ9A9zEr8tVW0qi3\n" + "-----END CERTIFICATE-----" var noBasicConstraintsCa = parseCertificateFromString(noBasicConstraintsCaPem) func TestFailNoBasicConstraintsCa(t *testing.T) { err := validateCodeSigningCACertificate(noBasicConstraintsCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": ca field in basic constraints must be present, critical, and set to true", err, t) } var basicConstraintsNotCaPem = "-----BEGIN CERTIFICATE-----\n" + "MIICyDCCAbCgAwIBAgIJAJcTAoRqhzWEMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAxMzMwNloYDzIxMjIwNjAxMDEzMzA2WjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKIk\n" + "nuaT8Ho7UM9kYJVWWEINS51/glLHj0Zqdu+Qa4gbzd3FjQBfjpyilJBDm7bhuxHu\n" + "UOeqPnn9QXAX9WLiR6NPXeaMuWG/D98gJ8+jMmOc+Xw6Nicc8w3rHFz3ZscOuCII\n" + "zcXLjh+GhW+1uwwOGalnud1yXs17gP1cYT3qSMIfsrNrCLMZfvmvI0LGYEAuLJLB\n" + "OhYiP0jq10vXAEsiwHKC24PGM3u+kQ35CsAEvUFlSLoKx0qkYvhfBsGfoisTXTJt\n" + "5kdnmxB1z0qCYvsub7/pID7WjnD2dFGfkEjF34wTM8Re6wrHl7ibk4Y3ojCurfVv\n" + "/LTp3QZuiS79zla7pGkCAwEAAaMjMCEwDwYDVR0TAQH/BAUwAwIBAzAOBgNVHQ8B\n" + "Af8EBAMCAgQwDQYJKoZIhvcNAQELBQADggEBAEJA6l1vPSnnpyONee2yAcN8nYR8\n" + "wMgv4KNfGKzD3ZBPofoFxN+rSbB6KyeRp9x5ot3vxfVTWuayMssga/BKQl9+Lbp2\n" + "Q2QMpGVgbCrJ3EQO7kADJroChncKs05nDTeZW84mwurkoQXKWlM7JVYgfnKfCpA9\n" + "CaK9Eeeyxpo2Hjgix1y5ml4L35+LUgP3yNZWE3RzyGAb28pgy0SqViBD9UQYa5xa\n" + "hPVr+C/Ukfl2UCzG6DW4kHNHyY1W0C7eYVN+YQiTlqzvqc7qkRsfq1cqVPEYKmzF\n" + "UMrDhmmzAhKOOW+MxCwWtwHt5m/6eUyal9L27jRdC9tfOfgyMp7kdzSQ/pI=\n" + "-----END CERTIFICATE-----" var basicConstraintsNotCa = parseCertificateFromString(basicConstraintsNotCaPem) func TestFailBasicConstraintsNotCa(t *testing.T) { err := validateCodeSigningCACertificate(basicConstraintsNotCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": ca field in basic constraints must be present, critical, and set to true", err, t) } var kuNotCriticalCertSignCaPem = "-----BEGIN CERTIFICATE-----\n" + "MIICyDCCAbCgAwIBAgIJAKo2P71SNRaxMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAxNDAwMVoYDzIxMjIwNjAxMDE0MDAxWjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsB\n" + "y9q80Va4LFIC1WzpfFLQnf2KqjSeJY9ELooMoZHdLDDr9tDKag58FyNJHQCM+9rF\n" + "TOArmxj97e2Eyppild2GhB/0RmqycvSo4ecrjDausTQIU6hpyvWJ27f2L9xnr0By\n" + "lDXBkDPiN+idYRmMRNGS2nnK6qtAPqhAix1SAZbu7Pn2uKYVjXgy1I5oJPA0bT66\n" + "nLfEynU8wxodXC8tR0+wsuRw+M0hRaPDtzq7JccOqSgokyfZwfWr7JmOQNqk6MQc\n" + "STbE6VrqOo8A1c4BL2V4Cqb7G7ixrcyeJkmExyJO1ENdDl5B1rKaeDggRLBSlXbe\n" + "lHz2lFexghYSModdsoMCAwEAAaMjMCEwEgYDVR0TAQH/BAgwBgEB/wIBAzALBgNV\n" + "HQ8EBAMCAgQwDQYJKoZIhvcNAQELBQADggEBAEA721CsrPbMQOJ57QsnhCrXl1V4\n" + "A9gg5+EDIqhp1V9Gv110THm1wonm3XnBGZ7vT1fmbgbfWpW5e/o1hreDOzHT+qnl\n" + "QAqEe9Ff7CuVwgoswordb+Z8bO63RhC3Vy4wsb6TjN8Mwzt62HT99eZQV4HABD9r\n" + "Ov5J38UIzsgjFiQ38WqOh5f1lc5XIajeLEln0Rb7gifuvCQ5rr72VPLNtRckOuRl\n" + "Bf0yN3aS5Sf7ulQwrCeDWgKYNh1nDfgX+wd4iDId3DGeT3aeR6wJ2t6jooKuuMpc\n" + "VaowNWN3SqUXWBLEROLhse6UiHAZprWkvAHVg/Ak2SOLx2T+LHPah7pSceM=\n" + "-----END CERTIFICATE-----" var kuNotCriticalCertSignCa = parseCertificateFromString(kuNotCriticalCertSignCaPem) func TestFailKuNotCriticalCertSignCa(t *testing.T) { err := validateCodeSigningCACertificate(kuNotCriticalCertSignCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be marked critical", err, t) } var kuMissingCaPem = "-----BEGIN CERTIFICATE-----\n" + "MIICuzCCAaOgAwIBAgIJAIeB8Anz7VXqMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAxNDM0OVoYDzIxMjIwNjAxMDE0MzQ5WjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKC3\n" + "3zL1jb7Htbom426qrZLNhBAjp4wmcMnj8zk19Yc4C6+pYmjxzQWvJlh1gRvMuCwv\n" + "TGA2WGn2IPJNwWWt2DkL2dxA30dzBUr2shLVnlMd0GkMthjRGqqU+ELPlCio3yC8\n" + "fBoUMWkH2AoDXPUJgAtT8ASmyRfcOnLUXMOreXgpQo4BjDt1i9QQGx6epqn4lvCH\n" + "3ptE6mmBcrVxRxmXuHFu9/IrHs98+EeRKHMwASwqf7+l2+pX+hQ4VjxOERJWX2Qt\n" + "8iTBS9eLGj72DVzMUKIvG/Br5HKTY/BDEsnWv53rtyZMs6tuLFmOmSTGJSfF3DgJ\n" + "EohedhICOga+t6Fkpm8CAwEAAaMWMBQwEgYDVR0TAQH/BAgwBgEB/wIBAzANBgkq\n" + "hkiG9w0BAQsFAAOCAQEAL41qG7xOJbwCV/Z7SqFOlCq1t7p4M0ZslQ8zuZ52VTUO\n" + "u+7Uzb6fcxfLZDeWYEUcjbTB4TpJexQBQoR1To/yWarNmcDc9Zar/+sggq+peO3m\n" + "vCp/hJRjGFnAy9FTCu0+BOCcT4gnTW8j6qXxek6+DuqPUuL93FH0tjteU4zdIf4/\n" + "/Kqq9FkbLJQ6wVDxfwA2Z/okhLhC4kCH6FHmeOVxguUktaq1qzOWiwCcvC/+/4VW\n" + "sKcKXaTkPwsDSoP7I0+YpUwhl5inLWhhiB2O8fG1D8EHDRZaZr/I7KDaQyYkX5VL\n" + "/8Qa6becfIs3wZKg+zDadgH6yFOFrR3bxK8RvWWw8w==\n" + "-----END CERTIFICATE-----" var kuMissingCa = parseCertificateFromString(kuMissingCaPem) func TestFailKuMissingCa(t *testing.T) { err := validateCodeSigningCACertificate(kuMissingCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be present", err, t) } var kuNotCertSignCaPem = "-----BEGIN CERTIFICATE-----\n" + "MIICyzCCAbOgAwIBAgIJALaWmRd5mz8WMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAxNDIzMVoYDzIxMjIwNjAxMDE0MjMxWjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJV1\n" + "Q8p/3spuT83/eXuEXT4G2bVQqho48ctbbSIAcW0x1q85XL+jAXlP+nynodUAaKif\n" + "W8hMx/ccqvBl8tlVTe4Dk25aSbUWQKJ+uMEKRyQdexj3WR6+QMxAbx4QB5rwGG9D\n" + "qLLXhio9u1Gsau5zN3oFlX89s0e+wk68XOh//LQEgmnSzgd/HHj8w3YQSJRzyQ0q\n" + "MzAhiSvdofnrOWL7JBj7xs+3MoYfnt3fNRLxxJYIJO8ap3CkWUVCRw/u/YTJu1QT\n" + "BwfWrLTdT0YykH2Ok7nK1FmBEyNgsfyeGhq2P5RDRO9nL/2WkcZw1RTvIvBC8fvb\n" + "8W8pVj++VKDeJBc5q/MCAwEAAaMmMCQwEgYDVR0TAQH/BAgwBgEB/wIBAzAOBgNV\n" + "HQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggEBAEEugs5u/lOxRHbg/qspyYkb\n" + "kcCuZsW6cpnKtEJED+dsBnswvG+LtIBlOazBzbPDbR78DbNWgYxTBkb/+40XIfFs\n" + "H8BTRXd3zMjrxELMjaHMzwCvec9BzIOiCz0eqYVJXKK8h/YhyCIy0F+GvrYOXTkp\n" + "llENwqKs26M2H4CScEpoMu0VsBnFNSNj1epV/Bs+sfIPcUY7EB0dMGURM+7vQ/uv\n" + "sotc3rHQCGFxArMpJ75nbWR53WtTm/l7p1RHVsKXb304nKT1bFc7xRnicDhjT4Ps\n" + "SZSlZJ3hMRHo1IyeSkjzCICEsfzmXozlIKkEpmcHxHkoT6+zazrdM4CH5DySGkk=\n" + "-----END CERTIFICATE-----" var kuNotCertSignCa = parseCertificateFromString(kuNotCertSignCaPem) func TestFailKuNotCertSignCa(t *testing.T) { err := validateCodeSigningCACertificate(kuNotCertSignCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage must have the bit positions for key cert sign set", err, t) } // ---------------- Code-Signing Validations ---------------- var validNoOptionsLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIICtzCCAZ+gAwIBAgIJAL+FUPhO8J8cMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAxNTMzMloYDzIxMjIwNjAxMDE1MzMyWjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALrP\n" + "tWr9wlTWsaZhliJbe+IWNwd+GRfQ/RDqkE8VLd/JmU5HTWzIHQ6Rb1Pkw7Ugp1vQ\n" + "xo8OcwN8w774QqdzoWgwtasnLsuLanyyyVacSByRz+VTeRKv2StMV/bezNnhcJ6T\n" + "7cj6dVrkIjf7ViCs9om0P724lvfEW+toMgbHMDyJ16Rwi2Q3OuHKvWrP7BL2l/0X\n" + "E35ZuE+gUdIOgVyoBnk56LIP1VtUtwE5Q9zO/cM+5vHbUj2xtmnLOkFmZGR+w15F\n" + "mG85PHR6aYRh7wr3tjd78eFYlvwdK9BfxCZlkHs5LmFEXt1g/I0C62xSjTKWQSi8\n" + "aa6+55MGgU+UVjh+6jkCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgeAMA0GCSqGSIb3\n" + "DQEBCwUAA4IBAQCtK8xTIK4lR6Po5p3BraeV8LDNCHKDRkHtYQfnqYxqaqGcNG36\n" + "6Bsp37cVS8Md6pRdMwCXizqJvu782iWk2oyQTf05TW4CW0doEzFLIO+HJk0+dVNg\n" + "+mUbXjWlRxA9PQS2NVzkBwKGnN2b7yNT70jA6CU+LHRJaGFYr2zLny4fhsViG8P4\n" + "0TtU6Zy0EPJ0k77iGd7TEazSyOyIaEjemKlQjrWYa6VRRCpp6iECwJLHekfvxXy5\n" + "pe1tg2DS30BrPdFe2v0lUWf7JkOAqy0Q7dfmUOgN7Oyjtgn3oC7lqpV4wBCRI9wq\n" + "DXnpXe7StmJYV3TPc7NX8gAuQHw+GfWnVmZr\n" + "-----END CERTIFICATE-----" var validNoOptionsLeaf = parseCertificateFromString(validNoOptionsLeafPem) func TestValidNoOptionsLeaf(t *testing.T) { if err := validateCodeSigningLeafCertificate(validNoOptionsLeaf); err != nil { t.Fatal(err) } } var caTrueLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIICyDCCAbCgAwIBAgIJAI/1c9qHTFkvMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAyMDQyNloYDzIxMjIwNjAxMDIwNDI2WjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMYF\n" + "Cw41ani7Dm9PY8m5IZZ6JbbATeANXA1DKKRqzt1jjvtCn7ya0Ll3Sw3HK6nqUoZz\n" + "3hrFPE4rIb84YzCRFs+uEWqFB4VnikyKrJsznfGsw3zQAZw3o2w0pIERDAIWZJlN\n" + "wLvFRoI5coRQcAi2DcNXiujssNp4O05ez3IxmCnnOCF6WD87ivB4lIpX/okYsC6g\n" + "wLWFiaA/aC5gyPHLXs6B3Vr8yl8Zd/QuOe7ZznTI4E0zObMCk7pCrvzPQjSOE8lE\n" + "XaVHs9pGu+Q3ePKw99MhU9zOzkCjwiFxyhMZLTTuLEfdShxJUWtkBjPeRfrFNuRV\n" + "v6UKVeQDjWeAF+V5LXMCAwEAAaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B\n" + "Af8EBAMCB4AwDQYJKoZIhvcNAQELBQADggEBACWjhZ6QwVseXcEOwCBMssCkkpiW\n" + "Xql/TKQH3b1FWE2FrDmV88J21nRRbtJECNlzkolkk5fHot08S3JBosIjiCaK3MHT\n" + "5o2WKOlUvYSUAbCzAIz69cbGCcOAXHKfyO7wP84HUbaFRKSX7xElKS06d6kxw/2R\n" + "iMfMobcjf2mlg4SAcUqbWptfDUdV+RfW4qLyqeM1FZCKmYRWAb9bJ2diS0QwU7Rz\n" + "XODxVkB4y9BXfh0TtKTvxaRYs/W1V4nyD3DBJDZ6UZKa59zXfPS+Das+d24GfEvz\n" + "4dtOHTXGn+s5KE1tmn2cpwZ/megX9mH3FEDNMsFgS00Jp1PmTRiDMZvWLFc=\n" + "-----END CERTIFICATE-----" var caTrueLeaf = parseCertificateFromString(caTrueLeafPem) func TestFailCaTrueLeaf(t *testing.T) { err := validateCodeSigningLeafCertificate(caTrueLeaf) assertErrorEqual("certificate with subject \"CN=Hello\": if the basic constraints extension is present, the ca field must be set to false", err, t) } var kuNoDigitalSignatureLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIICtzCCAZ+gAwIBAgIJAPcOgGijs80IMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAyMDg1OFoYDzIxMjIwNjAxMDIwODU4WjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKFu\n" + "0MuGPCD19KP29Kt1wbQ3d/YgYnVkRJL+HyzZMkcSISr+wBOeH5exsZBttWZLpjPK\n" + "cnMNf/anbzAYUxzYPys7LWu4XcfRIdI2HWsz+cGuHqDe8kIzmUv/EOxir34QFRXK\n" + "2zVzf8cxIQgh880q1nQG86GU7sObbuF2AnAUZr+ZDZ8QgWsoWQIBX7vijxEa94jh\n" + "AVobJsQUJxb/P+tqtLghOlsE6X5Ze/2EpMimNqSee13VRjPxaOnTTyUzYYtYPpoC\n" + "QdoHN/ia0Gjmhto6ZKPksmTpdsmsqZnhBCC2KGWlPHZ/mv2KNsGCShuDv7p1gJ91\n" + "B9qqefonbT4U1V/XzJcCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3\n" + "DQEBCwUAA4IBAQBWSg+EPKjdoHQ/aEHpQsOeTI3RONEwHO4XnWP3fJa/I75VSKFP\n" + "nSRnmzffvnxs8gNpx02K6nLZILjYNjg9VFRkTgRnX24Xn+h3Egv7/3xA9UpwkiJ1\n" + "eG7EUZhgA1HaANebfXIvIapSRMcO1NzKsjh5wUVGUWubt9RzBB2T+NFpB0tueDun\n" + "xeGn74C10jG1ER5ne1AjSuCTQvPTrBgw2uClQYxiU70+PS6ZOlfz4oF2sGHzFQCa\n" + "3bXCB2iP3UjyImB6lXnEYKpPMUo5yI5yJmABF4b4bJzrMaIvgBS0xxHnLom6/9FD\n" + "MJnGuo7eFw1u4lNJRWOpIIYc+8c8HzZsIAjL\n" + "-----END CERTIFICATE-----" var kuNoDigitalSignatureLeaf = parseCertificateFromString(kuNoDigitalSignatureLeafPem) func TestFailKuNoDigitalSignatureLeaf(t *testing.T) { err := validateCodeSigningLeafCertificate(kuNoDigitalSignatureLeaf) assertErrorEqual("the certificate with subject \"CN=Hello\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", err, t) } var kuWrongValuesLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIICtzCCAZ+gAwIBAgIJAMh6H4wfi2mqMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAyMTEzOVoYDzIxMjIwNjAxMDIxMTM5WjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANyn\n" + "YfSfV3AEB9WJmPcoE8LkwfYUKupLeuSTQ92y4biMNCE7RdGhRqLRtr/IkyLKtEal\n" + "36cAChinQhYYyi5m+7o6Q2hn+KGYWSI+zC1sylzh2vbmP+xsJpe0RZKdupJThDLg\n" + "8pAgtbYF5OhEwK96Xd3fHEmaXfOMCVNV64RiA/qE8mUYPCvCEkzy/9GRqweOYDDb\n" + "JuzWdsUEgDqfx/JdzprMzP3h3/qV3XZDNseKugK/ppdEMMnmPlE4wyzyawJdIPIf\n" + "otPrurUyxD6pBN2LVeYFJF1nX1Uf+29wjrWTpVSmtDmKQyzb6RYvqtS3LIA+u5bi\n" + "lElqTNwCbTGeIGGdnXcCAwEAAaMSMBAwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3\n" + "DQEBCwUAA4IBAQDPypA7akOvbk+mRbEjfiROiNjq7w8eua5EDG86hVVO+zEmQ54a\n" + "eNc9nWWwdCAgToiZSu5DExNLAwhZjs/p+6okdRIteaeQCf+6Ur6+XtgeSxQDd1zy\n" + "138vrqp+s1tcDGxfZzm5q7sT0U/nTJGEgwGaSketD/gV1pS12So6U1SIIbQ9cDey\n" + "c+jiKFj+BPs+WYPGfnj0B/R/Iq5Afsxonot3FlG9vaKZoBJG0KC3vH4Zyir4q6Hs\n" + "y0HBHbu0MqIJV0Me2T0IjixlGw1wlqtOHM6lw3qkZzSAdGu6+CPkODwrjur2J87S\n" + "rEtF5Jlc3Ea0DGcRTla8FdwZfi9w3TH8i8G0\n" + "-----END CERTIFICATE-----" var kuWrongValuesLeaf = parseCertificateFromString(kuWrongValuesLeafPem) func TestFailKuWrongValuesLeaf(t *testing.T) { err := validateCodeSigningLeafCertificate(kuWrongValuesLeaf) assertErrorEqual("the certificate with subject \"CN=Hello\" is invalid. The key usage must be \"Digital Signature\" only, but found \"CertSign\", \"CRLSign\"", err, t) } var rsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIIBsjCCARugAwIBAgIJAIoMv72RJPnkMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAyMjMzNFoYDzIxMjIwNjAxMDIyMzM0WjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEArtSO3a/D\n" + "dsI9CA80GtZgNMv1M/p3GWZ/Rvv+WTMT+MShYQ8fAReiyd9oaBVqLIIyusKDjZg6\n" + "LQYf3X5L/SLZhAtgCeCzH9/0E+N+ABsP9+DLGuRWct3wAPnI6PlvS6T29CsVIJva\n" + "QTuUzMD63Fxci7VzoeMlFLByg6cke8lXKnECAwEAAaMSMBAwDgYDVR0PAQH/BAQD\n" + "AgeAMA0GCSqGSIb3DQEBCwUAA4GBAGwNRvIxfbD/yDt0XAMaW6hoMNZ1pEA1fqbF\n" + "1Kj3cLNJL3x6JcMYFpoj1lVbF1iv4idIMwsZVnAQO/d3WvlZJEbAQB6J/Bt7zl1T\n" + "DamifLxcPW645Re/uWbTC9FrVCwP8Sw6VC3MSMniP3NIHPAQUPrky8Qm4SPYBnTo\n" + "rmmUMAVI\n" + "-----END CERTIFICATE-----" var rsaKeyTooSmallLeaf = parseCertificateFromString(rsaKeyTooSmallLeafPem) func TestFailRsaKeyTooSmallLeaf(t *testing.T) { err := validateCodeSigningLeafCertificate(rsaKeyTooSmallLeaf) assertErrorEqual("certificate with subject \"CN=Hello\": rsa key size 1024 bits is not supported", err, t) } var ecdsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIIBFzCBxqADAgECAgkA9KM1CXSlbcswCgYIKoZIzj0EAwIwEDEOMAwGA1UEAwwF\n" + "SGVsbG8wIBcNMjIwNjI1MDI0NzA3WhgPMjEyMjA2MDEwMjQ3MDdaMBAxDjAMBgNV\n" + "BAMMBUhlbGxvME4wEAYHKoZIzj0CAQYFK4EEACEDOgAE+7pD988OoErMBUx/nRYc\n" + "PNOKFf2VJN4rnRHCnbSYZf8gDYo4stt6Ovq19Zzu4vpwZzvgr4cmiFKjEjAQMA4G\n" + "A1UdDwEB/wQEAwIHgDAKBggqhkjOPQQDAgNAADA9Ahxf4a+tH/2/hBJ8/XV4yTus\n" + "j1G7ww5Ye2f+a02jAh0A6EgSPUhB7UY01gwjwgw7kL+p/hRwr6pHmOmR8g==\n" + "-----END CERTIFICATE-----" var ecdsaKeyTooSmallLeaf = parseCertificateFromString(ecdsaKeyTooSmallLeafPem) func TestFailEcdsaKeyTooSmallLeaf(t *testing.T) { err := validateCodeSigningLeafCertificate(ecdsaKeyTooSmallLeaf) assertErrorEqual("certificate with subject \"CN=Hello\": ecdsa key size 224 bits is not supported", err, t) } // ---------------- Code-Signing Leaf Validations ---------------- func TestValidFullOptionsCodeLeaf(t *testing.T) { if err := validateCodeSigningLeafCertificate(codeSigningCert); err != nil { t.Fatal(err) } } func TestValidateLeafKeyUsage(t *testing.T) { extensions := []pkix.Extension{{ Id: oid.KeyUsage, Critical: true, }} tests := []struct { name string cert *x509.Certificate expectedErrMsg string }{ { name: "Valid DigitalSignature usage", cert: &x509.Certificate{ Subject: pkix.Name{CommonName: "Test CN"}, KeyUsage: x509.KeyUsageDigitalSignature, Extensions: extensions, }, expectedErrMsg: "", }, { name: "Valid ContentCommitment usage", cert: &x509.Certificate{ Subject: pkix.Name{CommonName: "Test CN"}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageContentCommitment, Extensions: extensions, }, expectedErrMsg: "", }, { name: "Missing DigitalSignature usage", cert: &x509.Certificate{ Subject: pkix.Name{CommonName: "Test CN"}, KeyUsage: x509.KeyUsageCertSign, Extensions: extensions, }, expectedErrMsg: "the certificate with subject \"CN=Test CN\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", }, { name: "Invalid KeyEncipherment usage", cert: &x509.Certificate{ Subject: pkix.Name{CommonName: "Test CN"}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, Extensions: extensions, }, expectedErrMsg: "the certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"KeyEncipherment\"", }, { name: "Multiple Invalid usages", cert: &x509.Certificate{ Subject: pkix.Name{CommonName: "Test CN"}, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageEncipherOnly | x509.KeyUsageDecipherOnly | x509.KeyUsageEncipherOnly | x509.KeyUsageDecipherOnly, Extensions: extensions, }, expectedErrMsg: "the certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"KeyEncipherment\", \"DataEncipherment\", \"KeyAgreement\", \"CertSign\", \"CRLSign\", \"EncipherOnly\", \"DecipherOnly\"", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := validateLeafKeyUsage(tt.cert) if err != nil && tt.expectedErrMsg == "" { t.Fatalf("expected no error, but got: %s", err) } else if err == nil && tt.expectedErrMsg != "" { t.Fatalf("expected error %q, but got none", tt.expectedErrMsg) } else if err != nil && err.Error() != tt.expectedErrMsg { t.Fatalf("expected error %q, but got: %s", tt.expectedErrMsg, err) } }) } } var ekuWrongValuesCodeLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIIC6jCCAdKgAwIBAgIJAKZJHdWFNYPlMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMDEwM1oYDzIxMjIwNjAxMDMwMTAzWjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK2t\n" + "EFpNOJkX7B78d9ahTl5MXGWyKIjgfg1PhkYwHKHJWBiqHa1OUewfUG4ouVuaAvJ+\n" + "GPzcxt23/J3jK+3/szrzpBNv1f0vgIa+mqaRQDW2m/wfWw3kpcwxlRcL7GnCeHbv\n" + "gRFDXQW6MhKgGgKdQ5ezV+p01eF+CzMhUe+bZO+mvgxj36MJHzLMFHyh3x4/+z4x\n" + "qRKmj4uUqJ2FJLlQEk92vPE/N3r7rEWa6gd4mBZ+DsZSrCbVPXchS2mCkeg70qxA\n" + "4840qVLZ5eFxtqnTEUNytu3ug/8ydV9VmuT+C5fQYUp3Fl7D1QxHxWYTVTKdenCY\n" + "jxcJHW1cUWZQlgPTLq8CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + "MCgGCCsGAQUFBwMDBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + "SIb3DQEBCwUAA4IBAQBRfpNRu79i47yp73LWTKrnZRiLC4JAI3I3w5TTx8m2tYkq\n" + "tkSCP3Sn4y6VjKqo9Xtlt/bBLypw7XAOZOUZLEaoCjwRmAwq74VHAxDZO1LfFlKd\n" + "au8G3xhKjc5prOMJ2g4DELOcyDoLDlwYqQ/jfG/t8b0P37yakFVffSzIA7D0BjmS\n" + "OnWrGOJO/IJZjiaTdQkg+n5jk4FNqhwW91em64/M3MOmib3plnl89MgR90kuvQOV\n" + "ctDBylt8M61MgnbzeunAq4aKYJc4IeeIH++g4F3/pqyoC95sAZP+A6+LkmBDOcyE\n" + "5wUmNtUsL9xxKIUCvPR1JtiLNxHrfendWiuJnW1M\n" + "-----END CERTIFICATE-----" var ekuWrongValuesCodeLeaf = parseCertificateFromString(ekuWrongValuesCodeLeafPem) func TestFailEkuWrongValuesCodeLeaf(t *testing.T) { err := validateCodeSigningLeafCertificate(ekuWrongValuesCodeLeaf) assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain ServerAuth eku", err, t) } var ekuMissingCodeSigningLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIICzDCCAbSgAwIBAgIJAJtYOfTu82KRMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTMxM1oYDzIxMjIwNjAxMDMxMzEzWjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQN\n" + "GJKHE6cdcmrHkxXOTawWgYEF1X42IOK7gAXFg+KBPHPw4npDjUclLX0sY3XjBuhT\n" + "wI5DRATSNTV2ba3+DpFuH3D+Hbfjil91AG8XzormUPOOCbZqJxSKYAIZfPQGdUvV\n" + "UBulnbDsije00HoNZ03IvdjxbB/9y6a3qQEvIUaEjaZBH3s/YYQIiEmKu6eDpj3R\n" + "PnUcrP5b7jBMA/Vb8joLM0InzqGPRLPFAPf5womAjxZSsrgyVeA1xSm+6KtXMmaA\n" + "IKYwNVAOnhfqgUk0tlaRyXXji2T1M9w9l5XUA1iNOMcjTUTfFa5KW7c0TLTcK6vW\n" + "Eq1BEXUEw7HP7DQUjycCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n" + "MAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQCSr6A/YAMd6lisgipR0UCA\n" + "4Ye/1kl0jglT7stLTfftSeXgCKXYlwus9VSpZBtg+RvJkihlLNT6vtsiTMfJUBBc\n" + "jALLKYUQuCw9sReAbfvecIfc2bUve6X8isLWDVnxlC1udx2WG3lIfW2Sgs/dYeZW\n" + "yqLTagK5GLlDfg9gBpHLmQYOmshhI85ObOioUAiWTW+S6mx4Bphgl7dlcUabJxEJ\n" + "MpJJiGPkUUUCuYkp31E7S4JRbSXSkaHefZxB5fvhlbnACeqnOtMG/IKaTjCUemkK\n" + "ZRmJ0Al1PTWs+Dn8zLzexP/LkmQZU/FUMxeat/dAnc2blDbVnAsvcvnutXGHoZH5\n" + "-----END CERTIFICATE-----" var ekuMissingCodeSigningLeaf = parseCertificateFromString(ekuMissingCodeSigningLeafPem) func TestFailEkuMissingCodeSigningLeaf(t *testing.T) { err := validateCodeSigningLeafCertificate(ekuMissingCodeSigningLeaf) assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain OCSPSigning eku", err, t) } // ---------------- Utility Methods ---------------- func parseCertificateFromString(certPem string) *x509.Certificate { stringAsBytes := []byte(certPem) cert, _ := parseCertificates(stringAsBytes) return cert[0] } func assertErrorEqual(expected string, err error, t *testing.T) { if err == nil || expected != err.Error() { t.Fatalf("Expected error \"%v\" but was \"%v\"", expected, err) } } func readSingleCertificate(path string) (*x509.Certificate, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } certs, err := parseCertificates(data) if err != nil { return nil, err } if len(certs) == 0 { return nil, errors.New("no certificate in file") } return certs[0], nil } notation-core-go-1.1.0/x509/helper.go000066400000000000000000000110101466321423000172020ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package x509 import ( "bytes" "crypto/x509" "fmt" "strings" "time" "github.com/notaryproject/notation-core-go/signature" ) func isSelfSigned(cert *x509.Certificate) (bool, error) { return isIssuedBy(cert, cert) } func isIssuedBy(subject *x509.Certificate, issuer *x509.Certificate) (bool, error) { if err := subject.CheckSignatureFrom(issuer); err != nil { return false, err } return bytes.Equal(issuer.RawSubject, subject.RawIssuer), nil } func validateSigningTime(cert *x509.Certificate, signingTime *time.Time) error { if signingTime != nil && (signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter)) { return fmt.Errorf("certificate with subject %q was invalid at signing time of %s. Certificate is valid from [%s] to [%s]", cert.Subject, signingTime.UTC(), cert.NotBefore.UTC(), cert.NotAfter.UTC()) } return nil } func validateCABasicConstraints(cert *x509.Certificate, expectedPathLen int) error { if !cert.BasicConstraintsValid || !cert.IsCA { return fmt.Errorf("certificate with subject %q: ca field in basic constraints must be present, critical, and set to true", cert.Subject) } maxPathLen := cert.MaxPathLen isMaxPathLenPresent := maxPathLen > 0 || (maxPathLen == 0 && cert.MaxPathLenZero) if isMaxPathLenPresent && maxPathLen < expectedPathLen { return fmt.Errorf("certificate with subject %q: expected path length of %d but certificate has path length %d instead", cert.Subject, expectedPathLen, maxPathLen) } return nil } func validateLeafBasicConstraints(cert *x509.Certificate) error { if cert.BasicConstraintsValid && cert.IsCA { return fmt.Errorf("certificate with subject %q: if the basic constraints extension is present, the ca field must be set to false", cert.Subject) } return nil } func validateLeafKeyUsage(cert *x509.Certificate) error { if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { return fmt.Errorf("the certificate with subject %q is invalid. The key usage must have the bit positions for \"Digital Signature\" set", cert.Subject) } var invalidKeyUsages []string if cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 { invalidKeyUsages = append(invalidKeyUsages, `"KeyEncipherment"`) } if cert.KeyUsage&x509.KeyUsageDataEncipherment != 0 { invalidKeyUsages = append(invalidKeyUsages, `"DataEncipherment"`) } if cert.KeyUsage&x509.KeyUsageKeyAgreement != 0 { invalidKeyUsages = append(invalidKeyUsages, `"KeyAgreement"`) } if cert.KeyUsage&x509.KeyUsageCertSign != 0 { invalidKeyUsages = append(invalidKeyUsages, `"CertSign"`) } if cert.KeyUsage&x509.KeyUsageCRLSign != 0 { invalidKeyUsages = append(invalidKeyUsages, `"CRLSign"`) } if cert.KeyUsage&x509.KeyUsageEncipherOnly != 0 { invalidKeyUsages = append(invalidKeyUsages, `"EncipherOnly"`) } if cert.KeyUsage&x509.KeyUsageDecipherOnly != 0 { invalidKeyUsages = append(invalidKeyUsages, `"DecipherOnly"`) } if len(invalidKeyUsages) > 0 { return fmt.Errorf("the certificate with subject %q is invalid. The key usage must be \"Digital Signature\" only, but found %s", cert.Subject, strings.Join(invalidKeyUsages, ", ")) } return nil } func validateSignatureAlgorithm(cert *x509.Certificate) error { keySpec, err := signature.ExtractKeySpec(cert) if err != nil { return fmt.Errorf("certificate with subject %q: %w", cert.Subject, err) } if keySpec.SignatureAlgorithm() == 0 { return fmt.Errorf("certificate with subject %q: unsupported signature algorithm with key spec %+v", cert.Subject, keySpec) } return nil } func ekuToString(eku x509.ExtKeyUsage) string { switch eku { case x509.ExtKeyUsageAny: return "Any" case x509.ExtKeyUsageServerAuth: return "ServerAuth" case x509.ExtKeyUsageClientAuth: return "ClientAuth" case x509.ExtKeyUsageOCSPSigning: return "OCSPSigning" case x509.ExtKeyUsageEmailProtection: return "EmailProtection" case x509.ExtKeyUsageCodeSigning: return "CodeSigning" case x509.ExtKeyUsageTimeStamping: return "Timestamping" default: return fmt.Sprintf("%d", int(eku)) } } notation-core-go-1.1.0/x509/key.go000066400000000000000000000026401466321423000165240ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package x509 import ( "crypto" "crypto/x509" "encoding/pem" "errors" "fmt" "os" ) // ReadPrivateKeyFile reads a key PEM file as a signing key. func ReadPrivateKeyFile(path string) (crypto.PrivateKey, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } return ParsePrivateKeyPEM(data) } // ParsePrivateKeyPEM parses a PEM as a signing key. func ParsePrivateKeyPEM(data []byte) (crypto.PrivateKey, error) { block, _ := pem.Decode(data) if block == nil { return nil, errors.New("no PEM data found") } switch block.Type { case "PRIVATE KEY": return x509.ParsePKCS8PrivateKey(block.Bytes) case "EC PRIVATE KEY": return x509.ParseECPrivateKey(block.Bytes) case "RSA PRIVATE KEY": return x509.ParsePKCS1PrivateKey(block.Bytes) } return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type) } notation-core-go-1.1.0/x509/testdata/000077500000000000000000000000001466321423000172145ustar00rootroot00000000000000notation-core-go-1.1.0/x509/testdata/der.der000066400000000000000000000015431466321423000204650ustar00rootroot000000000000000_0G !XS0  *H  0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0 090318100000Z 290318100000Z0L1 0U GlobalSign Root CA - R310U  GlobalSign10U GlobalSign0"0  *H 0 %vyx"(vŭrFCDz_$.K`FR Gpld,= +׶y;wIjb/^h߉'8>&Y sް&[`I(i;(坊aW7tt:r/.л=3+S:sA :O.2`W˹hh8&`uw I@H1a^wdz_b lTin郓qviB0@0U0U00UK.E$MPc0  *H  K@P TEI A(3kt- sgJD{xnlo)39EÎWlS-$lcShgV>5!hS̐]FzX(/7ADmS(~g׊L'Lssvz- ,&Y sް&[`I(i;(坊aW7tt:r/.л=3+S:sA :O.2`W˹hh8&`uw I@H1a^wdz_b lTin郓qviB0@0U0U00UK.E$MPc0  *H  K@P TEI A(3kt- sgJD{xnlo)39EÎWlS-$lcShgV>5!hS̐]FzX(/7ADmS(~g׊L'Lssvz- ,&Y sް&[`I(i;(坊aW7tt:r/.л=3+S:sA :O.2`W˹hh8&`uw I@H1a^wdz_b lTin郓qviB0@0U0U00UK.E$MPc0  *H  K@P TEI A(3kt- sgJD{xnlo)39EÎWlS-$lcShgV>5!hS̐]FzX(/7ADmS(~g׊L'Lssvz- ,BgQ=V(-ӱue)iِF{DA|jWz7y]dRvGa_T !hn7!@_J}9gcl6 \dt@rźNXMy׏s,9H1W)'.NvU&p&G CCc{un'%:8;["ق*ǒ>sZlR+Xt@(sCJk8)ʪsBhF:^KvQɌ ;["&}_#dc>t? v]Fu`X (T]^0Fvk 3ͱ]0Y0U00UmM/s)v/uj o0U#0q]dL.g?纘O0U0U% 0 +0w+k0i0$+0http://ocsp.digicert.com0A+05http://cacerts.digicert.com/DigiCertTrustedRootG4.crt0CU<0:08642http://crl3.digicert.com/DigiCertTrustedRootG4.crl0 U 00g 0  `Hl0  *H  }YoD"~f!B.M0SοP]K)p )ii>` \[m %41gͶoPLb Vs"%Εi?GwrtO,zC_`Of,d&l|p |屮uOZ](TՊqver#'D'$&*yV Ečrjq Ķ͇$OIwfrKR7~S;I9z%c',=?kfAO@!!@з$x:䞭4q&k8sO?;xLĕ{ _39Axz8#(_+~Fu,',&o{6Yp7 O'`gfU:)+A:1b  Wټ2]# v&evB) G+UT++/DJ78+|notation-core-go-1.1.0/x509/testdata/timestamp_leaf.crt000066400000000000000000000033061466321423000227220ustar00rootroot0000000000000000D9?_a0  *H  0c1 0 UUS10U DigiCert, Inc.1;09U2DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA0 230714000000Z 341013235959Z0H1 0 UUS10U DigiCert, Inc.1 0UDigiCert Timestamp 20230"0  *H 0 SE[>T#ϟ] /Hz;*gbXͪj)bciX5q:P ǚ;/fii[+ P0hʃB $j;]E alq^<.yfR>_CӄH-^EuuRGx)9kxYD+JՕdM#ʆ!dpc.$_v}1eGUJ$/+{s>2R4ԻԠ,4nd7QͪLfhbAxmXAر,Qbi|dM^Pɳʼ;hD;Bs} y4~\ XL>iuǃdu͏vV$k!4/:k*{R8 qlq>oaG l$Bʠq=ip' O6_p .d"+(!IQ~f;8QʔP:ӊ@{00U0 U00U% 0 +0 U 00g 0  `Hl0U#0mM/s)v/uj o0UdVe1I0ZUS0Q0OMKIhttp://crl3.digicert.com/DigiCertTrustedG4RSA4096SHA256TimeStampingCA.crl0+00$+0http://ocsp.digicert.com0X+0Lhttp://cacerts.digicert.com/DigiCertTrustedG4RSA4096SHA256TimeStampingCA.crt0  *H  ޠpO_B֏ѪUㆿ',AК3J6Թr~y8H_=2u6gZO5<*lyD:8;^9X|s1U ~yeh";뚂5W(i2:Fkwlls:IF̶8C,NL}hpw \`(8RZ֬"#NPkwqDAɸFl2|X/gGesk,FA_٭DAnotation-core-go-1.1.0/x509/testdata/timestamp_root.crt000066400000000000000000000026241466321423000230000ustar00rootroot0000000000000000xW!29wu\0  *H  0b1 0 UUS10U  DigiCert Inc10U www.digicert.com1!0UDigiCert Trusted Root G40 130801120000Z 380115120000Z0b1 0 UUS10U  DigiCert Inc10U www.digicert.com1!0UDigiCert Trusted Root G40"0  *H 0 sh޻]J<0"0i3§%.!=Y)=Xvͮ{ 08VƗmy_pUA2s*n|!LԼu]xf:1D3@ZI橠gݤ'O9X$\Fdivv=Y]BvizHftKc:=E%D+~am3K}Ï!Ռp,A`cDvb~d3щίCw !T)%lRQGt&Auz_?ɼA[P1r" |Lu?c!_ QkoOE_ ~ &i/-٩B0@0U00U0Uq]dL.g?纘O0  *H  a}lđádhVwpJx\ _)V 6I]Dcଡ଼f# =ymkTY9"SD]Pz}b! sfѠ`_襴m5|Z֢8xM Gr 20Y.qVjoPmhz6z$ Pz#aB)͢ Aќd&LPAq=?Mp# J܁2  Ok t094!U2qI(PMMuACDO,6E#SlogUFL?n(Zy&ҤbJGJ gf~[A;;cTQ*xίI󒙶a҅POBl C:qM&5]b2Ҡ+TWJ'S趉m[h#QV𦀠Su)wތ!G=uf~notation-core-go-1.1.0/x509/timestamp_cert_validations.go000066400000000000000000000135371466321423000233600ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package x509 import ( "crypto/x509" "errors" "fmt" "github.com/notaryproject/notation-core-go/internal/oid" ) // ValidateTimestampingCertChain takes an ordered time stamping certificate // chain and validates issuance from leaf to root // Validates certificates according to this spec: // https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements func ValidateTimestampingCertChain(certChain []*x509.Certificate) error { if len(certChain) < 1 { return errors.New("certificate chain must contain at least one certificate") } // For self-signed signing certificate (not a CA) if len(certChain) == 1 { cert := certChain[0] if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: %w", cert.Subject, err) } if err := validateTimestampingLeafCertificate(cert); err != nil { return fmt.Errorf("invalid self-signed certificate. Error: %w", err) } return nil } for i, cert := range certChain { if i == len(certChain)-1 { selfSigned, selfSignedError := isSelfSigned(cert) if selfSignedError != nil { return fmt.Errorf("root certificate with subject %q is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: %v", cert.Subject, selfSignedError) } if !selfSigned { return fmt.Errorf("root certificate with subject %q is not self-signed. Certificate chain must end with a valid self-signed root certificate", cert.Subject) } } else { // This is to avoid extra/redundant multiple root cert at the end // of certificate-chain selfSigned, selfSignedError := isSelfSigned(cert) // not checking selfSignedError != nil here because we expect // a non-nil err. For a non-root certificate, it shouldn't be // self-signed, hence CheckSignatureFrom would return a non-nil // error. if selfSignedError == nil && selfSigned { if i == 0 { return fmt.Errorf("leaf certificate with subject %q is self-signed. Certificate chain must not contain self-signed leaf certificate", cert.Subject) } return fmt.Errorf("intermediate certificate with subject %q is self-signed. Certificate chain must not contain self-signed intermediate certificate", cert.Subject) } parentCert := certChain[i+1] issuedBy, issuedByError := isIssuedBy(cert, parentCert) if issuedByError != nil { return fmt.Errorf("invalid certificates or certificate with subject %q is not issued by %q. Error: %v", cert.Subject, parentCert.Subject, issuedByError) } if !issuedBy { return fmt.Errorf("certificate with subject %q is not issued by %q", cert.Subject, parentCert.Subject) } } if i == 0 { if err := validateTimestampingLeafCertificate(cert); err != nil { return err } } else { if err := validateTimestampingCACertificate(cert, i-1); err != nil { return err } } } return nil } func validateTimestampingCACertificate(cert *x509.Certificate, expectedPathLen int) error { if err := validateCABasicConstraints(cert, expectedPathLen); err != nil { return err } return validateTimestampingCAKeyUsage(cert) } func validateTimestampingLeafCertificate(cert *x509.Certificate) error { if err := validateLeafBasicConstraints(cert); err != nil { return err } if err := validateTimestampingLeafKeyUsage(cert); err != nil { return err } if err := validateTimestampingExtendedKeyUsage(cert); err != nil { return err } return validateSignatureAlgorithm(cert) } func validateTimestampingCAKeyUsage(cert *x509.Certificate) error { if err := validateTimestampingKeyUsagePresent(cert); err != nil { return err } if cert.KeyUsage&x509.KeyUsageCertSign == 0 { return fmt.Errorf("certificate with subject %q: key usage must have the bit positions for key cert sign set", cert.Subject) } return nil } func validateTimestampingLeafKeyUsage(cert *x509.Certificate) error { if err := validateTimestampingKeyUsagePresent(cert); err != nil { return err } return validateLeafKeyUsage(cert) } func validateTimestampingKeyUsagePresent(cert *x509.Certificate) error { var hasKeyUsageExtension bool for _, ext := range cert.Extensions { if ext.Id.Equal(oid.KeyUsage) { hasKeyUsageExtension = true break } } if !hasKeyUsageExtension { return fmt.Errorf("certificate with subject %q: key usage extension must be present", cert.Subject) } return nil } func validateTimestampingExtendedKeyUsage(cert *x509.Certificate) error { // RFC 3161 2.3: The corresponding certificate MUST contain only one // instance of the extended key usage field extension. And it MUST be // marked as critical. if len(cert.ExtKeyUsage) != 1 || cert.ExtKeyUsage[0] != x509.ExtKeyUsageTimeStamping || len(cert.UnknownExtKeyUsage) != 0 { return fmt.Errorf("timestamp signing certificate with subject %q must have and only have %s as extended key usage", cert.Subject, ekuToString(x509.ExtKeyUsageTimeStamping)) } // check if Extended Key Usage extension is marked critical for _, ext := range cert.Extensions { if ext.Id.Equal(oid.ExtKeyUsage) { if !ext.Critical { return fmt.Errorf("timestamp signing certificate with subject %q must have extended key usage extension marked as critical", cert.Subject) } break } } return nil } notation-core-go-1.1.0/x509/timestamp_cert_validations_test.go000066400000000000000000000253421466321423000244140ustar00rootroot00000000000000// Copyright The Notary Project Authors. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package x509 import ( "crypto/x509" "crypto/x509/pkix" "testing" ) func TestValidTimestampingChain(t *testing.T) { timestamp_leaf, err := readSingleCertificate("testdata/timestamp_leaf.crt") if err != nil { t.Fatal(err) } timestamp_intermediate, err := readSingleCertificate("testdata/timestamp_intermediate.crt") if err != nil { t.Fatal(err) } timestamp_root, err := readSingleCertificate("testdata/timestamp_root.crt") if err != nil { t.Fatal(err) } certChain := []*x509.Certificate{timestamp_leaf, timestamp_intermediate, timestamp_root} err = ValidateTimestampingCertChain(certChain) if err != nil { t.Fatal(err) } } func TestInvalidTimestampingChain(t *testing.T) { timestamp_leaf, err := readSingleCertificate("testdata/timestamp_leaf.crt") if err != nil { t.Fatal(err) } timestamp_intermediate, err := readSingleCertificate("testdata/timestamp_intermediate.crt") if err != nil { t.Fatal(err) } timestamp_root, err := readSingleCertificate("testdata/timestamp_root.crt") if err != nil { t.Fatal(err) } expectedErr := "certificate chain must contain at least one certificate" err = ValidateTimestampingCertChain([]*x509.Certificate{}) assertErrorEqual(expectedErr, err, t) certChain := []*x509.Certificate{timestamp_leaf, intermediateCert2, intermediateCert1, rootCert} expectedErr = "invalid certificates or certificate with subject \"CN=DigiCert Timestamp 2023,O=DigiCert\\\\, Inc.,C=US\" is not issued by \"CN=Intermediate2\". Error: crypto/rsa: verification error" err = ValidateTimestampingCertChain(certChain) assertErrorEqual(expectedErr, err, t) certChain = []*x509.Certificate{timestamp_leaf} expectedErr = "invalid self-signed certificate. subject: \"CN=DigiCert Timestamp 2023,O=DigiCert\\\\, Inc.,C=US\". Error: crypto/rsa: verification error" err = ValidateTimestampingCertChain(certChain) assertErrorEqual(expectedErr, err, t) certChain = []*x509.Certificate{timestamp_leaf, timestamp_intermediate} expectedErr = "root certificate with subject \"CN=DigiCert Trusted G4 RSA4096 SHA256 TimeStamping CA,O=DigiCert\\\\, Inc.,C=US\" is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: crypto/rsa: verification error" err = ValidateTimestampingCertChain(certChain) assertErrorEqual(expectedErr, err, t) certChain = []*x509.Certificate{timestamp_root, timestamp_root} expectedErr = "leaf certificate with subject \"CN=DigiCert Trusted Root G4,OU=www.digicert.com,O=DigiCert Inc,C=US\" is self-signed. Certificate chain must not contain self-signed leaf certificate" err = ValidateTimestampingCertChain(certChain) assertErrorEqual(expectedErr, err, t) certChain = []*x509.Certificate{timestamp_leaf, timestamp_intermediate, timestamp_root, timestamp_root} expectedErr = "intermediate certificate with subject \"CN=DigiCert Trusted Root G4,OU=www.digicert.com,O=DigiCert Inc,C=US\" is self-signed. Certificate chain must not contain self-signed intermediate certificate" err = ValidateTimestampingCertChain(certChain) assertErrorEqual(expectedErr, err, t) } var ekuNonCriticalTimeLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIIC5TCCAc2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1JbnRl\n" + "cm1lZGlhdGUyMCAXDTIyMDYzMDE5MjAwNFoYDzMwMjExMDMxMTkyMDA0WjAbMRkw\n" + "FwYDVQQDDBBUaW1lU3RhbXBpbmdMZWFmMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\n" + "MIIBCgKCAQEAyx2ispY5C5sQCiLAuCUTp4wv+fpgHwzE4an8eqi+Jrm0tEabTdzP\n" + "IdZFRYPZbgRx+D9DKeN76f+rt51G9gOX77fYWyIXgnVL4UAYNlQj58hqZ0IO22vT\n" + "nIFiDbJoSPuamQaLZNuluiirUwJv1uqSQiEnWHC4LhKwNOo4UHH5S3XkkYRpdFBF\n" + "Tm4uOTaQJA9dfCh+0wbe7ZlEjDiuk1GTSQu69EPIl4IK7aEWqdvk2z1Pg4YkgJZX\n" + "mWzkECNayUiBeHj7lL5ZnyZeki2l77WzXe/j5dgQ9E2+63hfBew+O/XeS/Tm/TyQ\n" + "0P8bQre6vbn9820Cpyg82fd1+5bwYedwVwIDAQABozUwMzAOBgNVHQ8BAf8EBAMC\n" + "B4AwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcDCDANBgkqhkiG9w0B\n" + "AQsFAAOCAQEAB9Z80K17p4J3VCqVcKyhgkzzYPoKiBWFThVwxS2+TKY0x4zezSAT\n" + "69Nmf7NkVH4XyvCEUfgdWYst4t41rH3b5MTMOc5/nPeMccDWT0eZRivodF5hFWZd\n" + "2QSFiMHmfUhnglY0ocLbfKeI/QoSGiPyBWO0SK6qOszRi14lP0TpgvgNDtMY/Jj5\n" + "AyINT6o0tyYJvYE23/7ysT3U6pq50M4vOZiSuRys83As/qvlDIDKe8OVlDt6xRvr\n" + "fqdMFWSk6Iay2OCfYcjUbTutMzSI7dvhDivn5FKnNA6M7QD1lqb7V9fymgrQTsth\n" + "We9tUxypXgMjYN74QEHYxEAIfNOTeBppWw==\n" + "-----END CERTIFICATE-----" var ekuNonCriticalTimeLeafCert = parseCertificateFromString(ekuNonCriticalTimeLeafPem) func TestTimestampLeafWithNonCriticalEKU(t *testing.T) { expectedErr := "timestamp signing certificate with subject \"CN=TimeStampingLeaf\" must have extended key usage extension marked as critical" err := validateTimestampingLeafCertificate(ekuNonCriticalTimeLeafCert) assertErrorEqual(expectedErr, err, t) } var ekuWrongValuesTimeLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOZe\n" + "9zjKWNlFD/HGrkaAI9mh9Fw1gF8S2tphQD/aPd9IS4HJJEQRkKz5oeHj2g1Y6TEk\n" + "plODrKlnoLe+ZFNFFD4xMVV55aQSJDTljCLPwIZt2VewlaAhIImYihOJvJFST1zW\n" + "K2NW4eLxt0awbE/YzL6beH4A6UsrcXcnN0KKiu6YD1/d5TezJoTQBMo6fboltuce\n" + "P/+RMxyqpvip7nyFF3Yrmhumb7DKJrmSfSjdziI5QoUqzqVgqJ8pXMRb3ZOKb499\n" + "d9RRxGkox93iOdSSlaP3FEl8VK9KqnD+MNhjVZbeYTfjm9UVdp91VLP1E/yfMXz+\n" + "fZhYkublK6v3GWSEcb0CAwEAAaNFMEMwDgYDVR0PAQH/BAQDAgeAMDEGA1UdJQQq\n" + "MCgGCCsGAQUFBwMIBggrBgEFBQcDAQYIKwYBBQUHAwQGCCsGAQUFBwMIMA0GCSqG\n" + "SIb3DQEBCwUAA4IBAQCaQZ+ws93F1azT6SKBYvBRBCj07+2DtNI83Q53GxrVy2vU\n" + "rP1ULX7beY87amy6kQcqnQ0QSaoLK+CDL88pPxR2PBzCauz70rMRY8O/KrrLcfwd\n" + "D5HM9DcbneqXQyfh0ZQpt0wK5wux0MFh2sAEv76jgYBMHq2zc+19skAW/oBtTUty\n" + "i/IdOVeO589KXwJzEJmKiswN9zKo9KGgAlKS05zohjv40AOCAs+8Q2lOJjRMq4Ji\n" + "z21qor5e/5+NnGY+2p4A7PbN+QnDdRC3y16dESRN50o5x6CwUWQO74+uRjrAWYCm\n" + "f/Y7qdOf5zZbY21n8KnLcFOsKhwv4t40Y/LQqN/L\n" + "-----END CERTIFICATE-----" var ekuWrongValuesTimeLeaf = parseCertificateFromString(ekuWrongValuesTimeLeafPem) func TestFailEkuWrongValuesTimeLeaf(t *testing.T) { err := validateTimestampingLeafCertificate(ekuWrongValuesTimeLeaf) assertErrorEqual("timestamp signing certificate with subject \"CN=Hello\" must have and only have Timestamping as extended key usage", err, t) } var ekuMissingTimestampingLeafPem = "-----BEGIN CERTIFICATE-----\n" + "MIICzDCCAbSgAwIBAgIJAJtYOfTu82KRMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" + "BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTMxM1oYDzIxMjIwNjAxMDMxMzEzWjAQMQ4w\n" + "DAYDVQQDDAVIZWxsbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALQN\n" + "GJKHE6cdcmrHkxXOTawWgYEF1X42IOK7gAXFg+KBPHPw4npDjUclLX0sY3XjBuhT\n" + "wI5DRATSNTV2ba3+DpFuH3D+Hbfjil91AG8XzormUPOOCbZqJxSKYAIZfPQGdUvV\n" + "UBulnbDsije00HoNZ03IvdjxbB/9y6a3qQEvIUaEjaZBH3s/YYQIiEmKu6eDpj3R\n" + "PnUcrP5b7jBMA/Vb8joLM0InzqGPRLPFAPf5womAjxZSsrgyVeA1xSm+6KtXMmaA\n" + "IKYwNVAOnhfqgUk0tlaRyXXji2T1M9w9l5XUA1iNOMcjTUTfFa5KW7c0TLTcK6vW\n" + "Eq1BEXUEw7HP7DQUjycCAwEAAaMnMCUwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQM\n" + "MAoGCCsGAQUFBwMJMA0GCSqGSIb3DQEBCwUAA4IBAQCSr6A/YAMd6lisgipR0UCA\n" + "4Ye/1kl0jglT7stLTfftSeXgCKXYlwus9VSpZBtg+RvJkihlLNT6vtsiTMfJUBBc\n" + "jALLKYUQuCw9sReAbfvecIfc2bUve6X8isLWDVnxlC1udx2WG3lIfW2Sgs/dYeZW\n" + "yqLTagK5GLlDfg9gBpHLmQYOmshhI85ObOioUAiWTW+S6mx4Bphgl7dlcUabJxEJ\n" + "MpJJiGPkUUUCuYkp31E7S4JRbSXSkaHefZxB5fvhlbnACeqnOtMG/IKaTjCUemkK\n" + "ZRmJ0Al1PTWs+Dn8zLzexP/LkmQZU/FUMxeat/dAnc2blDbVnAsvcvnutXGHoZH5\n" + "-----END CERTIFICATE-----" var ekuMissingTimestampingLeaf = parseCertificateFromString(ekuMissingTimestampingLeafPem) func TestFailEkuMissingTimestampingLeaf(t *testing.T) { err := validateTimestampingLeafCertificate(ekuMissingTimestampingLeaf) assertErrorEqual("timestamp signing certificate with subject \"CN=Hello\" must have and only have Timestamping as extended key usage", err, t) } func TestTimestampingFailNoBasicConstraintsCa(t *testing.T) { err := validateTimestampingCACertificate(noBasicConstraintsCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": ca field in basic constraints must be present, critical, and set to true", err, t) } func TestTimestampingFailKuMissingCa(t *testing.T) { err := validateTimestampingCACertificate(kuMissingCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be present", err, t) } func TestTimestampingFailInvalidPathLenCa(t *testing.T) { err := validateTimestampingCACertificate(rootCert, 3) assertErrorEqual("certificate with subject \"CN=Root\": expected path length of 3 but certificate has path length 2 instead", err, t) } func TestTimestampingFailKuNotCertSignCa(t *testing.T) { err := validateTimestampingCACertificate(kuNotCertSignCa, 3) assertErrorEqual("certificate with subject \"CN=Hello\": key usage must have the bit positions for key cert sign set", err, t) } func TestTimestampingFailWrongExtendedKeyUsage(t *testing.T) { err := validateTimestampingLeafCertificate(validNoOptionsLeaf) assertErrorEqual("timestamp signing certificate with subject \"CN=Hello\" must have and only have Timestamping as extended key usage", err, t) } func TestValidateTimestampingLeafCertificate(t *testing.T) { err := validateTimestampingLeafCertificate(caTrueLeaf) assertErrorEqual("certificate with subject \"CN=Hello\": if the basic constraints extension is present, the ca field must be set to false", err, t) err = validateTimestampingLeafCertificate(kuNoDigitalSignatureLeaf) assertErrorEqual("the certificate with subject \"CN=Hello\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", err, t) cert := &x509.Certificate{ Subject: pkix.Name{CommonName: "Test CN"}, KeyUsage: x509.KeyUsageDigitalSignature, } err = validateTimestampingLeafCertificate(cert) assertErrorEqual("certificate with subject \"CN=Test CN\": key usage extension must be present", err, t) } func TestEkuToString(t *testing.T) { if ekuToString(x509.ExtKeyUsageAny) != "Any" { t.Fatalf("expected Any") } if ekuToString(x509.ExtKeyUsageClientAuth) != "ClientAuth" { t.Fatalf("expected ClientAuth") } if ekuToString(x509.ExtKeyUsageEmailProtection) != "EmailProtection" { t.Fatalf("expected EmailProtection") } if ekuToString(x509.ExtKeyUsageCodeSigning) != "CodeSigning" { t.Fatalf("expected CodeSigning") } if ekuToString(x509.ExtKeyUsageIPSECUser) != "7" { t.Fatalf("expected 7") } }