Compare commits

...

64 Commits
v1.0.1 ... main

Author SHA1 Message Date
Junjie Gao a9c5e3f1a5
chore: update maintainer list: Junjie Gao retired (#282)
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2025-07-01 11:08:04 +08:00
Patrick Zheng ef8789627f
fix: error message of `SignatureAuthenticityError` (#269)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2025-04-16 11:39:23 +08:00
dependabot[bot] 46726d8697
build(deps): bump golang.org/x/crypto from 0.36.0 to 0.37.0 (#267) 2025-04-15 03:10:21 +00:00
dependabot[bot] b85e8f7e65
build(deps): bump github.com/fxamacker/cbor/v2 from 2.7.0 to 2.8.0 (#265) 2025-04-15 03:05:35 +00:00
dependabot[bot] 4d73532534
build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.1 to 4.5.2 (#263) 2025-03-25 09:45:10 +00:00
dependabot[bot] 6a378d5686
build(deps): bump golang.org/x/crypto from 0.35.0 to 0.36.0 (#262) 2025-03-25 09:37:33 +00:00
Junjie Gao ea37e4e6c3
fix: use iterator instead of looping through multiple slices (#259)
Fix:
- updated CRL revocation list check to use iterator rather than looping
through multiple slices

Test:
- added more test cases

Note: the revocation list may contain millions of entries, so the
implementation needs to consider memory allocation.

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2025-03-04 10:23:06 +08:00
dependabot[bot] fcf45123a3
build(deps): bump apache/skywalking-eyes from 0.6.0 to 0.7.0 (#258) 2025-02-27 02:59:00 +00:00
dependabot[bot] 9c4662fe2b
build(deps): bump golang.org/x/crypto from 0.32.0 to 0.35.0 (#261) 2025-02-27 02:54:05 +00:00
Junjie Gao 441bbe882a
bump: update go v1.23 (#260)
Bump:
- updated go v1.23

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2025-02-21 10:00:25 +08:00
Junjie Gao 751008360b
feat: delta CRL (#247)
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2025-01-21 10:58:18 +08:00
Patrick Zheng b07b0ef090
bump: bump up tspclient-go to v1.0.0 (#255)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2025-01-14 08:29:53 +08:00
dependabot[bot] 2bf73c7f71
build(deps): bump golang.org/x/crypto from 0.31.0 to 0.32.0 (#253) 2025-01-07 01:31:25 +00:00
dependabot[bot] 0c60ea723a
build(deps): bump golang.org/x/crypto from 0.29.0 to 0.31.0 (#251) 2024-12-31 00:28:46 +00:00
Junjie Gao 030abc293c
fix: `check-line-endings` command of Makefile (#252)
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-12-30 08:55:42 +08:00
Patrick Zheng 2cd55de6f4
bump: bump up dependencies (#249)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-12-06 14:35:34 +08:00
Patrick Zheng 95d89543c9
fix: add tsa cert chain revocation check after timestamping (#246)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-11-29 10:47:49 +08:00
dependabot[bot] ce88e08466
build(deps): bump codecov/codecov-action from 4 to 5 (#245) 2024-11-27 14:59:33 +00:00
dependabot[bot] f0b6f89f03
build(deps): bump golang.org/x/crypto from 0.28.0 to 0.29.0 (#244) 2024-11-13 05:39:42 +00:00
Patrick Zheng 33af15a189
fix: timestamping (#243)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-11-12 08:12:43 +08:00
Junjie Gao 49a6aaa61a
fix: revocation response status code should be 200 (#239)
Corresponding spec change:
https://github.com/notaryproject/specifications/pull/319

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-11-07 15:11:13 +08:00
dependabot[bot] 9524e15b88
build(deps): bump github.com/golang-jwt/jwt/v4 from 4.5.0 to 4.5.1 (#241) 2024-11-05 06:09:49 +00:00
dependabot[bot] 199c9a93f5
build(deps): bump golang.org/x/crypto from 0.27.0 to 0.28.0 (#233) 2024-11-05 06:05:32 +00:00
Junjie Gao 0c651a46c7
fix: self-signed leaf certificate is not self-issued (#236)
Fix:
- added self-issued check for self-signed leaf certificate
- improved test coverage for `x509` package
- fixed test case for `revocation` package

Resolves #126

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-11-05 11:03:32 +08:00
dependabot[bot] 1683ddbfc5
build(deps): bump github.com/veraison/go-cose from 1.1.0 to 1.3.0 (#232) 2024-11-05 02:22:52 +00:00
Junjie Gao 4819e1d242
fix(test): test cases with `example.com` URL (#238)
Fix:
- added mocked HTTP client for a test case of revocation package
- replaced `*.com` with `*.test` to avoid mentioning a real domain in
testing URL

NOTE: `.test` is a reserved domain for testing in RFC 2606

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-11-05 10:12:07 +08:00
Patrick Zheng 3067ab1223
fix: ocsp revocation check with context (#235)
This PR adds `context` to OCSP revocation check. Resolves #223

Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-10-11 09:22:03 -07:00
AdamKorcz e90546bd90
test(signature): add two fuzz tests (#229)
Signed-off-by: AdamKorcz <adam@adalogics.com>
Signed-off-by: Adam Korczynski <adam@adalogics.com>
2024-09-24 08:24:59 +08:00
dependabot[bot] 0786f51de7
build(deps): bump golang.org/x/crypto from 0.26.0 to 0.27.0 (#226) 2024-09-20 04:57:31 +00:00
Junjie Gao 5d9b2edc6a
feat: CRL cache (#216)
Feat:
- added a `Cache` interface for CRL
- added a `Fetcher` for fetching from the cache, downloading and setting
the cache
Test:
- added unit test

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-09-20 12:42:35 +08:00
Junjie Gao 695ea0c1ad
feat: CRL (#214)
Feat:
- CRL support, including CRL downloading, validation, and revocation
list checks.
- OCSP fallback to CRL when OCSP is unavailable or encounters an unknown
issue.

Refactor:
- move OCSP to internal package to export `ocsp.CertCheckStatus` and
`ocsp.Supported` function for `revocation` package

The PR implements a subset of RFC 5280:
- It only supports X.509 v2 CRL, as the Golang CRL parser only supports
v2.
- It only verifies that the CRL issuer is the certificate's CA with the
same key.
- Delta CRL and indirect CRL are not supported.

> NOTE: CRL cache is not included.

Resolves part 1 of #125 
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-09-18 09:16:23 +08:00
Junjie Gao 55e3568687
bump: bump up golang to v1.22 (#224)
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>

---------

Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-08-27 08:06:48 +08:00
dependabot[bot] 235910ba8e
build(deps): bump golang.org/x/crypto from 0.25.0 to 0.26.0 (#219) 2024-08-18 17:23:10 +00:00
Patrick Zheng 453a5ebcbf
bump: bump up tspclient-go (#217)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-08-08 11:20:14 +08:00
Patrick Zheng 004b86dbf2
refactor!: update revocation (#215)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-08-08 09:50:12 +08:00
dependabot[bot] 45dcf46937
build(deps): bump github.com/notaryproject/tspclient-go from 0.0.0-20240702050734-d91848411058 to 0.1.0 (#212) 2024-07-23 01:21:51 +00:00
Patrick Zheng f45197cbd5
fix: fix `signerInfo.authenticSigningTime` according to spec (#211)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-07-16 08:13:20 +08:00
dependabot[bot] e18808c298
build(deps): bump golang.org/x/crypto from 0.24.0 to 0.25.0 (#210) 2024-07-11 15:34:32 +00:00
Patrick Zheng faac9b7f3f
feat: Timestamp (#207)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-07-08 09:59:12 +08:00
Junjie Gao a1c0af6584
fix(ci): pass CODECOV_TOKEN to reusable-build.yml (#209)
Signed-off-by: Junjie Gao <junjiegao@microsoft.com>
2024-07-03 10:15:26 +08:00
dependabot[bot] c85a3d9f65
build(deps): bump golang.org/x/crypto from 0.23.0 to 0.24.0 (#206)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-28 09:41:49 +08:00
dependabot[bot] a3100ce024
build(deps): bump github.com/fxamacker/cbor/v2 from 2.6.0 to 2.7.0 (#208)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-28 08:32:41 +08:00
dependabot[bot] 4211b09afe
build(deps): bump golang.org/x/crypto from 0.22.0 to 0.23.0 (#204) 2024-05-22 16:54:07 +00:00
dependabot[bot] 6f8b75c6b0
build(deps): bump actions/stale from 8 to 9 (#195) 2024-04-26 15:22:02 +00:00
dependabot[bot] ff5e5b8d44
build(deps): bump apache/skywalking-eyes from a790ab8dd23a7f861c18bd6aaa9b012e3a234bce to cd7b195c51fd3d6ad52afceb760719ddc6b3ee91 (#197) 2024-04-26 15:19:25 +00:00
dependabot[bot] f624dfd1a3
build(deps): bump golang.org/x/crypto from 0.21.0 to 0.22.0 (#200) 2024-04-18 16:51:35 +00:00
Patrick Zheng 356b30e954
fix: leaf certificate validation (#202)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-04-18 09:02:25 +08:00
Junjie Gao 9f13c9ec46
fix(ci): update codecov token (#199) 2024-04-10 08:29:04 +08:00
Toddy Mladenov 66ff8c2adb
chore: org maintainers update (#196)
Addresses https://github.com/notaryproject/.github/issues/66,
https://github.com/notaryproject/.github/issues/67,
https://github.com/notaryproject/.github/issues/68, and
https://github.com/notaryproject/.github/issues/69

Signed-off-by: Toddy Mladenov <toddysm@gmail.com>
2024-04-08 12:45:38 -07:00
Patrick Zheng 807a338673
bump: bump up golang version to v1.21 (#194)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-03-25 14:19:45 +08:00
Yi Zha 9a2ff9ea5f
chore: add GitHub action for stale issues and PRs (#174)
This PR enables the `notation-core-go` repo to run stale action at 1:30
every day to label or close stale PRs and issues. See guideline
https://github.com/marketplace/actions/close-stale-issues

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

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

Signed-off-by: Yi Zha <yizha1@microsoft.com>

---------

Signed-off-by: Yi Zha <yizha1@microsoft.com>
2024-03-21 20:41:41 +08:00
dependabot[bot] 93218d92c1
build(deps): bump golang.org/x/crypto from 0.18.0 to 0.21.0 (#193) 2024-03-19 03:15:42 +00:00
dependabot[bot] c71a505669
build(deps): bump github.com/fxamacker/cbor/v2 from 2.5.0 to 2.6.0 (#190) 2024-03-06 17:50:04 +00:00
dependabot[bot] f6d353ba92
build(deps): bump codecov/codecov-action from 3 to 4 (#188) 2024-02-22 09:27:06 +00:00
dependabot[bot] 2bc927beb6
build(deps): bump golang.org/x/crypto from 0.17.0 to 0.18.0 (#184) 2024-01-19 03:37:03 +00:00
Toddy Mladenov d9131c7fec
Updated the maintainers and codeowners files (#183)
Addressing https://github.com/notaryproject/.github/issues/55
https://github.com/notaryproject/.github/issues/56 and
https://github.com/notaryproject/.github/issues/57

Signed-off-by: Toddy Mladenov <toddysm@gmail.com>
2024-01-14 14:07:38 -08:00
dependabot[bot] 90383555c1
build(deps): bump github/codeql-action from 2 to 3 (#179) 2023-12-26 05:37:10 +00:00
dependabot[bot] db1ee20431
build(deps): bump golang.org/x/crypto from 0.16.0 to 0.17.0 (#180) 2023-12-26 05:33:42 +00:00
dependabot[bot] bdbd23000c
build(deps): bump actions/setup-go from 4 to 5 (#177) 2023-12-19 16:48:18 +00:00
dependabot[bot] 383d314fe7
build(deps): bump golang.org/x/crypto from 0.15.0 to 0.16.0 (#175) 2023-12-13 01:46:24 +00:00
Fan Du 38b8f1a73b
chore: invalid signing time prompt improvement (#173)
Signed-off-by: fandu <dufann@hotmail.com>
Signed-off-by: fanndu <dufann@hotmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Patrick Zheng <patrickzheng@microsoft.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 10:02:11 +08:00
Feynman Zhou 483aabaade
doc: update README to align with the new project brand (#158)
Signed-off-by: Feynman Zhou <feynmanzhou@microsoft.com>
Signed-off-by: Feynman Zhou <feynman@kubesphere.io>
Co-authored-by: Pritesh Bandi <priteshbandi@gmail.com>
2023-11-27 15:42:47 +08:00
dependabot[bot] d957bed16d
build(deps): bump golang.org/x/crypto from 0.14.0 to 0.15.0 (#172)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-21 07:57:57 +08:00
Patrick Zheng 96d9a63946
fix: fix license dependency check workflow (#170)
Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2023-11-08 11:15:13 +08:00
77 changed files with 7368 additions and 1223 deletions

View File

@ -15,4 +15,7 @@ coverage:
status:
project:
default:
target: 89%
target: 89%
patch:
default:
target: 90%

View File

@ -21,4 +21,6 @@ on:
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
uses: ./.github/workflows/reusable-build.yml
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -15,23 +15,28 @@ name: Reusable build
on:
workflow_call:
secrets:
CODECOV_TOKEN:
required: true
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ["1.20", "1.21"]
go-version: ["1.23", "1.24"]
fail-fast: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go ${{ matrix.go-version }} environment
uses: actions/setup-go@v4
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@v3
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -26,19 +26,19 @@ jobs:
security-events: write
strategy:
matrix:
go-version: ["1.20", "1.21"]
go-version: ["1.23", "1.24"]
fail-fast: false
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Go ${{ matrix.go-version }} environment
uses: actions/setup-go@v4
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
check-latest: true
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: go
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3

View File

@ -23,11 +23,14 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Check license header
uses: apache/skywalking-eyes/header@v0.4.0
uses: apache/skywalking-eyes/header@5c5b974209f0de5d905f37deb69369068ebfc15c
with:
mode: check
config: .github/licenserc.yml
- name: Check dependencies license
uses: apache/skywalking-eyes/dependency@v0.4.0
uses: apache/skywalking-eyes/dependency@5c5b974209f0de5d905f37deb69369068ebfc15c
with:
config: .github/licenserc.yml
config: .github/licenserc.yml
flags:
--weak-compatible=true

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

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

View File

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

View File

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

View File

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

View File

@ -4,4 +4,28 @@
[![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)
Contains support for Notary Project signature envelope, and format specific implementation
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).

11
go.mod
View File

@ -1,12 +1,13 @@
module github.com/notaryproject/notation-core-go
go 1.20
go 1.23.0
require (
github.com/fxamacker/cbor/v2 v2.5.0
github.com/golang-jwt/jwt/v4 v4.5.0
github.com/veraison/go-cose v1.1.0
golang.org/x/crypto v0.14.0
github.com/fxamacker/cbor/v2 v2.8.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/notaryproject/tspclient-go v1.0.0
github.com/veraison/go-cose v1.3.0
golang.org/x/crypto v0.37.0
)
require github.com/x448/float16 v0.8.4 // indirect

18
go.sum
View File

@ -1,10 +1,12 @@
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
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/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/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4=
github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs=
github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7rk=
github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=

View File

@ -0,0 +1,121 @@
// 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 algorithm includes signature algorithms accepted by Notary Project
package algorithm
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"errors"
"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
)
// 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
}
// 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
}
// 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
}
// 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{}, fmt.Errorf("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{}, fmt.Errorf("ecdsa key size %d bits is not supported", bitSize)
}
}
return KeySpec{}, errors.New("unsupported public key type")
}

View File

@ -0,0 +1,244 @@
// 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 algorithm
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 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)
}
})
}
}
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)
}
})
}
}

31
internal/oid/oid.go Normal file
View File

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

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,109 @@
// 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"
"errors"
"fmt"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/notation-core-go/revocation/result"
"github.com/notaryproject/notation-core-go/signature"
nx509 "github.com/notaryproject/notation-core-go/x509"
"github.com/notaryproject/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
}
tsaCertChain, err := token.Verify(ctx, x509.VerifyOptions{
Roots: req.TSARootCAs,
})
if err != nil {
return nil, err
}
if err := nx509.ValidateTimestampingCertChain(tsaCertChain); err != nil {
return nil, err
}
// certificate chain revocation check after timestamping
if req.TSARevocationValidator != nil {
certResults, err := req.TSARevocationValidator.ValidateContext(ctx, revocation.ValidateContextOptions{
CertChain: tsaCertChain,
})
if err != nil {
return nil, fmt.Errorf("failed to validate the revocation status of timestamping certificate chain with error: %w", err)
}
if err := revocationResult(certResults, tsaCertChain); err != nil {
return nil, err
}
}
return resp.TimestampToken.FullBytes, nil
}
// revocationResult returns an error if any cert in the cert chain has
// a revocation status other than ResultOK or ResultNonRevokable.
// When ResultRevoked presents, always return the revoked error.
func revocationResult(certResults []*result.CertRevocationResult, certChain []*x509.Certificate) error {
//sanity check
if len(certResults) == 0 {
return errors.New("certificate revocation result cannot be empty")
}
if len(certResults) != len(certChain) {
return fmt.Errorf("length of certificate revocation result %d does not match length of the certificate chain %d", len(certResults), len(certChain))
}
numOKResults := 0
var problematicCertSubject string
var hasUnknownResult bool
for i := len(certResults) - 1; i >= 0; i-- {
cert := certChain[i]
certResult := certResults[i]
if certResult.Result == result.ResultOK || certResult.Result == result.ResultNonRevokable {
numOKResults++
} else {
if certResult.Result == result.ResultRevoked { // revoked
return fmt.Errorf("timestamping certificate with subject %q is revoked", cert.Subject.String())
}
if !hasUnknownResult { // unknown
// not returning because a following cert can be revoked
problematicCertSubject = cert.Subject.String()
hasUnknownResult = true
}
}
}
if numOKResults != len(certResults) {
return fmt.Errorf("timestamping certificate with subject %q revocation status is unknown", problematicCertSubject)
}
return nil
}

View File

@ -0,0 +1,383 @@
// 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"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"os"
"strings"
"testing"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/notation-core-go/revocation/result"
"github.com/notaryproject/notation-core-go/signature"
nx509 "github.com/notaryproject/notation-core-go/x509"
"github.com/notaryproject/tspclient-go"
"github.com/notaryproject/tspclient-go/pki"
)
const rfc3161TSAurl = "http://timestamp.digicert.com"
func TestTimestamp(t *testing.T) {
rootCerts, err := nx509.ReadCertificateFile("testdata/tsaRootCert.cer")
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 ----------------------------------
t.Run("Timestamping success", func(t *testing.T) {
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 ------------------------
t.Run("Timestamping SHA-1", func(t *testing.T) {
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.SHA1,
}
expectedErr := "malformed timestamping request: unsupported hashing algorithm: SHA-1"
_, err = Timestamp(req, opts)
assertErrorEqual(expectedErr, err, t)
})
t.Run("Timestamping failed", func(t *testing.T) {
req := &signature.SignRequest{
Timestamper: dummyTimestamper{},
TSARootCAs: rootCAs,
}
opts := tspclient.RequestOptions{
Content: []byte("notation"),
HashAlgorithm: crypto.SHA256,
}
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)
}
})
t.Run("Timestamping rejected", func(t *testing.T) {
req := &signature.SignRequest{
Timestamper: dummyTimestamper{
respWithRejectedStatus: true,
},
TSARootCAs: rootCAs,
}
opts := tspclient.RequestOptions{
Content: []byte("notation"),
HashAlgorithm: crypto.SHA256,
}
expectedErr := "invalid timestamping response: invalid response with status code 2: rejected"
_, err = Timestamp(req, opts)
assertErrorEqual(expectedErr, err, t)
})
t.Run("Timestamping with cms verification failure", func(t *testing.T) {
opts := tspclient.RequestOptions{
Content: []byte("notation"),
HashAlgorithm: crypto.SHA256,
}
req := &signature.SignRequest{
Timestamper: dummyTimestamper{
invalidSignature: true,
},
TSARootCAs: rootCAs,
}
expectedErr := "failed to verify signed token: cms verification failure: x509: certificate signed by unknown authority"
_, err = Timestamp(req, opts)
assertErrorEqual(expectedErr, err, t)
})
t.Run("Timestamping revocation failed", func(t *testing.T) {
timestamper, err := tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl)
if err != nil {
t.Fatal(err)
}
req := &signature.SignRequest{
Timestamper: timestamper,
TSARootCAs: rootCAs,
TSARevocationValidator: &dummyTSARevocationValidator{
failOnValidate: true,
},
}
opts := tspclient.RequestOptions{
Content: []byte("notation"),
HashAlgorithm: crypto.SHA256,
}
expectedErr := "failed to validate the revocation status of timestamping certificate chain with error: failed in ValidateContext"
_, err = Timestamp(req, opts)
assertErrorEqual(expectedErr, err, t)
})
t.Run("Timestamping certificate revoked", func(t *testing.T) {
timestamper, err := tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl)
if err != nil {
t.Fatal(err)
}
req := &signature.SignRequest{
Timestamper: timestamper,
TSARootCAs: rootCAs,
TSARevocationValidator: &dummyTSARevocationValidator{
revoked: true,
},
}
opts := tspclient.RequestOptions{
Content: []byte("notation"),
HashAlgorithm: crypto.SHA256,
}
expectedErr := `timestamping certificate with subject "CN=DigiCert Timestamp 2024,O=DigiCert,C=US" is revoked`
_, err = Timestamp(req, opts)
assertErrorEqual(expectedErr, err, t)
})
}
func TestRevocationResult(t *testing.T) {
certResult := []*result.CertRevocationResult{
{
// update leaf cert result in each sub-test
},
{
Result: result.ResultNonRevokable,
ServerResults: []*result.ServerResult{
{
Result: result.ResultNonRevokable,
},
},
},
}
certChain := []*x509.Certificate{
{
Subject: pkix.Name{
CommonName: "leafCert",
},
},
{
Subject: pkix.Name{
CommonName: "rootCert",
},
},
}
t.Run("OCSP error without fallback", func(t *testing.T) {
certResult[0] = &result.CertRevocationResult{
Result: result.ResultUnknown,
ServerResults: []*result.ServerResult{
{
Result: result.ResultUnknown,
Error: errors.New("ocsp error"),
RevocationMethod: result.RevocationMethodOCSP,
},
},
}
err := revocationResult(certResult, certChain)
assertErrorEqual(`timestamping certificate with subject "CN=leafCert" revocation status is unknown`, err, t)
})
t.Run("OCSP error with fallback", func(t *testing.T) {
certResult[0] = &result.CertRevocationResult{
Result: result.ResultOK,
ServerResults: []*result.ServerResult{
{
Result: result.ResultUnknown,
Error: errors.New("ocsp error"),
RevocationMethod: result.RevocationMethodOCSP,
},
{
Result: result.ResultOK,
RevocationMethod: result.RevocationMethodCRL,
},
},
RevocationMethod: result.RevocationMethodOCSPFallbackCRL,
}
if err := revocationResult(certResult, certChain); err != nil {
t.Fatal(err)
}
})
t.Run("OCSP error with fallback and CRL error", func(t *testing.T) {
certResult[0] = &result.CertRevocationResult{
Result: result.ResultUnknown,
ServerResults: []*result.ServerResult{
{
Result: result.ResultUnknown,
Error: errors.New("ocsp error"),
RevocationMethod: result.RevocationMethodOCSP,
},
{
Result: result.ResultUnknown,
Error: errors.New("crl error"),
RevocationMethod: result.RevocationMethodCRL,
},
},
RevocationMethod: result.RevocationMethodOCSPFallbackCRL,
}
err := revocationResult(certResult, certChain)
assertErrorEqual(`timestamping certificate with subject "CN=leafCert" revocation status is unknown`, err, t)
})
t.Run("revoked", func(t *testing.T) {
certResult[0] = &result.CertRevocationResult{
Result: result.ResultRevoked,
ServerResults: []*result.ServerResult{
{
Result: result.ResultRevoked,
Error: errors.New("revoked"),
RevocationMethod: result.RevocationMethodCRL,
},
},
}
err := revocationResult(certResult, certChain)
assertErrorEqual(`timestamping certificate with subject "CN=leafCert" is revoked`, err, t)
})
t.Run("revocation method unknown error(should never reach here)", func(t *testing.T) {
certResult[0] = &result.CertRevocationResult{
Result: result.ResultUnknown,
ServerResults: []*result.ServerResult{
{
Result: result.ResultUnknown,
Error: errors.New("unknown error"),
RevocationMethod: result.RevocationMethodUnknown,
},
},
}
err := revocationResult(certResult, certChain)
assertErrorEqual(`timestamping certificate with subject "CN=leafCert" revocation status is unknown`, err, t)
})
t.Run("empty cert result", func(t *testing.T) {
err := revocationResult([]*result.CertRevocationResult{}, certChain)
assertErrorEqual("certificate revocation result cannot be empty", err, t)
})
t.Run("cert result length does not equal to cert chain", func(t *testing.T) {
err := revocationResult([]*result.CertRevocationResult{
certResult[1],
}, certChain)
assertErrorEqual("length of certificate revocation result 1 does not match length of the certificate chain 2", 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
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.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")
}
type dummyTSARevocationValidator struct {
failOnValidate bool
revoked bool
}
func (v *dummyTSARevocationValidator) ValidateContext(ctx context.Context, validateContextOpts revocation.ValidateContextOptions) ([]*result.CertRevocationResult, error) {
if v.failOnValidate {
return nil, errors.New("failed in ValidateContext")
}
if v.revoked {
certResults := make([]*result.CertRevocationResult, len(validateContextOpts.CertChain))
for i := range certResults {
certResults[i] = &result.CertRevocationResult{
Result: result.ResultOK,
ServerResults: []*result.ServerResult{
{
Result: result.ResultOK,
RevocationMethod: result.RevocationMethodOCSP,
},
},
}
}
certResults[0] = &result.CertRevocationResult{
Result: result.ResultRevoked,
ServerResults: []*result.ServerResult{
{
Result: result.ResultRevoked,
Error: errors.New("revoked"),
RevocationMethod: result.RevocationMethodCRL,
},
},
}
certResults[len(certResults)-1] = &result.CertRevocationResult{
Result: result.ResultNonRevokable,
ServerResults: []*result.ServerResult{
{
Result: result.ResultNonRevokable,
},
},
}
return certResults, nil
}
return nil, nil
}

28
revocation/crl/bundle.go Normal file
View File

@ -0,0 +1,28 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crl
import "crypto/x509"
// Bundle is a collection of CRLs, including base and delta CRLs
type Bundle struct {
// BaseCRL is the parsed base CRL
BaseCRL *x509.RevocationList
// DeltaCRL is the parsed delta CRL
//
// TODO: support delta CRL https://github.com/notaryproject/notation-core-go/issues/228
// It will always be nil until we support delta CRL
DeltaCRL *x509.RevocationList
}

32
revocation/crl/cache.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crl
import "context"
// Cache is an interface that specifies methods used for caching
type Cache interface {
// Get retrieves the CRL bundle with the given url
//
// url is the key to retrieve the CRL bundle
//
// if the key does not exist or the content is expired, return ErrCacheMiss.
Get(ctx context.Context, url string) (*Bundle, error)
// Set stores the CRL bundle with the given url
//
// url is the key to store the CRL bundle
// bundle is the CRL collections to store
Set(ctx context.Context, url string, bundle *Bundle) error
}

22
revocation/crl/errors.go Normal file
View File

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

266
revocation/crl/fetcher.go Normal file
View File

@ -0,0 +1,266 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package crl provides Fetcher interface with its implementation, and the
// Cache interface.
package crl
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"time"
"github.com/notaryproject/notation-core-go/revocation/internal/x509util"
"golang.org/x/crypto/cryptobyte"
cbasn1 "golang.org/x/crypto/cryptobyte/asn1"
)
// oidFreshestCRL is the object identifier for the distribution point
// for the delta CRL. (See RFC 5280, Section 5.2.6)
var oidFreshestCRL = asn1.ObjectIdentifier{2, 5, 29, 46}
// maxCRLSize is the maximum size of CRL in bytes
//
// The 32 MiB limit is based on investigation that even the largest CRLs
// are less than 16 MiB. The limit is set to 32 MiB to prevent
const maxCRLSize = 32 * 1024 * 1024 // 32 MiB
// Fetcher is an interface that specifies methods used for fetching CRL
// from the given URL
type Fetcher interface {
// Fetch retrieves the CRL from the given URL.
Fetch(ctx context.Context, url string) (*Bundle, error)
}
// HTTPFetcher is a Fetcher implementation that fetches CRL from the given URL
type HTTPFetcher struct {
// Cache stores fetched CRLs and reuses them until the CRL reaches the
// NextUpdate time.
// If Cache is nil, no cache is used.
Cache Cache
// DiscardCacheError specifies whether to discard any error on cache.
//
// ErrCacheMiss is not considered as an failure and will not be returned as
// an error if DiscardCacheError is false.
DiscardCacheError bool
httpClient *http.Client
}
// NewHTTPFetcher creates a new HTTPFetcher with the given HTTP client
func NewHTTPFetcher(httpClient *http.Client) (*HTTPFetcher, error) {
if httpClient == nil {
return nil, errors.New("httpClient cannot be nil")
}
return &HTTPFetcher{
httpClient: httpClient,
}, nil
}
// Fetch retrieves the CRL from the given URL
//
// If cache is not nil, try to get the CRL from the cache first. On failure
// (e.g. cache miss), it will download the CRL from the URL and store it to the
// cache.
func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) {
if url == "" {
return nil, errors.New("CRL URL cannot be empty")
}
if f.Cache != nil {
bundle, err := f.Cache.Get(ctx, url)
if err == nil {
// check expiry of base CRL and delta CRL
if isEffective(bundle.BaseCRL) && (bundle.DeltaCRL == nil || isEffective(bundle.DeltaCRL)) {
return bundle, nil
}
} else if !errors.Is(err, ErrCacheMiss) && !f.DiscardCacheError {
return nil, fmt.Errorf("failed to retrieve CRL from cache: %w", err)
}
}
bundle, err := f.fetch(ctx, url)
if err != nil {
return nil, fmt.Errorf("failed to retrieve CRL: %w", err)
}
if f.Cache != nil {
err = f.Cache.Set(ctx, url, bundle)
if err != nil && !f.DiscardCacheError {
return nil, fmt.Errorf("failed to store CRL to cache: %w", err)
}
}
return bundle, nil
}
// isEffective checks if the CRL is effective by checking the NextUpdate time.
func isEffective(crl *x509.RevocationList) bool {
return !crl.NextUpdate.IsZero() && !time.Now().After(crl.NextUpdate)
}
// fetch downloads the CRL from the given URL.
func (f *HTTPFetcher) fetch(ctx context.Context, url string) (*Bundle, error) {
// fetch base CRL
base, err := fetchCRL(ctx, url, f.httpClient)
if err != nil {
return nil, err
}
// fetch delta CRL from base CRL extension
deltaCRL, err := f.fetchDeltaCRL(ctx, base.Extensions)
if err != nil && !errors.Is(err, errDeltaCRLNotFound) {
return nil, err
}
return &Bundle{
BaseCRL: base,
DeltaCRL: deltaCRL,
}, nil
}
// fetchDeltaCRL fetches the delta CRL from the given extensions of base CRL.
//
// It returns errDeltaCRLNotFound if the delta CRL is not found.
func (f *HTTPFetcher) fetchDeltaCRL(ctx context.Context, extensions []pkix.Extension) (*x509.RevocationList, error) {
extension := x509util.FindExtensionByOID(extensions, oidFreshestCRL)
if extension == nil {
return nil, errDeltaCRLNotFound
}
// RFC 5280, 4.2.1.15
// id-ce-freshestCRL OBJECT IDENTIFIER ::= { id-ce 46 }
//
// FreshestCRL ::= CRLDistributionPoints
urls, err := parseCRLDistributionPoint(extension.Value)
if err != nil {
return nil, fmt.Errorf("failed to parse Freshest CRL extension: %w", err)
}
if len(urls) == 0 {
return nil, errDeltaCRLNotFound
}
var (
lastError error
deltaCRL *x509.RevocationList
)
for _, cdpURL := range urls {
// RFC 5280, 5.2.6
// Delta CRLs from the base CRL have the same scope as the base
// CRL, so the URLs are for redundancy and should be tried in
// order until one succeeds.
deltaCRL, lastError = fetchCRL(ctx, cdpURL, f.httpClient)
if lastError == nil {
return deltaCRL, nil
}
}
return nil, lastError
}
// parseCRLDistributionPoint parses the CRL extension and returns the CRL URLs
//
// value is the raw value of the CRL distribution point extension
func parseCRLDistributionPoint(value []byte) ([]string, error) {
var urls []string
// borrowed from crypto/x509: https://cs.opensource.google/go/go/+/refs/tags/go1.23.4:src/crypto/x509/parser.go;l=700-743
//
// RFC 5280, 4.2.1.13
//
// CRLDistributionPoints ::= SEQUENCE SIZE (1..MAX) OF DistributionPoint
//
// DistributionPoint ::= SEQUENCE {
// distributionPoint [0] DistributionPointName OPTIONAL,
// reasons [1] ReasonFlags OPTIONAL,
// cRLIssuer [2] GeneralNames OPTIONAL }
//
// DistributionPointName ::= CHOICE {
// fullName [0] GeneralNames,
// nameRelativeToCRLIssuer [1] RelativeDistinguishedName }
val := cryptobyte.String(value)
if !val.ReadASN1(&val, cbasn1.SEQUENCE) {
return nil, errors.New("x509: invalid CRL distribution points")
}
for !val.Empty() {
var dpDER cryptobyte.String
if !val.ReadASN1(&dpDER, cbasn1.SEQUENCE) {
return nil, errors.New("x509: invalid CRL distribution point")
}
var dpNameDER cryptobyte.String
var dpNamePresent bool
if !dpDER.ReadOptionalASN1(&dpNameDER, &dpNamePresent, cbasn1.Tag(0).Constructed().ContextSpecific()) {
return nil, errors.New("x509: invalid CRL distribution point")
}
if !dpNamePresent {
continue
}
if !dpNameDER.ReadASN1(&dpNameDER, cbasn1.Tag(0).Constructed().ContextSpecific()) {
return nil, errors.New("x509: invalid CRL distribution point")
}
for !dpNameDER.Empty() {
if !dpNameDER.PeekASN1Tag(cbasn1.Tag(6).ContextSpecific()) {
break
}
var uri cryptobyte.String
if !dpNameDER.ReadASN1(&uri, cbasn1.Tag(6).ContextSpecific()) {
return nil, errors.New("x509: invalid CRL distribution point")
}
urls = append(urls, string(uri))
}
}
return urls, nil
}
func fetchCRL(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) {
// validate URL
parsedURL, err := url.Parse(crlURL)
if err != nil {
return nil, fmt.Errorf("invalid CRL URL: %w", err)
}
if parsedURL.Scheme != "http" {
return nil, fmt.Errorf("unsupported scheme: %s. Only supports CRL URL in HTTP protocol", parsedURL.Scheme)
}
// download CRL
req, err := http.NewRequestWithContext(ctx, http.MethodGet, crlURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create CRL request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode)
}
// read with size limit
data, err := io.ReadAll(io.LimitReader(resp.Body, maxCRLSize))
if err != nil {
return nil, fmt.Errorf("failed to read CRL response: %w", err)
}
if len(data) == maxCRLSize {
return nil, fmt.Errorf("CRL size exceeds the limit: %d", maxCRLSize)
}
// parse CRL
return x509.ParseRevocationList(data)
}

View File

@ -0,0 +1,638 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package crl
import (
"bytes"
"context"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"os"
"strings"
"sync"
"testing"
"time"
"github.com/notaryproject/notation-core-go/testhelper"
)
func TestNewHTTPFetcher(t *testing.T) {
t.Run("httpClient is nil", func(t *testing.T) {
_, err := NewHTTPFetcher(nil)
if err.Error() != "httpClient cannot be nil" {
t.Errorf("NewHTTPFetcher() error = %v, want %v", err, "httpClient cannot be nil")
}
})
}
func TestFetch(t *testing.T) {
// prepare crl
certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true)
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
Number: big.NewInt(1),
NextUpdate: time.Now().Add(1 * time.Hour),
}, certChain[1].Cert, certChain[1].PrivateKey)
if err != nil {
t.Fatalf("failed to create base CRL: %v", err)
}
baseCRL, err := x509.ParseRevocationList(crlBytes)
if err != nil {
t.Fatalf("failed to parse base CRL: %v", err)
}
const exampleURL = "http://localhost.test"
const uncachedURL = "http://uncached.test"
bundle := &Bundle{
BaseCRL: baseCRL,
}
t.Run("url is empty", func(t *testing.T) {
c := &memoryCache{}
httpClient := &http.Client{}
f, err := NewHTTPFetcher(httpClient)
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
f.Cache = c
_, err = f.Fetch(context.Background(), "")
if err.Error() != "CRL URL cannot be empty" {
t.Fatalf("Fetcher.Fetch() error = %v, want CRL URL cannot be empty", err)
}
})
t.Run("fetch without cache", func(t *testing.T) {
httpClient := &http.Client{
Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
}
f, err := NewHTTPFetcher(httpClient)
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
bundle, err := f.Fetch(context.Background(), exampleURL)
if err != nil {
t.Errorf("Fetcher.Fetch() error = %v, want nil", err)
}
if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) {
t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw)
}
})
t.Run("cache hit", func(t *testing.T) {
// set the cache
c := &memoryCache{}
if err := c.Set(context.Background(), exampleURL, bundle); err != nil {
t.Errorf("Cache.Set() error = %v, want nil", err)
}
httpClient := &http.Client{}
f, err := NewHTTPFetcher(httpClient)
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
f.Cache = c
bundle, err := f.Fetch(context.Background(), exampleURL)
if err != nil {
t.Errorf("Fetcher.Fetch() error = %v, want nil", err)
}
if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) {
t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw)
}
})
t.Run("cache miss and download failed error", func(t *testing.T) {
c := &memoryCache{}
httpClient := &http.Client{
Transport: errorRoundTripperMock{},
}
f, err := NewHTTPFetcher(httpClient)
f.Cache = c
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
_, err = f.Fetch(context.Background(), uncachedURL)
if err == nil {
t.Errorf("Fetcher.Fetch() error = nil, want not nil")
}
})
t.Run("cache miss", func(t *testing.T) {
c := &memoryCache{}
httpClient := &http.Client{
Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
}
f, err := NewHTTPFetcher(httpClient)
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
f.Cache = c
f.DiscardCacheError = false
bundle, err := f.Fetch(context.Background(), uncachedURL)
if err != nil {
t.Errorf("Fetcher.Fetch() error = %v, want nil", err)
}
if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) {
t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw)
}
})
t.Run("cache expired", func(t *testing.T) {
c := &memoryCache{}
// prepare an expired CRL
certChain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true)
expiredCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
Number: big.NewInt(1),
NextUpdate: time.Now().Add(-1 * time.Hour),
}, certChain[1].Cert, certChain[1].PrivateKey)
if err != nil {
t.Fatalf("failed to create base CRL: %v", err)
}
expiredCRL, err := x509.ParseRevocationList(expiredCRLBytes)
if err != nil {
t.Fatalf("failed to parse base CRL: %v", err)
}
// store the expired CRL
const expiredCRLURL = "http://localhost.test/expired"
bundle := &Bundle{
BaseCRL: expiredCRL,
}
if err := c.Set(context.Background(), expiredCRLURL, bundle); err != nil {
t.Errorf("Cache.Set() error = %v, want nil", err)
}
// fetch the expired CRL
httpClient := &http.Client{
Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
}
f, err := NewHTTPFetcher(httpClient)
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
f.Cache = c
f.DiscardCacheError = true
bundle, err = f.Fetch(context.Background(), expiredCRLURL)
if err != nil {
t.Errorf("Fetcher.Fetch() error = %v, want nil", err)
}
// should re-download the CRL
if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) {
t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw)
}
})
t.Run("Set cache error", func(t *testing.T) {
c := &errorCache{
GetError: ErrCacheMiss,
SetError: errors.New("cache error"),
}
httpClient := &http.Client{
Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
}
f, err := NewHTTPFetcher(httpClient)
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
f.Cache = c
f.DiscardCacheError = true
bundle, err = f.Fetch(context.Background(), exampleURL)
if err != nil {
t.Errorf("Fetcher.Fetch() error = %v, want nil", err)
}
if !bytes.Equal(bundle.BaseCRL.Raw, baseCRL.Raw) {
t.Errorf("Fetcher.Fetch() base.Raw = %v, want %v", bundle.BaseCRL.Raw, baseCRL.Raw)
}
})
t.Run("Get error without discard", func(t *testing.T) {
c := &errorCache{
GetError: errors.New("cache error"),
}
httpClient := &http.Client{
Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
}
f, err := NewHTTPFetcher(httpClient)
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
f.Cache = c
f.DiscardCacheError = false
_, err = f.Fetch(context.Background(), exampleURL)
if !strings.HasPrefix(err.Error(), "failed to retrieve CRL from cache:") {
t.Errorf("Fetcher.Fetch() error = %v, want failed to retrieve CRL from cache:", err)
}
})
t.Run("Set error without discard", func(t *testing.T) {
c := &errorCache{
GetError: ErrCacheMiss,
SetError: errors.New("cache error"),
}
httpClient := &http.Client{
Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
}
f, err := NewHTTPFetcher(httpClient)
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
f.Cache = c
f.DiscardCacheError = false
_, err = f.Fetch(context.Background(), exampleURL)
if !strings.HasPrefix(err.Error(), "failed to store CRL to cache:") {
t.Errorf("Fetcher.Fetch() error = %v, want failed to store CRL to cache:", err)
}
})
t.Run("test fetch delta CRL from base CRL extension failed", func(t *testing.T) {
crlWithDeltaCRL, err := os.ReadFile("testdata/crlWithMultipleFreshestCRLs.crl")
if err != nil {
t.Fatalf("failed to read CRL: %v", err)
}
httpClient := &http.Client{
Transport: &expectedRoundTripperMock{
Body: crlWithDeltaCRL,
SecondRoundBody: []byte("invalid crl"),
},
}
f, err := NewHTTPFetcher(httpClient)
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
_, err = f.Fetch(context.Background(), exampleURL)
expectedErrorMsg := "failed to retrieve CRL: x509: malformed crl"
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, err)
}
})
}
func TestParseFreshestCRL(t *testing.T) {
loadExtentsion := func(certPath string) pkix.Extension {
certData, err := os.ReadFile(certPath)
if err != nil {
t.Fatalf("failed to read certificate: %v", err)
}
block, _ := pem.Decode(certData)
if block == nil {
t.Fatalf("failed to decode PEM block")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to parse certificate: %v", err)
}
for _, ext := range cert.Extensions {
if ext.Id.Equal([]int{2, 5, 29, 46}) { // id-ce-freshestCRL
return ext
}
}
t.Fatalf("freshestCRL extension not found")
return pkix.Extension{}
}
t.Run("valid 1 delta CRL URL", func(t *testing.T) {
certPath := "testdata/certificateWithDeltaCRL.cer"
freshestCRLExtension := loadExtentsion(certPath)
urls, err := parseCRLDistributionPoint(freshestCRLExtension.Value)
if err != nil {
t.Fatalf("failed to parse freshest CRL: %v", err)
}
if len(urls) != 1 {
t.Fatalf("expected 1 URL, got %d", len(urls))
}
if !strings.HasPrefix(urls[0], "http://localhost:80") {
t.Fatalf("unexpected URL: %s", urls[0])
}
})
t.Run("empty extension", func(t *testing.T) {
_, err := parseCRLDistributionPoint(nil)
if err == nil {
t.Fatalf("expected error")
}
})
t.Run("URL doesn't exist", func(t *testing.T) {
certPath := "testdata/certificateWithZeroDeltaCRLURL.cer"
freshestCRLExtension := loadExtentsion(certPath)
url, err := parseCRLDistributionPoint(freshestCRLExtension.Value)
if err != nil {
t.Fatalf("failed to parse freshest CRL: %v", err)
}
if len(url) != 0 {
t.Fatalf("expected 0 URL, got %d", len(url))
}
})
t.Run("non URI freshest CRL extension", func(t *testing.T) {
certPath := "testdata/certificateWithNonURIDeltaCRL.cer"
freshestCRLExtension := loadExtentsion(certPath)
url, err := parseCRLDistributionPoint(freshestCRLExtension.Value)
if err != nil {
t.Fatalf("failed to parse freshest CRL: %v", err)
}
if len(url) != 0 {
t.Fatalf("expected 0 URL, got %d", len(url))
}
})
t.Run("certificate with incomplete freshest CRL extension", func(t *testing.T) {
certPath := "testdata/certificateWithIncompleteFreshestCRL.cer"
freshestCRLExtension := loadExtentsion(certPath)
_, err := parseCRLDistributionPoint(freshestCRLExtension.Value)
expectErrorMsg := "x509: invalid CRL distribution point"
if err == nil || err.Error() != expectErrorMsg {
t.Fatalf("expected error %q, got %v", expectErrorMsg, err)
}
})
t.Run("certificate with incomplete freshest CRL extension2", func(t *testing.T) {
certPath := "testdata/certificateWithIncompleteFreshestCRL2.cer"
freshestCRLExtension := loadExtentsion(certPath)
url, err := parseCRLDistributionPoint(freshestCRLExtension.Value)
if err != nil {
t.Fatalf("failed to parse freshest CRL: %v", err)
}
if len(url) != 0 {
t.Fatalf("expected 0 URL, got %d", len(url))
}
})
}
func TestFetchDeltaCRL(t *testing.T) {
loadExtentsion := func(certPath string) []pkix.Extension {
certData, err := os.ReadFile(certPath)
if err != nil {
t.Fatalf("failed to read certificate: %v", err)
}
block, _ := pem.Decode(certData)
if block == nil {
t.Fatalf("failed to decode PEM block")
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("failed to parse certificate: %v", err)
}
return cert.Extensions
}
deltaCRL, err := os.ReadFile("testdata/delta.crl")
if err != nil {
t.Fatalf("failed to read delta CRL: %v", err)
}
fetcher, err := NewHTTPFetcher(&http.Client{
Transport: &expectedRoundTripperMock{Body: deltaCRL},
})
if err != nil {
t.Fatalf("failed to create fetcher: %v", err)
}
t.Run("parse freshest CRL failed", func(t *testing.T) {
certPath := "testdata/certificateWithIncompleteFreshestCRL.cer"
extensions := loadExtentsion(certPath)
_, err := fetcher.fetchDeltaCRL(context.Background(), extensions)
expectedErrorMsg := "failed to parse Freshest CRL extension: x509: invalid CRL distribution point"
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, err)
}
})
t.Run("zero freshest CRL URL", func(t *testing.T) {
certPath := "testdata/certificateWithZeroDeltaCRLURL.cer"
extensions := loadExtentsion(certPath)
_, err := fetcher.fetchDeltaCRL(context.Background(), extensions)
expectedErr := errDeltaCRLNotFound
if err == nil || !errors.Is(err, expectedErr) {
t.Fatalf("expected error %v, got %v", expectedErr, err)
}
})
t.Run("one freshest CRL URL", func(t *testing.T) {
certPath := "testdata/certificateWithDeltaCRL.cer"
extensions := loadExtentsion(certPath)
deltaCRL, err := fetcher.fetchDeltaCRL(context.Background(), extensions)
if err != nil {
t.Fatalf("failed to process delta CRL: %v", err)
}
if deltaCRL == nil {
t.Fatalf("expected non-nil delta CRL")
}
})
t.Run("multiple freshest CRL URLs failed", func(t *testing.T) {
fetcherWithError, err := NewHTTPFetcher(&http.Client{
Transport: errorRoundTripperMock{},
})
if err != nil {
t.Fatalf("failed to create fetcher: %v", err)
}
certPath := "testdata/certificateWith2DeltaCRL.cer"
extensions := loadExtentsion(certPath)
_, err = fetcherWithError.fetchDeltaCRL(context.Background(), extensions)
expectedErrorMsg := "request failed"
if err == nil || !strings.Contains(err.Error(), expectedErrorMsg) {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, err)
}
})
t.Run("process delta crl from certificate extension failed", func(t *testing.T) {
certPath := "testdata/certificateWithIncompleteFreshestCRL.cer"
extensions := loadExtentsion(certPath)
_, err := fetcher.fetchDeltaCRL(context.Background(), extensions)
expectedErrorMsg := "failed to parse Freshest CRL extension: x509: invalid CRL distribution point"
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, err)
}
})
}
func TestDownload(t *testing.T) {
t.Run("parse url error", func(t *testing.T) {
_, err := fetchCRL(context.Background(), ":", http.DefaultClient)
if err == nil {
t.Fatal("expected error")
}
})
t.Run("https download", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "https://localhost.test", http.DefaultClient)
if err == nil {
t.Fatal("expected error")
}
})
t.Run("http.NewRequestWithContext error", func(t *testing.T) {
var ctx context.Context = nil
_, err := fetchCRL(ctx, "http://localhost.test", &http.Client{})
if err == nil {
t.Fatal("expected error")
}
})
t.Run("client.Do error", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "http://localhost.test", &http.Client{
Transport: errorRoundTripperMock{},
})
if err == nil {
t.Fatal("expected error")
}
})
t.Run("status code is not 2xx", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "http://localhost.test", &http.Client{
Transport: serverErrorRoundTripperMock{},
})
if err == nil {
t.Fatal("expected error")
}
})
t.Run("readAll error", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "http://localhost.test", &http.Client{
Transport: readFailedRoundTripperMock{},
})
if err == nil {
t.Fatal("expected error")
}
})
t.Run("exceed the size limit", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "http://localhost.test", &http.Client{
Transport: &expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)},
})
if err == nil {
t.Fatal("expected error")
}
})
t.Run("invalid crl", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "http://localhost.test", &http.Client{
Transport: &expectedRoundTripperMock{Body: []byte("invalid crl")},
})
if err == nil {
t.Fatal("expected error")
}
})
}
type errorRoundTripperMock struct{}
func (rt errorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) {
return nil, fmt.Errorf("error")
}
type serverErrorRoundTripperMock struct{}
func (rt serverErrorRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
Request: req,
StatusCode: http.StatusInternalServerError,
}, nil
}
type readFailedRoundTripperMock struct{}
func (rt readFailedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: errorReaderMock{},
}, nil
}
type errorReaderMock struct{}
func (r errorReaderMock) Read(p []byte) (n int, err error) {
return 0, fmt.Errorf("error")
}
func (r errorReaderMock) Close() error {
return nil
}
type expectedRoundTripperMock struct {
Body []byte
SecondRoundBody []byte
count int
}
func (rt *expectedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) {
if rt.count == 0 {
rt.count += 1
return &http.Response{
Request: req,
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(rt.Body)),
}, nil
}
return &http.Response{
Request: req,
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(rt.SecondRoundBody)),
}, nil
}
// memoryCache is an in-memory cache that stores CRL bundles for testing.
type memoryCache struct {
store sync.Map
}
// Get retrieves the CRL from the memory store.
//
// - if the key does not exist, return ErrNotFound
// - if the CRL is expired, return ErrCacheMiss
func (c *memoryCache) Get(ctx context.Context, url string) (*Bundle, error) {
value, ok := c.store.Load(url)
if !ok {
return nil, ErrCacheMiss
}
bundle, ok := value.(*Bundle)
if !ok {
return nil, fmt.Errorf("invalid type: %T", value)
}
return bundle, nil
}
// Set stores the CRL in the memory store.
func (c *memoryCache) Set(ctx context.Context, url string, bundle *Bundle) error {
c.store.Store(url, bundle)
return nil
}
type errorCache struct {
GetError error
SetError error
}
func (c *errorCache) Get(ctx context.Context, url string) (*Bundle, error) {
return nil, c.GetError
}
func (c *errorCache) Set(ctx context.Context, url string, bundle *Bundle) error {
return c.SetError
}

View File

@ -0,0 +1,32 @@
-----BEGIN CERTIFICATE-----
MIIFdTCCA92gAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL
BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV
BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr
c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD
DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm
VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71
E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+
j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajggJ0MIICcDAMBgNVHRMBAf8E
AjAAMB8GA1UdIwQYMBaAFFPceeG3G4d5oezFSybFwehynbPqMIIBTQYDVR0uBIIB
RDCCAUAwggE8oIIBOKCCATSGgZdodHRwOi8vbG9jYWxob3N0OjgwL2VqYmNhL3B1
YmxpY3dlYi93ZWJkaXN0L2NlcnRkaXN0P2NtZD1kZWx0YWNybCZpc3N1ZXI9VUlE
JTNEYy0wbnd3b3pxdmF3dmFvM2VjaCUyQ0NOJTNETWFuYWdlbWVudENBJTJDTyUz
REVKQkNBK0NvbnRhaW5lcitRdWlja3N0YXJ0hoGXaHR0cDovL2xvY2FsaG9zdDo4
MC9lamJjYS9wdWJsaWN3ZWIvd2ViZGlzdC9jZXJ0ZGlzdD9jbWQ9ZGVsdGFjcmwm
aXNzdWVyPVVJRCUzRGMtMG53d296cXZhd3ZhbzNlY2glMkNDTiUzRE1hbmFnZW1l
bnRDQSUyQ08lM0RFSkJDQStDb250YWluZXIrUXVpY2tzdGFydDATBgNVHSUEDDAK
BggrBgEFBQcDATCBqQYDVR0fBIGhMIGeMIGboIGYoIGVhoGSaHR0cDovL2xvY2Fs
aG9zdDo4MC9lamJjYS9wdWJsaWN3ZWIvd2ViZGlzdC9jZXJ0ZGlzdD9jbWQ9Y3Js
Jmlzc3Vlcj1VSUQlM0RjLTBud3dvenF2YXd2YW8zZWNoJTJDQ04lM0RNYW5hZ2Vt
ZW50Q0ElMkNPJTNERUpCQ0ErQ29udGFpbmVyK1F1aWNrc3RhcnQwHQYDVR0OBBYE
FDHE82/06xOocYbvMIyGt2gofk88MA4GA1UdDwEB/wQEAwIFoDANBgkqhkiG9w0B
AQsFAAOCAYEAV7G7WMPn3tQNqB8RxYATV3eVhB3WC5BxqBbQzp2loNycDRmX95fa
7EV5xcPIUv42B+TzLu/ann9FLOMkqEhA+F5zsEomikUA+L4cuIWLXUhwIWwE2I/p
fgHJ61JtMMxv3rWQHyo6YpzpIAG23oxGXzrlN4/oNfWzWMIYlcl4xiHxC2vOKnNO
wId3Ck3jsJE10tImdD/tQYXh7h5ueESyPUZtqM/g2QPap+tEHArpgfAQdpEvRj1v
ZWAotcEIr+a5popE56UaE4a29DspVTA1rVchhKYl2gpDxieSQgQr61fWHXzRoKcd
FZ+NgqJuwd9CxrXbkl6EKDpefivwz9G4b3b6R8lMl+wmeTgPvSUYO34c8GsF143H
V/VKNoBvoz44QyLUf+1+XiHuUjfHaXCtXmDOoQ64M3d9gGrz23G5KJAUBubcbcdu
7Ah3D5zqvieGgoYt8qye1nsIVYC1KrYP2Kp5jWCadLvIwu2B0j7eA+LwN2MBWdlh
dRTSOVkICjWU
-----END CERTIFICATE-----

View File

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIE1TCCAz2gAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL
BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV
BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr
c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD
DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm
VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71
E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+
j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajggHUMIIB0DAMBgNVHRMBAf8E
AjAAMB8GA1UdIwQYMBaAFFPceeG3G4d5oezFSybFwehynbPqMIGuBgNVHS4EgaYw
gaMwgaCggZ2ggZqGgZdodHRwOi8vbG9jYWxob3N0OjgwL2VqYmNhL3B1YmxpY3dl
Yi93ZWJkaXN0L2NlcnRkaXN0P2NtZD1kZWx0YWNybCZpc3N1ZXI9VUlEJTNEYy0w
bnd3b3pxdmF3dmFvM2VjaCUyQ0NOJTNETWFuYWdlbWVudENBJTJDTyUzREVKQkNB
K0NvbnRhaW5lcitRdWlja3N0YXJ0MBMGA1UdJQQMMAoGCCsGAQUFBwMBMIGpBgNV
HR8EgaEwgZ4wgZuggZiggZWGgZJodHRwOi8vbG9jYWxob3N0OjgwL2VqYmNhL3B1
YmxpY3dlYi93ZWJkaXN0L2NlcnRkaXN0P2NtZD1jcmwmaXNzdWVyPVVJRCUzRGMt
MG53d296cXZhd3ZhbzNlY2glMkNDTiUzRE1hbmFnZW1lbnRDQSUyQ08lM0RFSkJD
QStDb250YWluZXIrUXVpY2tzdGFydDAdBgNVHQ4EFgQUMcTzb/TrE6hxhu8wjIa3
aCh+TzwwDgYDVR0PAQH/BAQDAgWgMA0GCSqGSIb3DQEBCwUAA4IBgQBXsbtYw+fe
1A2oHxHFgBNXd5WEHdYLkHGoFtDOnaWg3JwNGZf3l9rsRXnFw8hS/jYH5PMu79qe
f0Us4ySoSED4XnOwSiaKRQD4vhy4hYtdSHAhbATYj+l+AcnrUm0wzG/etZAfKjpi
nOkgAbbejEZfOuU3j+g19bNYwhiVyXjGIfELa84qc07Ah3cKTeOwkTXS0iZ0P+1B
heHuHm54RLI9Rm2oz+DZA9qn60QcCumB8BB2kS9GPW9lYCi1wQiv5rmmikTnpRoT
hrb0OylVMDWtVyGEpiXaCkPGJ5JCBCvrV9YdfNGgpx0Vn42Com7B30LGtduSXoQo
Ol5+K/DP0bhvdvpHyUyX7CZ5OA+9JRg7fhzwawXXjcdX9Uo2gG+jPjhDItR/7X5e
Ie5SN8dpcK1eYM6hDrgzd32AavPbcbkokBQG5txtx27sCHcPnOq+J4aChi3yrJ7W
ewhVgLUqtg/YqnmNYJp0u8jC7YHSPt4D4vA3YwFZ2WF1FNI5WQgKNZQ=
-----END CERTIFICATE-----

View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDmTCCAgGgAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL
BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV
BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr
c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD
DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm
VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71
E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+
j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajgZkwgZYwDAYDVR0TAQH/BAIw
ADAfBgNVHSMEGDAWgBRT3HnhtxuHeaHsxUsmxcHocp2z6jANBgNVHS4EBjAEMAKg
ADATBgNVHSUEDDAKBggrBgEFBQcDATASBgNVHR8ECzAJMAegBaADhQH/MB0GA1Ud
DgQWBBQxxPNv9OsTqHGG7zCMhrdoKH5PPDAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZI
hvcNAQELBQADggGBAFexu1jD597UDagfEcWAE1d3lYQd1guQcagW0M6dpaDcnA0Z
l/eX2uxFecXDyFL+Ngfk8y7v2p5/RSzjJKhIQPhec7BKJopFAPi+HLiFi11IcCFs
BNiP6X4ByetSbTDMb961kB8qOmKc6SABtt6MRl865TeP6DX1s1jCGJXJeMYh8Qtr
zipzTsCHdwpN47CRNdLSJnQ/7UGF4e4ebnhEsj1GbajP4NkD2qfrRBwK6YHwEHaR
L0Y9b2VgKLXBCK/muaaKROelGhOGtvQ7KVUwNa1XIYSmJdoKQ8YnkkIEK+tX1h18
0aCnHRWfjYKibsHfQsa125JehCg6Xn4r8M/RuG92+kfJTJfsJnk4D70lGDt+HPBr
BdeNx1f1SjaAb6M+OEMi1H/tfl4h7lI3x2lwrV5gzqEOuDN3fYBq89txuSiQFAbm
3G3HbuwIdw+c6r4nhoKGLfKsntZ7CFWAtSq2D9iqeY1gmnS7yMLtgdI+3gPi8Ddj
AVnZYXUU0jlZCAo1lA==
-----END CERTIFICATE-----

View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDlzCCAf+gAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL
BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV
BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr
c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD
DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm
VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71
E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+
j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajgZcwgZQwDAYDVR0TAQH/BAIw
ADAfBgNVHSMEGDAWgBRT3HnhtxuHeaHsxUsmxcHocp2z6jALBgNVHS4EBDACMAAw
EwYDVR0lBAwwCgYIKwYBBQUHAwEwEgYDVR0fBAswCTAHoAWgA4UB/zAdBgNVHQ4E
FgQUMcTzb/TrE6hxhu8wjIa3aCh+TzwwDgYDVR0PAQH/BAQDAgWgMA0GCSqGSIb3
DQEBCwUAA4IBgQBXsbtYw+fe1A2oHxHFgBNXd5WEHdYLkHGoFtDOnaWg3JwNGZf3
l9rsRXnFw8hS/jYH5PMu79qef0Us4ySoSED4XnOwSiaKRQD4vhy4hYtdSHAhbATY
j+l+AcnrUm0wzG/etZAfKjpinOkgAbbejEZfOuU3j+g19bNYwhiVyXjGIfELa84q
c07Ah3cKTeOwkTXS0iZ0P+1BheHuHm54RLI9Rm2oz+DZA9qn60QcCumB8BB2kS9G
PW9lYCi1wQiv5rmmikTnpRoThrb0OylVMDWtVyGEpiXaCkPGJ5JCBCvrV9YdfNGg
px0Vn42Com7B30LGtduSXoQoOl5+K/DP0bhvdvpHyUyX7CZ5OA+9JRg7fhzwawXX
jcdX9Uo2gG+jPjhDItR/7X5eIe5SN8dpcK1eYM6hDrgzd32AavPbcbkokBQG5txt
x27sCHcPnOq+J4aChi3yrJ7WewhVgLUqtg/YqnmNYJp0u8jC7YHSPt4D4vA3YwFZ
2WF1FNI5WQgKNZQ=
-----END CERTIFICATE-----

View File

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDnjCCAgagAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL
BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV
BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr
c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD
DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm
VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71
E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+
j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajgZ4wgZswDAYDVR0TAQH/BAIw
ADAfBgNVHSMEGDAWgBRT3HnhtxuHeaHsxUsmxcHocp2z6jASBgNVHS4ECzAJMAeg
BaADhQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMBMBIGA1UdHwQLMAkwB6AFoAOFAf8w
HQYDVR0OBBYEFDHE82/06xOocYbvMIyGt2gofk88MA4GA1UdDwEB/wQEAwIFoDAN
BgkqhkiG9w0BAQsFAAOCAYEAV7G7WMPn3tQNqB8RxYATV3eVhB3WC5BxqBbQzp2l
oNycDRmX95fa7EV5xcPIUv42B+TzLu/ann9FLOMkqEhA+F5zsEomikUA+L4cuIWL
XUhwIWwE2I/pfgHJ61JtMMxv3rWQHyo6YpzpIAG23oxGXzrlN4/oNfWzWMIYlcl4
xiHxC2vOKnNOwId3Ck3jsJE10tImdD/tQYXh7h5ueESyPUZtqM/g2QPap+tEHArp
gfAQdpEvRj1vZWAotcEIr+a5popE56UaE4a29DspVTA1rVchhKYl2gpDxieSQgQr
61fWHXzRoKcdFZ+NgqJuwd9CxrXbkl6EKDpefivwz9G4b3b6R8lMl+wmeTgPvSUY
O34c8GsF143HV/VKNoBvoz44QyLUf+1+XiHuUjfHaXCtXmDOoQ64M3d9gGrz23G5
KJAUBubcbcdu7Ah3D5zqvieGgoYt8qye1nsIVYC1KrYP2Kp5jWCadLvIwu2B0j7e
A+LwN2MBWdlhdRTSOVkICjWU
-----END CERTIFICATE-----

View File

@ -0,0 +1,25 @@
-----BEGIN CERTIFICATE-----
MIIENTCCAp2gAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL
BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV
BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr
c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD
DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm
VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71
E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+
j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajggE0MIIBMDAMBgNVHRMBAf8E
AjAAMB8GA1UdIwQYMBaAFFPceeG3G4d5oezFSybFwehynbPqMA8GA1UdLgQIMAYw
BKACoAAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwgakGA1UdHwSBoTCBnjCBm6CBmKCB
lYaBkmh0dHA6Ly9sb2NhbGhvc3Q6ODAvZWpiY2EvcHVibGljd2ViL3dlYmRpc3Qv
Y2VydGRpc3Q/Y21kPWNybCZpc3N1ZXI9VUlEJTNEYy0wbnd3b3pxdmF3dmFvM2Vj
aCUyQ0NOJTNETWFuYWdlbWVudENBJTJDTyUzREVKQkNBK0NvbnRhaW5lcitRdWlj
a3N0YXJ0MB0GA1UdDgQWBBQxxPNv9OsTqHGG7zCMhrdoKH5PPDAOBgNVHQ8BAf8E
BAMCBaAwDQYJKoZIhvcNAQELBQADggGBAFexu1jD597UDagfEcWAE1d3lYQd1guQ
cagW0M6dpaDcnA0Zl/eX2uxFecXDyFL+Ngfk8y7v2p5/RSzjJKhIQPhec7BKJopF
APi+HLiFi11IcCFsBNiP6X4ByetSbTDMb961kB8qOmKc6SABtt6MRl865TeP6DX1
s1jCGJXJeMYh8QtrzipzTsCHdwpN47CRNdLSJnQ/7UGF4e4ebnhEsj1GbajP4NkD
2qfrRBwK6YHwEHaRL0Y9b2VgKLXBCK/muaaKROelGhOGtvQ7KVUwNa1XIYSmJdoK
Q8YnkkIEK+tX1h180aCnHRWfjYKibsHfQsa125JehCg6Xn4r8M/RuG92+kfJTJfs
Jnk4D70lGDt+HPBrBdeNx1f1SjaAb6M+OEMi1H/tfl4h7lI3x2lwrV5gzqEOuDN3
fYBq89txuSiQFAbm3G3HbuwIdw+c6r4nhoKGLfKsntZ7CFWAtSq2D9iqeY1gmnS7
yMLtgdI+3gPi8DdjAVnZYXUU0jlZCAo1lA==
-----END CERTIFICATE-----

Binary file not shown.

BIN
revocation/crl/testdata/delta.crl vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,388 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package crl provides methods for checking the revocation status of a
// certificate using CRL
package crl
import (
"context"
"crypto/x509"
"encoding/asn1"
"errors"
"fmt"
"math/big"
"time"
"github.com/notaryproject/notation-core-go/revocation/crl"
"github.com/notaryproject/notation-core-go/revocation/internal/x509util"
"github.com/notaryproject/notation-core-go/revocation/result"
"golang.org/x/crypto/cryptobyte"
)
// RFC 5280, 5.3.1
//
// CRLReason ::= ENUMERATED {
// unspecified (0),
// keyCompromise (1),
// cACompromise (2),
// affiliationChanged (3),
// superseded (4),
// cessationOfOperation (5),
// certificateHold (6),
// -- value 7 is not used
// removeFromCRL (8),
// privilegeWithdrawn (9),
// aACompromise (10) }
const (
reasonCodeCertificateHold = 6 // certificateHold
reasonCodeRemoveFromCRL = 8 // removeFromCRL
)
var (
// oidFreshestCRL is the object identifier for the distribution point
// for the delta CRL. (See RFC 5280, Section 5.2.6)
oidFreshestCRL = asn1.ObjectIdentifier{2, 5, 29, 46}
// oidIssuingDistributionPoint is the object identifier for the issuing
// distribution point CRL extension. (See RFC 5280, Section 5.2.5)
oidIssuingDistributionPoint = asn1.ObjectIdentifier{2, 5, 29, 28}
// oidDeltaCRLIndicator is the object identifier for the delta CRL indicator
// (See RFC 5280, Section 5.2.4)
oidDeltaCRLIndicator = asn1.ObjectIdentifier{2, 5, 29, 27}
// oidInvalidityDate is the object identifier for the invalidity date
// CRL entry extension. (See RFC 5280, Section 5.3.2)
oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24}
)
// CertCheckStatusOptions specifies values that are needed to check CRL.
type CertCheckStatusOptions struct {
// Fetcher is used to fetch the CRL from the CRL distribution points.
Fetcher crl.Fetcher
// SigningTime is used to compare with the invalidity date during revocation
// check.
SigningTime time.Time
}
// CertCheckStatus checks the revocation status of a certificate using CRL
//
// The function checks the revocation status of the certificate by downloading
// the CRL from the CRL distribution points specified in the certificate.
//
// If the invalidity date extension is present in the CRL entry and SigningTime
// is not zero, the certificate is considered revoked if the SigningTime is
// after the invalidity date. (See RFC 5280, Section 5.3.2)
func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts CertCheckStatusOptions) *result.CertRevocationResult {
if !Supported(cert) {
// CRL not enabled for this certificate.
return &result.CertRevocationResult{
Result: result.ResultNonRevokable,
ServerResults: []*result.ServerResult{{
RevocationMethod: result.RevocationMethodCRL,
Error: errors.New("CRL is not supported"),
Result: result.ResultNonRevokable,
}},
RevocationMethod: result.RevocationMethodCRL,
}
}
if opts.Fetcher == nil {
return &result.CertRevocationResult{
Result: result.ResultUnknown,
ServerResults: []*result.ServerResult{{
RevocationMethod: result.RevocationMethodCRL,
Error: errors.New("CRL fetcher cannot be nil"),
Result: result.ResultUnknown,
}},
RevocationMethod: result.RevocationMethodCRL,
}
}
var (
serverResults = make([]*result.ServerResult, 0, len(cert.CRLDistributionPoints))
lastErr error
crlURL string
hasFreshestCRLInCertificate = x509util.FindExtensionByOID(cert.Extensions, oidFreshestCRL) != nil
)
// The CRLDistributionPoints contains the URIs of all the CRL distribution
// points. Since it does not distinguish the reason field, it needs to check
// all the URIs to avoid missing any partial CRLs.
//
// For the majority of the certificates, there is only one CRL distribution
// point with one CRL URI, which will be cached, so checking all the URIs is
// not a performance issue.
for _, crlURL = range cert.CRLDistributionPoints {
bundle, err := opts.Fetcher.Fetch(ctx, crlURL)
if err != nil {
lastErr = fmt.Errorf("failed to download CRL from %s: %w", crlURL, err)
break
}
if hasFreshestCRLInCertificate && bundle.DeltaCRL == nil {
// | deltaCRL URL in cert | deltaCRL URL in baseCRL | support it? |
// |----------------------|-------------------------|-------------|
// | True | True | Yes |
// | True | False | No |
// | False | True | Yes |
// | False | False | Yes |
//
// if only the certificate has the freshest CRL, the bundle.DeltaCRL
// should be nil. We don't support this case now because the delta
// CRLs may have different scopes, but the Go built-in function
// skips the scope of the base CRL when parsing the certificate.
lastErr = errors.New("freshest CRL from certificate extension is not supported")
break
}
if err = validate(bundle, issuer); err != nil {
lastErr = fmt.Errorf("failed to validate CRL from %s: %w", crlURL, err)
break
}
crlResult, err := checkRevocation(cert, bundle, opts.SigningTime, crlURL)
if err != nil {
lastErr = fmt.Errorf("failed to check revocation status from %s: %w", crlURL, err)
break
}
if crlResult.Result == result.ResultRevoked {
return &result.CertRevocationResult{
Result: result.ResultRevoked,
ServerResults: []*result.ServerResult{crlResult},
RevocationMethod: result.RevocationMethodCRL,
}
}
serverResults = append(serverResults, crlResult)
}
if lastErr != nil {
return &result.CertRevocationResult{
Result: result.ResultUnknown,
ServerResults: []*result.ServerResult{
{
Result: result.ResultUnknown,
Server: crlURL,
Error: lastErr,
RevocationMethod: result.RevocationMethodCRL,
}},
RevocationMethod: result.RevocationMethodCRL,
}
}
return &result.CertRevocationResult{
Result: result.ResultOK,
ServerResults: serverResults,
RevocationMethod: result.RevocationMethodCRL,
}
}
// Supported checks if the certificate supports CRL.
func Supported(cert *x509.Certificate) bool {
return cert != nil && len(cert.CRLDistributionPoints) > 0
}
func validate(bundle *crl.Bundle, issuer *x509.Certificate) error {
// validate base CRL
baseCRL := bundle.BaseCRL
if err := validateCRL(baseCRL, issuer); err != nil {
return fmt.Errorf("failed to validate base CRL: %w", err)
}
if bundle.DeltaCRL == nil {
return nil
}
// validate delta CRL
// RFC 5280, Section 5.2.4
deltaCRL := bundle.DeltaCRL
if err := validateCRL(deltaCRL, issuer); err != nil {
return fmt.Errorf("failed to validate delta CRL: %w", err)
}
if deltaCRL.Number.Cmp(baseCRL.Number) <= 0 {
return fmt.Errorf("delta CRL number %d is not greater than the base CRL number %d", deltaCRL.Number, baseCRL.Number)
}
// check delta CRL indicator extension
extension := x509util.FindExtensionByOID(deltaCRL.Extensions, oidDeltaCRLIndicator)
if extension == nil {
return errors.New("delta CRL indicator extension is not found")
}
minimumBaseCRLNumber := new(big.Int)
value := cryptobyte.String(extension.Value)
if !value.ReadASN1Integer(minimumBaseCRLNumber) {
return errors.New("failed to parse delta CRL indicator extension")
}
if minimumBaseCRLNumber.Cmp(baseCRL.Number) > 0 {
return fmt.Errorf("delta CRL indicator %d is not less than or equal to the base CRL number %d", minimumBaseCRLNumber, baseCRL.Number)
}
return nil
}
func validateCRL(crl *x509.RevocationList, issuer *x509.Certificate) error {
// check signature
if err := crl.CheckSignatureFrom(issuer); err != nil {
return fmt.Errorf("CRL is not signed by CA %s: %w,", issuer.Subject, err)
}
// check validity
if crl.NextUpdate.IsZero() {
return errors.New("CRL NextUpdate is not set")
}
now := time.Now()
if now.After(crl.NextUpdate) {
return fmt.Errorf("expired CRL. Current time %v is after CRL NextUpdate %v", now, crl.NextUpdate)
}
for _, ext := range crl.Extensions {
switch {
case ext.Id.Equal(oidIssuingDistributionPoint):
// IssuingDistributionPoint is a critical extension that identifies
// the scope of the CRL. Since we will check all the CRL
// distribution points, it is not necessary to check this extension.
case ext.Id.Equal(oidDeltaCRLIndicator):
// will be checked in validate()
default:
if ext.Critical {
// unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2)
return fmt.Errorf("unsupported critical extension found in CRL: %v", ext.Id)
}
}
}
return nil
}
// checkRevocation checks if the certificate is revoked or not
func checkRevocation(cert *x509.Certificate, b *crl.Bundle, signingTime time.Time, crlURL string) (*result.ServerResult, error) {
if cert == nil {
return nil, errors.New("certificate cannot be nil")
}
if b == nil {
return nil, errors.New("CRL bundle cannot be nil")
}
if b.BaseCRL == nil {
return nil, errors.New("baseCRL cannot be nil")
}
// merge the base and delta CRLs in a single iterator
revocationListIter := func(yield func(*x509.RevocationListEntry) bool) {
for i := range b.BaseCRL.RevokedCertificateEntries {
if !yield(&b.BaseCRL.RevokedCertificateEntries[i]) {
return
}
}
if b.DeltaCRL != nil {
for i := range b.DeltaCRL.RevokedCertificateEntries {
if !yield(&b.DeltaCRL.RevokedCertificateEntries[i]) {
return
}
}
}
}
// latestTempRevokedEntry contains the most recent revocation entry with
// reasons such as CertificateHold or RemoveFromCRL.
//
// If the certificate is revoked with CertificateHold, it is temporarily
// revoked. If the certificate is shown in the CRL with RemoveFromCRL,
// it is unrevoked.
var latestTempRevokedEntry *x509.RevocationListEntry
// iterate over all the entries in the base and delta CRLs
for revocationEntry := range revocationListIter {
if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 {
extensions, err := parseEntryExtensions(revocationEntry)
if err != nil {
return nil, err
}
// validate signingTime and invalidityDate
if !signingTime.IsZero() && !extensions.invalidityDate.IsZero() &&
signingTime.Before(extensions.invalidityDate) {
// signing time is before the invalidity date which means the
// certificate is not revoked at the time of signing.
return &result.ServerResult{
Result: result.ResultOK,
Server: crlURL,
RevocationMethod: result.RevocationMethodCRL,
}, nil
}
switch revocationEntry.ReasonCode {
case reasonCodeCertificateHold, reasonCodeRemoveFromCRL:
// temporarily revoked or unrevoked
if latestTempRevokedEntry == nil || latestTempRevokedEntry.RevocationTime.Before(revocationEntry.RevocationTime) {
// the revocation status depends on the most recent reason
latestTempRevokedEntry = revocationEntry
}
default:
// permanently revoked
return &result.ServerResult{
Result: result.ResultRevoked,
Server: crlURL,
RevocationMethod: result.RevocationMethodCRL,
}, nil
}
}
}
if latestTempRevokedEntry != nil && latestTempRevokedEntry.ReasonCode == reasonCodeCertificateHold {
// revoked with CertificateHold
return &result.ServerResult{
Result: result.ResultRevoked,
Server: crlURL,
RevocationMethod: result.RevocationMethodCRL,
}, nil
}
return &result.ServerResult{
Result: result.ResultOK,
Server: crlURL,
RevocationMethod: result.RevocationMethodCRL,
}, nil
}
type entryExtensions struct {
// invalidityDate is the date when the key is invalid.
invalidityDate time.Time
}
func parseEntryExtensions(entry *x509.RevocationListEntry) (entryExtensions, error) {
var extensions entryExtensions
for _, ext := range entry.Extensions {
switch {
case ext.Id.Equal(oidInvalidityDate):
var invalidityDate time.Time
rest, err := asn1.UnmarshalWithParams(ext.Value, &invalidityDate, "generalized")
if err != nil {
return entryExtensions{}, fmt.Errorf("failed to parse invalidity date: %w", err)
}
if len(rest) > 0 {
return entryExtensions{}, fmt.Errorf("invalid invalidity date extension: trailing data")
}
extensions.invalidityDate = invalidityDate
default:
if ext.Critical {
// unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2)
return entryExtensions{}, fmt.Errorf("unsupported critical extension found in CRL: %v", ext.Id)
}
}
}
return extensions, nil
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,254 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package 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"
"context"
"crypto"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/notaryproject/notation-core-go/revocation/result"
"golang.org/x/crypto/ocsp"
)
// CertCheckStatusOptions specifies values that are needed to check OCSP revocation
type CertCheckStatusOptions struct {
// HTTPClient is the HTTP client used to perform the OCSP request
HTTPClient *http.Client
// SigningTime is used to compare with the invalidity date during revocation
SigningTime time.Time
}
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
)
// CertCheckStatus checks the revocation status of a certificate using OCSP
func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts CertCheckStatusOptions) *result.CertRevocationResult {
if !Supported(cert) {
// OCSP not enabled for this certificate.
return &result.CertRevocationResult{
Result: result.ResultNonRevokable,
ServerResults: []*result.ServerResult{toServerResult("", NoServerError{})},
RevocationMethod: result.RevocationMethodOCSP,
}
}
ocspURLs := cert.OCSPServer
serverResults := make([]*result.ServerResult, len(ocspURLs))
for serverIndex, server := range ocspURLs {
serverResult := checkStatusFromServer(ctx, 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)
}
// Supported returns true if the certificate supports OCSP.
func Supported(cert *x509.Certificate) bool {
return cert != nil && len(cert.OCSPServer) > 0
}
func checkStatusFromServer(ctx context.Context, cert, issuer *x509.Certificate, server string, opts CertCheckStatusOptions) *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(ctx, 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(ctx context.Context, cert, issuer *x509.Certificate, server string, opts CertCheckStatusOptions) (*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}
}
var httpReq *http.Request
httpReq, err = http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return nil, err
}
resp, err = opts.HTTPClient.Do(httpReq)
} else {
resp, err = postRequest(ctx, ocspRequest, server, opts.HTTPClient)
}
} else {
resp, err = postRequest(ctx, ocspRequest, server, opts.HTTPClient)
}
if err != nil {
var urlErr *url.Error
if errors.As(err, &urlErr) && urlErr.Timeout() {
return nil, TimeoutError{
timeout: opts.HTTPClient.Timeout,
}
}
return nil, GenericError{Err: err}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
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(ctx context.Context, req []byte, server string, httpClient *http.Client) (*http.Response, error) {
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, server, bytes.NewReader(req))
if err != nil {
return nil, err
}
httpReq.Header.Set("Content-Type", "application/ocsp-request")
return httpClient.Do(httpReq)
}
func toServerResult(server string, err error) *result.ServerResult {
var serverResult *result.ServerResult
switch t := err.(type) {
case nil:
serverResult = result.NewServerResult(result.ResultOK, server, nil)
case NoServerError:
serverResult = result.NewServerResult(result.ResultNonRevokable, server, nil)
case RevokedError:
serverResult = result.NewServerResult(result.ResultRevoked, server, t)
default:
// Includes GenericError, UnknownStatusError, result.InvalidChainError,
// and TimeoutError
serverResult = result.NewServerResult(result.ResultUnknown, server, t)
}
serverResult.RevocationMethod = result.RevocationMethodOCSP
return serverResult
}
func serverResultsToCertRevocationResult(serverResults []*result.ServerResult) *result.CertRevocationResult {
return &result.CertRevocationResult{
Result: serverResults[len(serverResults)-1].Result,
ServerResults: serverResults,
RevocationMethod: result.RevocationMethodOCSP,
}
}

View File

@ -0,0 +1,296 @@
// 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 (
"context"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"strings"
"testing"
"time"
"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 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}
ctx := context.Background()
t.Run("check non-revoked cert", func(t *testing.T) {
client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
opts := CertCheckStatusOptions{
SigningTime: time.Now(),
HTTPClient: client,
}
certResult := CertCheckStatus(ctx, 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 := CertCheckStatusOptions{
SigningTime: time.Now(),
HTTPClient: client,
}
certResult := CertCheckStatus(ctx, 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 := CertCheckStatusOptions{
SigningTime: time.Now(),
HTTPClient: client,
}
certResult := CertCheckStatus(ctx, 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 := CertCheckStatusOptions{
SigningTime: time.Now(),
HTTPClient: client,
}
certResult := CertCheckStatus(ctx, revokableChain[0], revokableChain[1], opts)
expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)}
validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t)
})
t.Run("certificate doesn't support OCSP", func(t *testing.T) {
ocspResult := CertCheckStatus(ctx, &x509.Certificate{}, revokableIssuerTuple.Cert, CertCheckStatusOptions{})
expectedResult := &result.CertRevocationResult{
Result: result.ResultNonRevokable,
ServerResults: []*result.ServerResult{toServerResult("", NoServerError{})},
}
validateEquivalentCertResults([]*result.CertRevocationResult{ocspResult}, []*result.CertRevocationResult{expectedResult}, t)
})
}
func TestCheckStatusFromServer(t *testing.T) {
revokableCertTuple := testhelper.GetRevokableRSALeafCertificate()
revokableIssuerTuple := testhelper.GetRSARootCertificate()
ctx := context.Background()
t.Run("server url is not http", func(t *testing.T) {
server := "https://localhost.test"
serverResult := checkStatusFromServer(ctx, revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{})
expectedResult := toServerResult(server, GenericError{Err: fmt.Errorf("OCSPServer protocol %s is not supported", "https")})
if serverResult.Result != expectedResult.Result {
t.Errorf("Expected Result to be %s, but got %s", expectedResult.Result, serverResult.Result)
}
if serverResult.Server != expectedResult.Server {
t.Errorf("Expected Server to be %s, but got %s", expectedResult.Server, serverResult.Server)
}
if serverResult.Error == nil {
t.Errorf("Expected Error to be %v, but got nil", expectedResult.Error)
} else if serverResult.Error.Error() != expectedResult.Error.Error() {
t.Errorf("Expected Error to be %v, but got %v", expectedResult.Error, serverResult.Error)
}
})
t.Run("request error", func(t *testing.T) {
server := "http://localhost.test"
serverResult := checkStatusFromServer(ctx, revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{
HTTPClient: &http.Client{
Transport: &failedTransport{},
},
})
errorMessage := "failed to execute request"
if !strings.Contains(serverResult.Error.Error(), errorMessage) {
t.Errorf("Expected Error to contain %v, but got %v", errorMessage, serverResult.Error)
}
})
t.Run("ocsp expired", func(t *testing.T) {
client := testhelper.MockClient([]testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
server := "http://localhost.test/expired_ocsp"
serverResult := checkStatusFromServer(ctx, revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{
HTTPClient: client,
})
errorMessage := "expired OCSP response"
if !strings.Contains(serverResult.Error.Error(), errorMessage) {
t.Errorf("Expected Error to contain %v, but got %v", errorMessage, serverResult.Error)
}
})
t.Run("ocsp request roundtrip failed", func(t *testing.T) {
client := testhelper.MockClient([]testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
server := "http://localhost.test"
serverResult := checkStatusFromServer(nil, revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{
HTTPClient: client,
})
errorMessage := "net/http: nil Context"
if !strings.Contains(serverResult.Error.Error(), errorMessage) {
t.Errorf("Expected Error to contain %v, but got %v", errorMessage, serverResult.Error)
}
})
t.Run("ocsp request roundtrip timeout", func(t *testing.T) {
server := "http://localhost.test"
serverResult := checkStatusFromServer(ctx, revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{
HTTPClient: &http.Client{
Timeout: 1 * time.Second,
Transport: &failedTransport{
timeout: true,
},
},
})
errorMessage := "exceeded timeout threshold of 1.00 seconds for OCSP check"
if !strings.Contains(serverResult.Error.Error(), errorMessage) {
t.Errorf("Expected Error to contain %v, but got %v", errorMessage, serverResult.Error)
}
})
}
func TestPostRequest(t *testing.T) {
t.Run("failed to generate request", func(t *testing.T) {
_, err := postRequest(nil, nil, "http://localhost.test", &http.Client{
Transport: &failedTransport{},
})
expectedErrMsg := "net/http: nil Context"
if err == nil || err.Error() != expectedErrMsg {
t.Errorf("Expected error %s, but got %s", expectedErrMsg, err)
}
})
t.Run("failed to execute request", func(t *testing.T) {
_, err := postRequest(context.Background(), nil, "http://localhost.test", &http.Client{
Transport: &failedTransport{},
})
expectedErrMsg := "Post \"http://localhost.test\": failed to execute request"
if err == nil || err.Error() != expectedErrMsg {
t.Errorf("Expected error %s, but got %s", expectedErrMsg, err)
}
})
}
func TestExecuteOCSPCheck(t *testing.T) {
revokableCertTuple := testhelper.GetRevokableRSALeafCertificate()
revokableIssuerTuple := testhelper.GetRSARootCertificate()
ctx := context.Background()
t.Run("http response status is not 200", func(t *testing.T) {
_, err := executeOCSPCheck(ctx, revokableCertTuple.Cert, revokableIssuerTuple.Cert, "localhost.test", CertCheckStatusOptions{
HTTPClient: &http.Client{
Transport: &failedTransport{
statusCode: http.StatusNotFound,
},
},
})
expectedErrMsg := "failed to retrieve OCSP: response had status code 404"
if err == nil || err.Error() != expectedErrMsg {
t.Errorf("Expected error %s, but got %s", expectedErrMsg, err)
}
})
}
type testTimeoutError struct{}
func (e testTimeoutError) Error() string {
return "test timeout"
}
func (e testTimeoutError) Timeout() bool {
return true
}
type failedTransport struct {
timeout bool
statusCode int
}
func (f *failedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if f.timeout {
return nil, &url.Error{
Err: testTimeoutError{},
}
}
if f.statusCode != 0 {
return &http.Response{
StatusCode: f.statusCode,
}, nil
}
return nil, fmt.Errorf("failed to execute request")
}

View File

@ -0,0 +1,31 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package x509util
import (
"crypto/x509/pkix"
"encoding/asn1"
"slices"
)
// FindExtensionByOID finds the extension by the given OID.
func FindExtensionByOID(extensions []pkix.Extension, oid asn1.ObjectIdentifier) *pkix.Extension {
idx := slices.IndexFunc(extensions, func(ext pkix.Extension) bool {
return ext.Id.Equal(oid)
})
if idx < 0 {
return nil
}
return &extensions[idx]
}

View File

@ -0,0 +1,58 @@
// 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 x509util
import (
"crypto/x509/pkix"
"encoding/asn1"
"testing"
)
func TestFindExtensionByOID(t *testing.T) {
oid1 := asn1.ObjectIdentifier{1, 2, 3, 4}
oid2 := asn1.ObjectIdentifier{1, 2, 3, 5}
extensions := []pkix.Extension{
{Id: oid1, Value: []byte("value1")},
{Id: oid2, Value: []byte("value2")},
}
tests := []struct {
name string
oid asn1.ObjectIdentifier
extensions []pkix.Extension
expected *pkix.Extension
}{
{
name: "Extension found",
oid: oid1,
extensions: extensions,
expected: &extensions[0],
},
{
name: "Extension not found",
oid: asn1.ObjectIdentifier{1, 2, 3, 6},
extensions: extensions,
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FindExtensionByOID(tt.extensions, tt.oid)
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}

View File

@ -0,0 +1,48 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package x509util provides the method to validate the certificate chain for a
// specific purpose, including code signing and timestamping. It also provides
// the method to find the extension by the given OID.
package x509util
import (
"crypto/x509"
"fmt"
"github.com/notaryproject/notation-core-go/revocation/purpose"
"github.com/notaryproject/notation-core-go/revocation/result"
coreX509 "github.com/notaryproject/notation-core-go/x509"
)
// ValidateChain checks the certificate chain for a specific purpose, including
// code signing and timestamping.
func ValidateChain(certChain []*x509.Certificate, certChainPurpose purpose.Purpose) error {
switch 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(certChain, nil); err != nil {
return result.InvalidChainError{Err: err}
}
case purpose.Timestamping:
if err := coreX509.ValidateTimestampingCertChain(certChain); err != nil {
return result.InvalidChainError{Err: err}
}
default:
return result.InvalidChainError{Err: fmt.Errorf("unsupported certificate chain purpose %v", certChainPurpose)}
}
return nil
}

View File

@ -0,0 +1,60 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package x509util
import (
"crypto/x509"
"testing"
"github.com/notaryproject/notation-core-go/revocation/purpose"
"github.com/notaryproject/notation-core-go/testhelper"
)
func TestValidate(t *testing.T) {
t.Run("unsupported_certificate_chain_purpose", func(t *testing.T) {
certChain := []*x509.Certificate{}
certChainPurpose := purpose.Purpose(-1)
err := ValidateChain(certChain, certChainPurpose)
if err == nil {
t.Errorf("Validate() failed, expected error, got nil")
}
})
t.Run("invalid code signing certificate chain", func(t *testing.T) {
certChain := []*x509.Certificate{}
certChainPurpose := purpose.CodeSigning
err := ValidateChain(certChain, certChainPurpose)
if err == nil {
t.Errorf("Validate() failed, expected error, got nil")
}
})
t.Run("invalid timestamping certificate chain", func(t *testing.T) {
certChain := []*x509.Certificate{}
certChainPurpose := purpose.Timestamping
err := ValidateChain(certChain, certChainPurpose)
if err == nil {
t.Errorf("Validate() failed, expected error, got nil")
}
})
t.Run("valid code signing certificate chain", func(t *testing.T) {
certChain := testhelper.GetRevokableRSAChain(2)
certChainPurpose := purpose.CodeSigning
err := ValidateChain([]*x509.Certificate{certChain[0].Cert, certChain[1].Cert}, certChainPurpose)
if err != nil {
t.Errorf("Validate() failed, expected nil, got %v", err)
}
})
}

View File

@ -11,58 +11,27 @@
// 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"
import "github.com/notaryproject/notation-core-go/revocation/internal/ocsp"
type (
// RevokedError is returned when the certificate's status for OCSP is
// ocsp.Revoked
RevokedError = ocsp.RevokedError
// UnknownStatusError is returned when the certificate's status for OCSP is
// ocsp.Unknown
UnknownStatusError = ocsp.UnknownStatusError
// GenericError is returned when there is an error during the OCSP revocation
// check, not necessarily a revocation
GenericError = ocsp.GenericError
// NoServerError is returned when the OCSPServer is not specified.
NoServerError = ocsp.NoServerError
// TimeoutError is returned when the connection attempt to an OCSP URL exceeds
// the specified threshold
TimeoutError = ocsp.TimeoutError
)
// 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())
}

View File

@ -16,40 +16,30 @@
package ocsp
import (
"bytes"
"crypto"
"context"
"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/internal/ocsp"
"github.com/notaryproject/notation-core-go/revocation/internal/x509util"
"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
SigningTime time.Time
HTTPClient *http.Client
}
CertChain []*x509.Certificate
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
)
// 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
}
// CheckStatus checks OCSP based on the passed options and returns an array of
// result.CertRevocationResult objects that contains the results and error. The
@ -60,24 +50,25 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) {
return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")}
}
// Validate cert chain structure
// Since this 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}
if err := x509util.ValidateChain(opts.CertChain, opts.CertChainPurpose); err != nil {
return nil, err
}
certResults := make([]*result.CertRevocationResult, len(opts.CertChain))
certCheckStatusOptions := ocsp.CertCheckStatusOptions{
SigningTime: opts.SigningTime,
HTTPClient: opts.HTTPClient,
}
// Check status for each cert in cert chain
var wg sync.WaitGroup
ctx := context.Background()
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)
certResults[i] = ocsp.CertCheckStatus(ctx, cert, opts.CertChain[i+1], certCheckStatusOptions)
}(i, cert)
}
// Last is root cert, which will never be revoked by OCSP
@ -92,182 +83,3 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) {
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,
}
}

View File

@ -21,6 +21,7 @@ import (
"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"
@ -78,81 +79,14 @@ func getRootCertResult() *result.CertRevocationResult {
}
}
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,
CertChain: []*x509.Certificate{selfSignedTuple.Cert},
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -167,9 +101,10 @@ 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,
CertChain: []*x509.Certificate{rootTuple.Cert},
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -186,9 +121,10 @@ 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,
CertChain: []*x509.Certificate{certTuple.Cert},
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -212,9 +148,10 @@ func TestCheckStatusForChain(t *testing.T) {
t.Run("empty chain", func(t *testing.T) {
opts := Options{
CertChain: []*x509.Certificate{},
SigningTime: time.Now(),
HTTPClient: http.DefaultClient,
CertChain: []*x509.Certificate{},
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: http.DefaultClient,
}
certResults, err := CheckStatus(opts)
expectedErr := result.InvalidChainError{Err: errors.New("chain does not contain any certificates")}
@ -228,9 +165,10 @@ func TestCheckStatusForChain(t *testing.T) {
t.Run("check non-revoked chain", func(t *testing.T) {
client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
opts := Options{
CertChain: revokableChain,
SigningTime: time.Now(),
HTTPClient: client,
CertChain: revokableChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -251,9 +189,10 @@ func TestCheckStatusForChain(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,
CertChain: revokableChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -279,9 +218,10 @@ func TestCheckStatusForChain(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,
CertChain: revokableChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -307,9 +247,10 @@ func TestCheckStatusForChain(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,
CertChain: revokableChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -341,9 +282,10 @@ func TestCheckStatusForChain(t *testing.T) {
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,
CertChain: revokableChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -365,9 +307,10 @@ func TestCheckStatusForChain(t *testing.T) {
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,
CertChain: revokableChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -393,9 +336,10 @@ func TestCheckStatusForChain(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,
CertChain: revokableChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now().Add(time.Hour),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -422,9 +366,10 @@ func TestCheckStatusForChain(t *testing.T) {
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,
CertChain: revokableChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: zeroTime,
HTTPClient: client,
}
if !zeroTime.IsZero() {
@ -465,15 +410,16 @@ func TestCheckStatusErrors(t *testing.T) {
expiredLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw)
expiredLeaf.IsCA = false
expiredLeaf.KeyUsage = x509.KeyUsageDigitalSignature
expiredLeaf.OCSPServer = []string{"http://example.com/expired_ocsp"}
expiredLeaf.OCSPServer = []string{"http://localhost.test/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"}
noHTTPLeaf.OCSPServer = []string{"ldap://ds.localhost.test: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")}
@ -481,9 +427,10 @@ func TestCheckStatusErrors(t *testing.T) {
t.Run("no OCSPServer specified", func(t *testing.T) {
opts := Options{
CertChain: noOCSPChain,
SigningTime: time.Now(),
HTTPClient: http.DefaultClient,
CertChain: noOCSPChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: http.DefaultClient,
}
certResults, err := CheckStatus(opts)
if err != nil {
@ -503,9 +450,10 @@ func TestCheckStatusErrors(t *testing.T) {
t.Run("chain missing root", func(t *testing.T) {
opts := Options{
CertChain: noRootChain,
SigningTime: time.Now(),
HTTPClient: http.DefaultClient,
CertChain: noRootChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: http.DefaultClient,
}
certResults, err := CheckStatus(opts)
if err == nil || err.Error() != chainRootErr.Error() {
@ -518,9 +466,10 @@ func TestCheckStatusErrors(t *testing.T) {
t.Run("backwards chain", func(t *testing.T) {
opts := Options{
CertChain: backwardsChain,
SigningTime: time.Now(),
HTTPClient: http.DefaultClient,
CertChain: backwardsChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: http.DefaultClient,
}
certResults, err := CheckStatus(opts)
if err == nil || err.Error() != backwardsChainErr.Error() {
@ -531,12 +480,57 @@ func TestCheckStatusErrors(t *testing.T) {
}
})
t.Run("timeout", func(t *testing.T) {
timeoutClient := &http.Client{Timeout: 1 * time.Nanosecond}
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: timeoutClient,
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,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: timeoutClient,
}
certResults, err := CheckStatus(opts)
if err != nil {
@ -563,9 +557,10 @@ func TestCheckStatusErrors(t *testing.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,
CertChain: expiredChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
if err != nil {
@ -587,9 +582,10 @@ func TestCheckStatusErrors(t *testing.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,
CertChain: okChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
@ -607,9 +603,10 @@ func TestCheckStatusErrors(t *testing.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,
CertChain: noHTTPChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
if err != nil {
@ -636,7 +633,7 @@ func TestCheckOCSPInvalidChain(t *testing.T) {
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)
cert.OCSPServer[0] = fmt.Sprintf("http://localhost.test/chain_ocsp/%d", i)
}
}
@ -644,7 +641,7 @@ func TestCheckOCSPInvalidChain(t *testing.T) {
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)
cert.OCSPServer[0] = fmt.Sprintf("http://localhost.test/chain_ocsp/%d", i)
}
}
@ -654,9 +651,10 @@ func TestCheckOCSPInvalidChain(t *testing.T) {
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,
CertChain: missingIntermediateChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
if err == nil || err.Error() != missingIntermediateErr.Error() {
@ -670,9 +668,10 @@ func TestCheckOCSPInvalidChain(t *testing.T) {
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,
CertChain: misorderedIntermediateChain,
CertChainPurpose: purpose.CodeSigning,
SigningTime: time.Now(),
HTTPClient: client,
}
certResults, err := CheckStatus(opts)
if err == nil || err.Error() != misorderedChainErr.Error() {

View File

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

View File

@ -16,23 +16,27 @@ package result
import "strconv"
// Result is a type of enumerated value to help characterize errors. It can be
// OK, Unknown, or Revoked
// Result is a type of enumerated value to help characterize revocation result.
// It can be OK, Unknown, NonRevokable, 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
// 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 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 is a Result that indicates that the certificate cannot
// be checked for revocation. This may be due to the absence of OCSP servers
// or CRL distribution points, or because the certificate is a root
// certificate.
ResultNonRevokable
// ResultRevoked is a Result that indicates that at least one certificate was
// revoked when performing a revocation check on the certificate chain
// revoked when performing a revocation check on the certificate chain.
ResultRevoked
)
@ -52,8 +56,45 @@ func (r Result) String() string {
}
}
// ServerResult encapsulates the result for a single server for a single
// certificate in the chain
// RevocationMethod defines the method used to check the revocation status of a
// certificate.
type RevocationMethod int
const (
// RevocationMethodUnknown is used for root certificates or when the method
// used to check the revocation status of a certificate is unknown.
RevocationMethodUnknown RevocationMethod = iota
// RevocationMethodOCSP represents OCSP as the method used to check the
// revocation status of a certificate.
RevocationMethodOCSP
// RevocationMethodCRL represents CRL as the method used to check the
// revocation status of a certificate.
RevocationMethodCRL
// RevocationMethodOCSPFallbackCRL represents OCSP check with unknown error
// fallback to CRL as the method used to check the revocation status of a
// certificate.
RevocationMethodOCSPFallbackCRL
)
// String provides a conversion from a Method to a string
func (m RevocationMethod) String() string {
switch m {
case RevocationMethodOCSP:
return "OCSP"
case RevocationMethodCRL:
return "CRL"
case RevocationMethodOCSPFallbackCRL:
return "OCSPFallbackCRL"
default:
return "Unknown"
}
}
// ServerResult encapsulates the OCSP result for a single server or the CRL
// result for a single CRL URI for a 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)
@ -67,6 +108,11 @@ type ServerResult struct {
// Error is set if there is an error associated with the revocation check
// to this server
Error error
// RevocationMethod is the method used to check the revocation status of the
// certificate, including RevocationMethodUnknown, RevocationMethodOCSP,
// RevocationMethodCRL
RevocationMethod RevocationMethod
}
// NewServerResult creates a ServerResult object from its individual parts: a
@ -83,21 +129,31 @@ func NewServerResult(result Result, server string, err error) *ServerResult {
// 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 of revocation for a specific certificate in the chain.
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.
// ServerResults is an array of results for each server associated with the
// certificate.
//
// If the length is 1, then a valid status was able to be retrieved. Only
// When RevocationMethod is MethodOCSP, the length will be
// either 1 or the number of OCSPServers for the certificate.
// If the length is 1, then a valid status was 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
// status from being retrieved. These are all contained here for evaluation.
//
// When RevocationMethod is MethodCRL, the length will be the number of
// CRL distribution points' URIs checked. If the result is Revoked, or
// there is an error, the length will be 1.
//
// When RevocationMethod is MethodOCSPFallbackCRL, the length
// will be the sum of the previous two cases. The CRL result will be
// appended after the OCSP results.
ServerResults []*ServerResult
// RevocationMethod is the method used to check the revocation status of the
// certificate, including RevocationMethodUnknown, RevocationMethodOCSP,
// RevocationMethodCRL and RevocationMethodOCSPFallbackCRL
RevocationMethod RevocationMethod
}

View File

@ -46,6 +46,27 @@ func TestResultString(t *testing.T) {
})
}
func TestMethodString(t *testing.T) {
tests := []struct {
method RevocationMethod
expected string
}{
{RevocationMethodOCSP, "OCSP"},
{RevocationMethodCRL, "CRL"},
{RevocationMethodOCSPFallbackCRL, "OCSPFallbackCRL"},
{RevocationMethod(999), "Unknown"}, // Test for default case
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
result := tt.method.String()
if result != tt.expected {
t.Errorf("expected %s, got %s", tt.expected, result)
}
})
}
}
func TestNewServerResult(t *testing.T) {
expectedR := &ServerResult{
Result: ResultNonRevokable,

View File

@ -16,50 +16,256 @@
package revocation
import (
"context"
"crypto/x509"
"errors"
"fmt"
"net/http"
"sync"
"time"
"github.com/notaryproject/notation-core-go/revocation/ocsp"
crlutil "github.com/notaryproject/notation-core-go/revocation/crl"
"github.com/notaryproject/notation-core-go/revocation/internal/crl"
"github.com/notaryproject/notation-core-go/revocation/internal/ocsp"
"github.com/notaryproject/notation-core-go/revocation/internal/x509util"
"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
// 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 CRL if OCSP is not available. It 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
Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error)
ValidateContext(ctx context.Context, validateContextOpts ValidateContextOptions) ([]*result.CertRevocationResult, error)
}
// revocation is an internal struct used for revocation checking
type revocation struct {
httpClient *http.Client
ocspHTTPClient *http.Client
crlFetcher crlutil.Fetcher
certChainPurpose purpose.Purpose
}
// New constructs a revocation object
// 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")
}
fetcher, err := crlutil.NewHTTPFetcher(httpClient)
if err != nil {
return nil, err
}
return &revocation{
httpClient: httpClient,
ocspHTTPClient: httpClient,
crlFetcher: fetcher,
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
// CRLFetcher is a fetcher for CRL with cache. If not provided, a default
// fetcher with an HTTP client and a timeout of 5 seconds will be used
// without cache.
CRLFetcher crlutil.Fetcher
// 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}
}
fetcher := opts.CRLFetcher
if fetcher == nil {
newFetcher, err := crlutil.NewHTTPFetcher(&http.Client{Timeout: 5 * time.Second})
if err != nil {
return nil, err
}
fetcher = newFetcher
}
switch opts.CertChainPurpose {
case purpose.CodeSigning, purpose.Timestamping:
default:
return nil, fmt.Errorf("unsupported certificate chain purpose %v", opts.CertChainPurpose)
}
return &revocation{
ocspHTTPClient: opts.OCSPHTTPClient,
crlFetcher: fetcher,
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
// CRL if OCSP is not available. It 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
// This function tries OCSP and falls back to CRL when:
// - OCSP is not supported by the certificate
// - OCSP returns an unknown status
//
// NOTE: The certificate chain is expected to be in the order of leaf to root.
func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) {
return ocsp.CheckStatus(ocsp.Options{
CertChain: certChain,
SigningTime: signingTime,
HTTPClient: r.httpClient,
return r.ValidateContext(context.Background(), ValidateContextOptions{
CertChain: certChain,
AuthenticSigningTime: signingTime,
})
// TODO: add CRL support
// https://github.com/notaryproject/notation-core-go/issues/125
}
// ValidateContext checks the revocation status for a certificate chain using OCSP and
// CRL if OCSP is not available. It returns an array of CertRevocationResults
// that contain the results and any errors that are encountered during the
// process.
//
// This function tries OCSP and falls back to CRL when:
// - OCSP is not supported by the certificate
// - OCSP returns an unknown status
//
// NOTE: The certificate chain is expected to be in the order of leaf to root.
func (r *revocation) ValidateContext(ctx context.Context, validateContextOpts ValidateContextOptions) ([]*result.CertRevocationResult, error) {
// validate certificate chain
if len(validateContextOpts.CertChain) == 0 {
return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")}
}
certChain := validateContextOpts.CertChain
if err := x509util.ValidateChain(certChain, r.certChainPurpose); err != nil {
return nil, err
}
ocspOpts := ocsp.CertCheckStatusOptions{
HTTPClient: r.ocspHTTPClient,
SigningTime: validateContextOpts.AuthenticSigningTime,
}
crlOpts := crl.CertCheckStatusOptions{
Fetcher: r.crlFetcher,
SigningTime: validateContextOpts.AuthenticSigningTime,
}
// panicChain is used to store the panic in goroutine and handle it
panicChan := make(chan any, len(certChain))
defer close(panicChan)
certResults := make([]*result.CertRevocationResult, len(certChain))
var wg sync.WaitGroup
for i, cert := range certChain[:len(certChain)-1] {
switch {
case ocsp.Supported(cert):
// do OCSP check for the certificate
wg.Add(1)
go func(i int, cert *x509.Certificate) {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
// catch panic and send it to panicChan to avoid
// losing the panic
panicChan <- r
}
}()
ocspResult := ocsp.CertCheckStatus(ctx, cert, certChain[i+1], ocspOpts)
if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.Supported(cert) {
// try CRL check if OCSP serverResult is unknown
serverResult := crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts)
// append CRL result to OCSP result
serverResult.ServerResults = append(ocspResult.ServerResults, serverResult.ServerResults...)
serverResult.RevocationMethod = result.RevocationMethodOCSPFallbackCRL
certResults[i] = serverResult
} else {
certResults[i] = ocspResult
}
}(i, cert)
case crl.Supported(cert):
// do CRL check for the certificate
wg.Add(1)
go func(i int, cert *x509.Certificate) {
defer wg.Done()
defer func() {
if r := recover(); r != nil {
// catch panic and send it to panicChan to avoid
// losing the panic
panicChan <- r
}
}()
certResults[i] = crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts)
}(i, cert)
default:
certResults[i] = &result.CertRevocationResult{
Result: result.ResultNonRevokable,
ServerResults: []*result.ServerResult{{
Result: result.ResultNonRevokable,
RevocationMethod: result.RevocationMethodUnknown,
}},
RevocationMethod: result.RevocationMethodUnknown,
}
}
}
// Last is root cert, which will never be revoked by OCSP or CRL
certResults[len(certChain)-1] = &result.CertRevocationResult{
Result: result.ResultNonRevokable,
ServerResults: []*result.ServerResult{{
Result: result.ResultNonRevokable,
RevocationMethod: result.RevocationMethodUnknown,
}},
RevocationMethod: result.RevocationMethodUnknown,
}
wg.Wait()
// handle panic
select {
case p := <-panicChan:
panic(p)
default:
}
return certResults, nil
}

View File

@ -14,14 +14,23 @@
package revocation
import (
"bytes"
"context"
"crypto/rand"
"crypto/x509"
"errors"
"fmt"
"io"
"math/big"
"net/http"
"strconv"
"strings"
"testing"
"time"
revocationocsp "github.com/notaryproject/notation-core-go/revocation/ocsp"
"github.com/notaryproject/notation-core-go/revocation/crl"
revocationocsp "github.com/notaryproject/notation-core-go/revocation/internal/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"
@ -58,6 +67,9 @@ func validateEquivalentCertResults(certResults, expectedCertResults []*result.Ce
t.Errorf("Expected certResults[%d].ServerResults[%d].Error to be %v, but got %v", i, j, expectedCertResults[i].ServerResults[j].Error, serverResult.Error)
}
}
if certResult.RevocationMethod != expectedCertResults[i].RevocationMethod {
t.Errorf("Expected certResults[%d].RevocationMethod to be %d, but got %d", i, expectedCertResults[i].RevocationMethod, certResult.RevocationMethod)
}
}
}
@ -67,6 +79,7 @@ func getOKCertResult(server string) *result.CertRevocationResult {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultOK, server, nil),
},
RevocationMethod: result.RevocationMethodOCSP,
}
}
@ -94,11 +107,34 @@ func TestNew(t *testing.T) {
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)
} else if revR.ocspHTTPClient != client {
t.Errorf("Expected New to set client to %v, but it was set to %v", client, revR.ocspHTTPClient)
}
}
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()
@ -136,6 +172,7 @@ func TestCheckRevocationStatusForSingleCert(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultUnknown, revokableChain[0].OCSPServer[0], revocationocsp.UnknownStatusError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getRootCertResult(),
}
@ -158,6 +195,7 @@ func TestCheckRevocationStatusForSingleCert(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultRevoked, revokableChain[0].OCSPServer[0], revocationocsp.RevokedError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getRootCertResult(),
}
@ -280,6 +318,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getOKCertResult(revokableChain[3].OCSPServer[0]),
getOKCertResult(revokableChain[4].OCSPServer[0]),
@ -307,6 +346,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getOKCertResult(revokableChain[3].OCSPServer[0]),
getOKCertResult(revokableChain[4].OCSPServer[0]),
@ -334,6 +374,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getOKCertResult(revokableChain[3].OCSPServer[0]),
{
@ -341,6 +382,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], revocationocsp.RevokedError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getRootCertResult(),
}
@ -390,6 +432,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getOKCertResult(revokableChain[3].OCSPServer[0]),
getOKCertResult(revokableChain[4].OCSPServer[0]),
@ -417,6 +460,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getOKCertResult(revokableChain[3].OCSPServer[0]),
getOKCertResult(revokableChain[4].OCSPServer[0]),
@ -450,6 +494,313 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
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("invalid revocation purpose", func(t *testing.T) {
revocationClient := &revocation{
ocspHTTPClient: &http.Client{Timeout: 5 * time.Second},
certChainPurpose: -1,
}
_, err := revocationClient.Validate(revokableChain, time.Now())
if err == nil {
t.Error("Expected Validate to fail with an error, but it succeeded")
}
})
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{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
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{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
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{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getOKCertResult(revokableChain[3].OCSPServer[0]),
{
Result: result.ResultRevoked,
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], revocationocsp.RevokedError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
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{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
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{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
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{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getOKCertResult(revokableChain[3].OCSPServer[0]),
getOKCertResult(revokableChain[4].OCSPServer[0]),
@ -472,13 +823,13 @@ func TestCheckRevocationErrors(t *testing.T) {
expiredLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw)
expiredLeaf.IsCA = false
expiredLeaf.KeyUsage = x509.KeyUsageDigitalSignature
expiredLeaf.OCSPServer = []string{"http://example.com/expired_ocsp"}
expiredLeaf.OCSPServer = []string{"http://localhost.test/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"}
noHTTPLeaf.OCSPServer = []string{"ldap://ds.localhost.test: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")}
@ -544,12 +895,14 @@ func TestCheckRevocationErrors(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultUnknown, okChain[0].OCSPServer[0], revocationocsp.TimeoutError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
{
Result: result.ResultUnknown,
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultUnknown, okChain[1].OCSPServer[0], revocationocsp.TimeoutError{}),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getRootCertResult(),
}
@ -572,6 +925,7 @@ func TestCheckRevocationErrors(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultUnknown, expiredChain[0].OCSPServer[0], expiredRespErr),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getOKCertResult(expiredChain[1].OCSPServer[0]),
getRootCertResult(),
@ -614,6 +968,7 @@ func TestCheckRevocationErrors(t *testing.T) {
ServerResults: []*result.ServerResult{
result.NewServerResult(result.ResultUnknown, noHTTPChain[0].OCSPServer[0], noHTTPErr),
},
RevocationMethod: result.RevocationMethodOCSP,
},
getOKCertResult(noHTTPChain[1].OCSPServer[0]),
getRootCertResult(),
@ -629,7 +984,7 @@ func TestCheckRevocationInvalidChain(t *testing.T) {
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)
cert.OCSPServer[0] = fmt.Sprintf("http://localhost.test/chain_ocsp/%d", i)
}
}
@ -637,7 +992,7 @@ func TestCheckRevocationInvalidChain(t *testing.T) {
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)
cert.OCSPServer[0] = fmt.Sprintf("http://localhost.test/chain_ocsp/%d", i)
}
}
@ -676,3 +1031,349 @@ func TestCheckRevocationInvalidChain(t *testing.T) {
}
})
}
func TestCRL(t *testing.T) {
t.Run("CRL check valid", func(t *testing.T) {
chain := testhelper.GetRevokableRSAChainWithRevocations(3, false, true)
fetcher, err := crl.NewHTTPFetcher(&http.Client{
Timeout: 5 * time.Second,
Transport: &crlRoundTripper{
CertChain: chain,
Revoked: false,
},
})
if err != nil {
t.Errorf("Expected successful creation of fetcher, but received error: %v", err)
}
revocationClient, err := NewWithOptions(Options{
OCSPHTTPClient: &http.Client{},
CRLFetcher: fetcher,
CertChainPurpose: purpose.CodeSigning,
})
if err != nil {
t.Errorf("Expected successful creation of revocation, but received error: %v", err)
}
certResults, err := revocationClient.ValidateContext(context.Background(), ValidateContextOptions{
CertChain: []*x509.Certificate{chain[0].Cert, chain[1].Cert, chain[2].Cert},
AuthenticSigningTime: time.Now(),
})
if err != nil {
t.Errorf("Expected CheckStatus to succeed, but got error: %v", err)
}
expectedCertResults := []*result.CertRevocationResult{
{
Result: result.ResultOK,
ServerResults: []*result.ServerResult{{
Result: result.ResultOK,
Server: "http://localhost.test/chain_crl/0",
}},
RevocationMethod: result.RevocationMethodCRL,
},
{
Result: result.ResultOK,
ServerResults: []*result.ServerResult{{
Result: result.ResultOK,
Server: "http://localhost.test/chain_crl/1",
}},
RevocationMethod: result.RevocationMethodCRL,
},
getRootCertResult(),
}
validateEquivalentCertResults(certResults, expectedCertResults, t)
})
t.Run("CRL check with revoked status", func(t *testing.T) {
chain := testhelper.GetRevokableRSAChainWithRevocations(3, false, true)
fetcher, err := crl.NewHTTPFetcher(&http.Client{
Timeout: 5 * time.Second,
Transport: &crlRoundTripper{
CertChain: chain,
Revoked: true,
},
})
if err != nil {
t.Errorf("Expected successful creation of fetcher, but received error: %v", err)
}
revocationClient, err := NewWithOptions(Options{
OCSPHTTPClient: &http.Client{},
CRLFetcher: fetcher,
CertChainPurpose: purpose.CodeSigning,
})
if err != nil {
t.Errorf("Expected successful creation of revocation, but received error: %v", err)
}
certResults, err := revocationClient.ValidateContext(context.Background(), ValidateContextOptions{
CertChain: []*x509.Certificate{
chain[0].Cert, // leaf
chain[1].Cert, // intermediate
chain[2].Cert, // root
},
AuthenticSigningTime: 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: result.ResultRevoked,
Server: "http://localhost.test/chain_crl/0",
},
},
RevocationMethod: result.RevocationMethodCRL,
},
{
Result: result.ResultRevoked,
ServerResults: []*result.ServerResult{
{
Result: result.ResultRevoked,
Server: "http://localhost.test/chain_crl/1",
},
},
RevocationMethod: result.RevocationMethodCRL,
},
getRootCertResult(),
}
validateEquivalentCertResults(certResults, expectedCertResults, t)
})
t.Run("OCSP fallback to CRL", func(t *testing.T) {
chain := testhelper.GetRevokableRSAChainWithRevocations(3, true, true)
fetcher, err := crl.NewHTTPFetcher(&http.Client{
Timeout: 5 * time.Second,
Transport: &crlRoundTripper{
CertChain: chain,
Revoked: true,
FailOCSP: true,
},
})
if err != nil {
t.Errorf("Expected successful creation of fetcher, but received error: %v", err)
}
revocationClient, err := NewWithOptions(Options{
OCSPHTTPClient: &http.Client{
Transport: &serverErrorTransport{},
},
CRLFetcher: fetcher,
CertChainPurpose: purpose.CodeSigning,
})
if err != nil {
t.Errorf("Expected successful creation of revocation, but received error: %v", err)
}
certResults, err := revocationClient.ValidateContext(context.Background(), ValidateContextOptions{
CertChain: []*x509.Certificate{
chain[0].Cert, // leaf
chain[1].Cert, // intermediate
chain[2].Cert, // root
},
AuthenticSigningTime: 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: result.ResultUnknown,
Server: "http://localhost.test/chain_ocsp/0",
Error: errors.New("failed to retrieve OCSP: response had status code 500"),
RevocationMethod: result.RevocationMethodOCSP,
},
{
Result: result.ResultRevoked,
Server: "http://localhost.test/chain_crl/0",
RevocationMethod: result.RevocationMethodCRL,
},
},
RevocationMethod: result.RevocationMethodOCSPFallbackCRL,
},
{
Result: result.ResultRevoked,
ServerResults: []*result.ServerResult{
{
Result: result.ResultUnknown,
Server: "http://localhost.test/chain_ocsp/1",
Error: errors.New("failed to retrieve OCSP: response had status code 500"),
RevocationMethod: result.RevocationMethodOCSPFallbackCRL,
},
{
Result: result.ResultRevoked,
Server: "http://localhost.test/chain_crl/1",
RevocationMethod: result.RevocationMethodCRL,
},
},
RevocationMethod: result.RevocationMethodOCSPFallbackCRL,
},
getRootCertResult(),
}
validateEquivalentCertResults(certResults, expectedCertResults, t)
})
}
func TestPanicHandling(t *testing.T) {
t.Run("panic in OCSP", func(t *testing.T) {
chain := testhelper.GetRevokableRSAChainWithRevocations(2, true, false)
client := &http.Client{
Transport: panicTransport{},
}
fetcher, err := crl.NewHTTPFetcher(client)
if err != nil {
t.Errorf("Expected successful creation of fetcher, but received error: %v", err)
}
r, err := NewWithOptions(Options{
OCSPHTTPClient: client,
CRLFetcher: fetcher,
CertChainPurpose: purpose.CodeSigning,
})
if err != nil {
t.Errorf("Expected successful creation of revocation, but received error: %v", err)
}
defer func() {
if r := recover(); r == nil {
t.Error("Expected panic, but got nil")
}
}()
_, _ = r.ValidateContext(context.Background(), ValidateContextOptions{
CertChain: []*x509.Certificate{chain[0].Cert, chain[1].Cert},
AuthenticSigningTime: time.Now(),
})
})
t.Run("panic in CRL", func(t *testing.T) {
chain := testhelper.GetRevokableRSAChainWithRevocations(2, false, true)
client := &http.Client{
Transport: panicTransport{},
}
fetcher, err := crl.NewHTTPFetcher(client)
if err != nil {
t.Errorf("Expected successful creation of fetcher, but received error: %v", err)
}
r, err := NewWithOptions(Options{
OCSPHTTPClient: client,
CRLFetcher: fetcher,
CertChainPurpose: purpose.CodeSigning,
})
if err != nil {
t.Errorf("Expected successful creation of revocation, but received error: %v", err)
}
defer func() {
if r := recover(); r == nil {
t.Error("Expected panic, but got nil")
}
}()
_, _ = r.ValidateContext(context.Background(), ValidateContextOptions{
CertChain: []*x509.Certificate{chain[0].Cert, chain[1].Cert},
AuthenticSigningTime: time.Now(),
})
})
}
type crlRoundTripper struct {
CertChain []testhelper.RSACertTuple
Revoked bool
FailOCSP bool
}
func (rt *crlRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// e.g. ocsp URL: http://localhost.test/chain_ocsp/0
// e.g. crl URL: http://localhost.test/chain_crl/0
parts := strings.Split(req.URL.Path, "/")
isOCSP := parts[len(parts)-2] == "chain_ocsp"
// fail OCSP
if rt.FailOCSP && isOCSP {
return nil, errors.New("OCSP failed")
}
// choose the cert suffix based on suffix of request url
// e.g. http://localhost.test/chain_crl/0 -> 0
i, err := strconv.Atoi(parts[len(parts)-1])
if err != nil {
return nil, err
}
if i >= len(rt.CertChain) {
return nil, errors.New("invalid index")
}
cert := rt.CertChain[i].Cert
crl := &x509.RevocationList{
NextUpdate: time.Now().Add(time.Hour),
Number: big.NewInt(20240720),
}
if rt.Revoked {
crl.RevokedCertificateEntries = []x509.RevocationListEntry{
{
SerialNumber: cert.SerialNumber,
RevocationTime: time.Now().Add(-time.Hour),
},
}
}
issuerCert := rt.CertChain[i+1].Cert
issuerKey := rt.CertChain[i+1].PrivateKey
crlBytes, err := x509.CreateRevocationList(rand.Reader, crl, issuerCert, issuerKey)
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(crlBytes)),
}, nil
}
type panicTransport struct{}
func (t panicTransport) RoundTrip(req *http.Request) (*http.Response, error) {
panic("panic")
}
type serverErrorTransport struct{}
func (t serverErrorTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusInternalServerError,
Body: io.NopCloser(bytes.NewReader([]byte{})),
}, nil
}
func TestValidateContext(t *testing.T) {
r, err := NewWithOptions(Options{
OCSPHTTPClient: &http.Client{},
CertChainPurpose: purpose.CodeSigning,
})
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)
}
}

View File

@ -14,112 +14,44 @@
package signature
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"fmt"
"github.com/notaryproject/notation-core-go/internal/algorithm"
)
// Algorithm defines the signature algorithm.
type Algorithm int
type Algorithm = algorithm.Algorithm
// 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
AlgorithmPS256 = algorithm.AlgorithmPS256 // RSASSA-PSS with SHA-256
AlgorithmPS384 = algorithm.AlgorithmPS384 // RSASSA-PSS with SHA-384
AlgorithmPS512 = algorithm.AlgorithmPS512 // RSASSA-PSS with SHA-512
AlgorithmES256 = algorithm.AlgorithmES256 // ECDSA on secp256r1 with SHA-256
AlgorithmES384 = algorithm.AlgorithmES384 // ECDSA on secp384r1 with SHA-384
AlgorithmES512 = algorithm.AlgorithmES512 // ECDSA on secp521r1 with SHA-512
)
// KeyType defines the key type.
type KeyType int
type KeyType = algorithm.KeyType
const (
KeyTypeRSA KeyType = 1 + iota // KeyType RSA
KeyTypeEC // KeyType EC
KeyTypeRSA = algorithm.KeyTypeRSA // KeyType RSA
KeyTypeEC = algorithm.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
}
type KeySpec = algorithm.KeySpec
// 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),
}
ks, err := algorithm.ExtractKeySpec(signingCert)
if err != nil {
return KeySpec{}, &UnsupportedSigningKeyError{
Msg: err.Error(),
}
}
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
return ks, nil
}

View File

@ -14,7 +14,6 @@
package signature
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
@ -28,59 +27,6 @@ import (
"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
@ -168,77 +114,3 @@ func TestExtractKeySpec(t *testing.T) {
})
}
}
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)
}
})
}
}

View File

@ -57,7 +57,7 @@ func TestConformance(t *testing.T) {
// testSign does conformance check on COSE_Sign1_Tagged
func testSign(t *testing.T, sign1 *sign1) {
signRequest, err := getSignReq(sign1)
signRequest, err := getSignReq()
if err != nil {
t.Fatalf("getSignReq() failed. Error = %s", err)
}
@ -90,7 +90,7 @@ func testSign(t *testing.T, sign1 *sign1) {
// testVerify does conformance check by decoding COSE_Sign1_Tagged object
// into Sign1Message
func testVerify(t *testing.T, sign1 *sign1) {
signRequest, err := getSignReq(sign1)
signRequest, err := getSignReq()
if err != nil {
t.Fatalf("getSignReq() failed. Error = %s", err)
}
@ -124,7 +124,7 @@ func testVerify(t *testing.T, sign1 *sign1) {
verifySignerInfo(&content.SignerInfo, signRequest, t)
}
func getSignReq(sign1 *sign1) (*signature.SignRequest, error) {
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 {

View File

@ -24,8 +24,10 @@ import (
"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"
)
@ -76,7 +78,7 @@ const (
// Unprotected Headers
// https://github.com/notaryproject/notaryproject/blob/cose-envelope/signature-envelope-cose.md
const (
headerLabelTimeStampSignature = "io.cncf.notary.timestampSignature"
headerLabelTimestampSignature = "io.cncf.notary.timestampSignature"
headerLabelSigningAgent = "io.cncf.notary.signingAgent"
)
@ -234,10 +236,26 @@ func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) {
return nil, &signature.InvalidSignRequestError{Msg: err.Error()}
}
// generate unprotected headers of COSE envelope
// generate unprotected headers of COSE envelope.
generateUnprotectedHeaders(req, signer, msg.Headers.Unprotected)
// TODO: needs to add headerKeyTimeStampSignature.
// 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()
@ -368,7 +386,10 @@ func (e *envelope) signerInfo() (*signature.SignerInfo, error) {
signerInfo.UnsignedAttributes.SigningAgent = h
}
// TODO: needs to add headerKeyTimeStampSignature.
// populate signerInfo.UnsignedAttributes.TimestampSignature
if timestamepToken, ok := e.base.Headers.Unprotected[headerLabelTimestampSignature].([]byte); ok {
signerInfo.UnsignedAttributes.TimestampSignature = timestamepToken
}
return &signerInfo, nil
}
@ -701,3 +722,17 @@ func generateRawProtectedCBORMap(rawProtected cbor.RawMessage) (map[any]cbor.Raw
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)
}
}

View File

@ -14,6 +14,7 @@
package cose
import (
"context"
"crypto"
"crypto/x509"
"errors"
@ -25,11 +26,15 @@ import (
"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://timestamp.digicert.com"
)
var (
@ -123,6 +128,49 @@ func TestSign(t *testing.T) {
}
}
}
t.Run("with timestamp 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.cer")
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) {
@ -288,6 +336,26 @@ func TestSignErrors(t *testing.T) {
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 = &dummyTimestamper{}
expected := errors.New("timestamp: failed to timestamp")
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, &timestampErr) {
t.Fatal("expected signature.TimestampError")
}
if encoded != nil {
t.Fatal("expected nil signature envelope")
}
})
}
func TestVerifyErrors(t *testing.T) {
@ -801,6 +869,44 @@ func TestGenerateExtendedAttributesError(t *testing.T) {
}
}
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 {
@ -993,3 +1099,9 @@ func generateTestRawMessage(raw cbor.RawMessage, label string, unmarshalError bo
return resRaw
}
type dummyTimestamper tspclient.Timestamp
func (dts *dummyTimestamper) Timestamp(context.Context, *tspclient.Request) (*tspclient.Response, error) {
return nil, errors.New("failed to timestamp")
}

View File

@ -0,0 +1,33 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cose
import (
"testing"
)
func FuzzSignatureCose(f *testing.F) {
f.Fuzz(func(t *testing.T, envelopeBytes []byte, shouldVerify bool) {
e, err := ParseEnvelope(envelopeBytes)
if err != nil {
t.Skip()
}
if shouldVerify {
_, _ = e.Verify()
} else {
_, _ = e.Content()
}
})
}

View File

@ -67,7 +67,7 @@ type SignatureAuthenticityError struct{}
// Error returns the default error message.
func (e *SignatureAuthenticityError) Error() string {
return "signature is not produced by a trusted signer"
return "the signature's certificate chain does not contain any trusted certificate"
}
// UnsupportedSigningKeyError is used when a signing key is not supported.
@ -142,3 +142,28 @@ type DuplicateKeyError struct {
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
}

View File

@ -162,7 +162,7 @@ func TestSignatureEnvelopeNotFoundError(t *testing.T) {
func TestSignatureAuthenticityError(t *testing.T) {
err := &SignatureAuthenticityError{}
expectMsg := "signature is not produced by a trusted signer"
expectMsg := "the signature's certificate chain does not contain any trusted certificate"
if err.Error() != expectMsg {
t.Errorf("Expected %v but got %v", expectMsg, err.Error())
@ -177,3 +177,34 @@ func TestEnvelopeKeyRepeatedError(t *testing.T) {
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())
}
}

View File

@ -53,7 +53,6 @@ func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) {
if err != nil {
return nil, err
}
if err := validateCertificateChain(
content.SignerInfo.CertificateChain,
&content.SignerInfo.SignedAttributes.SigningTime,
@ -62,7 +61,9 @@ func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) {
return nil, err
}
// store the raw signature
e.Raw = raw
return e.Raw, nil
}

View File

@ -22,10 +22,12 @@ import (
"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"
@ -35,13 +37,13 @@ var (
time08_02 time.Time
time08_03 time.Time
timeLayout = "2006-01-02"
signiningSchema = signature.SigningScheme("notary.x509")
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,
SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore,
Expiry: testhelper.GetECLeafCertificate().Cert.NotAfter,
SigningScheme: signiningSchema,
},
CertificateChain: []*x509.Certificate{
@ -62,8 +64,8 @@ var (
ContentType: validContentType,
Content: validBytes,
},
SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore,
Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter,
SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore,
Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter,
SigningScheme: signiningSchema,
Signer: &mockSigner{
keySpec: signature.KeySpec{
@ -82,8 +84,8 @@ var (
ContentType: validContentType,
Content: validBytes,
},
SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore,
Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter,
SigningTime: testhelper.GetRSALeafCertificate().Cert.NotBefore,
Expiry: testhelper.GetRSALeafCertificate().Cert.NotAfter,
SigningScheme: signiningSchema,
Signer: &mockSigner{
keySpec: signature.KeySpec{
@ -97,19 +99,42 @@ var (
},
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
failVerify bool
payload *signature.Payload
signerInfo *signature.SignerInfo
content *signature.EnvelopeContent
failTimestamp bool
failVerify bool
}
// Sign implements Sign of signature.Envelope.
@ -118,6 +143,9 @@ func (e mockEnvelope) Sign(req *signature.SignRequest) ([]byte, error) {
case invalidSigningAgent:
return nil, errors.New(errMsg)
case validSigningAgent:
if e.failTimestamp {
return validBytes, &signature.TimestampError{}
}
return validBytes, nil
}
return nil, nil
@ -234,6 +262,21 @@ func TestSign(t *testing.T) {
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 {
@ -246,6 +289,17 @@ func TestSign(t *testing.T) {
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, &timestampErr) {
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)
}
}
})
}
}
@ -456,8 +510,8 @@ func TestValidateSignRequest(t *testing.T) {
ContentType: validContentType,
Content: validBytes,
},
SigningTime: time08_02,
Expiry: time08_03,
SigningTime: time08_02,
Expiry: time08_03,
SigningScheme: signiningSchema,
Signer: &mockSigner{
certs: []*x509.Certificate{

View File

@ -20,8 +20,10 @@ import (
"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.
@ -91,11 +93,17 @@ func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) {
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
}
@ -220,3 +228,34 @@ func sign(payload jwt.MapClaims, headers map[string]interface{}, method signingM
}
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
}

View File

@ -14,6 +14,7 @@
package jws
import (
"context"
"crypto"
"crypto/ecdsa"
"crypto/rand"
@ -32,8 +33,12 @@ import (
"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://timestamp.digicert.com"
// remoteMockSigner is used to mock remote signer
type remoteMockSigner struct {
privateKey crypto.PrivateKey
@ -226,8 +231,8 @@ func TestNewEnvelope(t *testing.T) {
}
}
// Test the same key exists both in extended signed attributes and protected header
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())
@ -253,6 +258,29 @@ func TestSignFailed(t *testing.T) {
_, 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 = &dummyTimestamper{}
expected := errors.New("timestamp: failed to timestamp")
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, &timestampErr) {
t.Fatal("expected signature.TimestampError")
}
if encoded != nil {
t.Fatal("expected nil signature envelope")
}
})
}
func TestSigningScheme(t *testing.T) {
@ -302,6 +330,52 @@ func TestSignVerify(t *testing.T) {
}
}
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.cer")
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)
@ -601,3 +675,19 @@ func TestEmptyEnvelope(t *testing.T) {
}
})
}
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
}
type dummyTimestamper tspclient.Timestamp
func (dts *dummyTimestamper) Timestamp(context.Context, *tspclient.Request) (*tspclient.Response, error) {
return nil, errors.New("failed to timestamp")
}

View File

@ -0,0 +1,33 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package jws
import (
"testing"
)
func FuzzSignatureJws(f *testing.F) {
f.Fuzz(func(t *testing.T, envelopeBytes []byte, shouldVerify bool) {
e, err := ParseEnvelope(envelopeBytes)
if err != nil {
t.Skip()
}
if shouldVerify {
_, _ = e.Verify()
} else {
_, _ = e.Content()
}
})
}

View File

@ -73,7 +73,7 @@ type jwsProtectedHeader struct {
// jwsUnprotectedHeader contains the set of unprotected headers.
type jwsUnprotectedHeader struct {
// RFC3161 time stamp token Base64-encoded.
// RFC3161 timestamp token Base64-encoded.
TimestampSignature []byte `json:"io.cncf.notary.timestampSignature,omitempty"`
// List of X.509 Base64-DER-encoded certificates

View File

@ -122,22 +122,21 @@ 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.
// VerifyAuthenticity iterates the certificate chain in signerInfo, for each
// certificate in the chain, it checks if the certificate matches with one of
// the trusted certificates in trustedCerts. It returns the first matching
// certificate. If no match is found, it returns an error.
//
// 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 {
for _, cert := range signerInfo.CertificateChain {
for _, trust := range trustedCerts {
if trust.Equal(cert) {
return trust, nil
}

View File

@ -14,9 +14,14 @@
package signature
import (
"context"
"crypto/x509"
"errors"
"fmt"
"time"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/tspclient-go"
)
// SignatureMediaType list the supported media-type for signatures.
@ -101,6 +106,46 @@ type SignRequest struct {
// 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
// TSARevocationValidator is used for timestamping certificate
// chain revocation check after signing.
// When present, only used when timestamping is performed.
TSARevocationValidator revocation.Validator
// 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
@ -159,17 +204,21 @@ func (signerInfo *SignerInfo) ExtendedAttribute(key string) (Attribute, error) {
return Attribute{}, errors.New("key not in ExtendedAttributes")
}
// AuthenticSigningTime returns the authentic signing time
// 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 signerInfo.SignedAttributes.SigningScheme {
switch signingScheme := signerInfo.SignedAttributes.SigningScheme; signingScheme {
case SigningSchemeX509SigningAuthority:
return signerInfo.SignedAttributes.SigningTime, nil
case SigningSchemeX509:
if len(signerInfo.UnsignedAttributes.TimestampSignature) > 0 {
// TODO: Add TSA support for AutheticSigningTime
// https://github.com/notaryproject/notation-core-go/issues/38
return time.Time{}, errors.New("TSA checking has not been implemented")
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)
}
return time.Time{}, errors.New("authenticSigningTime not found")
}

93
signature/types_test.go Normal file
View File

@ -0,0 +1,93 @@
// 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)
}
}

View File

@ -22,12 +22,15 @@ import (
"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 (
@ -72,16 +75,46 @@ func GetRevokableRSALeafCertificate() RSACertTuple {
return revokableRSALeaf
}
// GetRevokableRSAChainWithRevocations returns a certificate chain with OCSP
// and CRL enabled for revocation checks.
func GetRevokableRSAChainWithRevocations(size int, enabledOCSP, enabledCRL bool) []RSACertTuple {
setupCertificates()
chain := make([]RSACertTuple, size)
chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1, enabledCRL)
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, enabledOCSP, enabledCRL)
}
if size > 1 {
chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false, enabledOCSP, enabledCRL)
}
return chain
}
// 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)
chain[size-1] = getRevokableRSARootChainCertTuple("Notation Test Revokable RSA Chain Cert Root", size-1, false)
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)
chain[i] = getRevokableRSAChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size-i), &chain[i+1], i, true, false)
}
if size > 1 {
chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0)
chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, true, false, 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, false)
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, true, false)
}
if size > 1 {
chain[0] = getRevokableRSALeafChainCertTuple(fmt.Sprintf("Notation Test Revokable RSA Chain Cert %d", size), &chain[1], 0, false, true, true, false)
}
return chain
}
@ -148,42 +181,56 @@ func getRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple {
}
func getRevokableRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple {
template := getCertTemplate(issuer == nil, true, cn)
template.OCSPServer = []string{"http://example.com/ocsp"}
template := getCertTemplate(issuer == nil, true, false, cn)
template.OCSPServer = []string{"http://localhost.test/ocsp"}
return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer)
}
func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int) RSACertTuple {
template := getCertTemplate(previous == nil, true, cn)
func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int, enabledOCSP, enabledCRL bool) 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)}
if enabledOCSP {
template.OCSPServer = []string{fmt.Sprintf("http://localhost.test/chain_ocsp/%d", index)}
}
if enabledCRL {
template.KeyUsage |= x509.KeyUsageCRLSign
template.CRLDistributionPoints = []string{fmt.Sprintf("http://localhost.test/chain_crl/%d", index)}
}
return getRSACertTupleWithTemplate(template, previous.PrivateKey, previous)
}
func getRevokableRSARootChainCertTuple(cn string, pathLen int) RSACertTuple {
func getRevokableRSARootChainCertTuple(cn string, pathLen int, enabledCRL bool) RSACertTuple {
pk, _ := rsa.GenerateKey(rand.Reader, 3072)
template := getCertTemplate(true, true, cn)
template := getCertTemplate(true, true, false, cn)
template.BasicConstraintsValid = true
template.IsCA = true
template.KeyUsage = x509.KeyUsageCertSign
if enabledCRL {
template.KeyUsage |= x509.KeyUsageCRLSign
}
template.MaxPathLen = pathLen
return getRSACertTupleWithTemplate(template, pk, nil)
}
func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int) RSACertTuple {
template := getCertTemplate(false, true, cn)
func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp, enabledOCSP, enabledCRL 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)}
if enabledOCSP {
template.OCSPServer = []string{fmt.Sprintf("http://localhost.test/chain_ocsp/%d", index)}
}
if enabledCRL {
template.CRLDistributionPoints = []string{fmt.Sprintf("http://localhost.test/chain_crl/%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, cn)
template := getCertTemplate(issuer == nil, false, false, cn)
return getRSACertTupleWithTemplate(template, pk, issuer)
}
@ -200,18 +247,18 @@ func getECCertTuple(cn string, issuer *ECCertTuple) ECCertTuple {
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, cn)
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, cn)
template := getCertTemplate(issuer == nil, true, false, cn)
return getRSACertTupleWithTemplate(template, privKey, issuer)
}
func GetRSASelfSignedCertTupleWithPK(privKey *rsa.PrivateKey, cn string) RSACertTuple {
template := getCertTemplate(false, true, cn)
template := getCertTemplate(false, true, false, cn)
return getRSACertTupleWithTemplate(template, privKey, nil)
}
@ -231,7 +278,7 @@ func getRSACertTupleWithTemplate(template *x509.Certificate, privKey *rsa.Privat
}
func GetECDSACertTupleWithPK(privKey *ecdsa.PrivateKey, cn string, issuer *ECCertTuple) ECCertTuple {
template := getCertTemplate(issuer == nil, true, cn)
template := getCertTemplate(issuer == nil, true, false, cn)
var certBytes []byte
if issuer != nil {
@ -247,7 +294,7 @@ func GetECDSACertTupleWithPK(privKey *ecdsa.PrivateKey, cn string, issuer *ECCer
}
}
func getCertTemplate(isRoot bool, setCodeSignEKU bool, cn string) *x509.Certificate {
func getCertTemplate(isRoot bool, setCodeSignEKU, setTimestampEKU bool, cn string) *x509.Certificate {
template := &x509.Certificate{
Subject: pkix.Name{
Organization: []string{"Notary"},
@ -262,6 +309,15 @@ func getCertTemplate(isRoot bool, setCodeSignEKU bool, cn string) *x509.Certific
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 {
@ -275,7 +331,6 @@ func getCertTemplate(isRoot bool, setCodeSignEKU bool, cn string) *x509.Certific
template.SerialNumber = big.NewInt(int64(mrand.Intn(200)))
template.NotAfter = time.Now().AddDate(0, 0, 1)
}
return template
}

View File

@ -1,301 +0,0 @@
// 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/ecdsa"
"crypto/rsa"
"crypto/x509"
"errors"
"fmt"
"strings"
"time"
)
// 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 {
return validateCertChain(certChain, 0, signingTime)
}
// 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, signingTime *time.Time) error {
return validateCertChain(certChain, x509.ExtKeyUsageTimeStamping, signingTime)
}
func validateCertChain(certChain []*x509.Certificate, expectedLeafEku x509.ExtKeyUsage, 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 signingTime != nil && (signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter)) {
return fmt.Errorf("certificate with subject %q was not valid at signing time of %s", cert.Subject, signingTime.UTC())
}
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 := validateLeafCertificate(cert, expectedLeafEku); err != nil {
return fmt.Errorf("invalid self-signed certificate. Error: %w", err)
}
return nil
}
for i, cert := range certChain {
if signingTime != nil && (signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter)) {
return fmt.Errorf("certificate with subject %q was not valid at signing time of %s", cert.Subject, signingTime.UTC())
}
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 := validateLeafCertificate(cert, expectedLeafEku); err != nil {
return err
}
} else {
if err := validateCACertificate(cert, i-1); err != nil {
return err
}
}
}
return nil
}
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 validateCACertificate(cert *x509.Certificate, expectedPathLen int) error {
if err := validateCABasicConstraints(cert, expectedPathLen); err != nil {
return err
}
return validateCAKeyUsage(cert)
}
func validateLeafCertificate(cert *x509.Certificate, expectedEku x509.ExtKeyUsage) error {
if err := validateLeafBasicConstraints(cert); err != nil {
return err
}
if err := validateLeafKeyUsage(cert); err != nil {
return err
}
if err := validateExtendedKeyUsage(cert, expectedEku); err != nil {
return err
}
return validateKeyLength(cert)
}
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 validateCAKeyUsage(cert *x509.Certificate) error {
if err := validateKeyUsagePresent(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 validateLeafKeyUsage(cert *x509.Certificate) error {
if err := validateKeyUsagePresent(cert); err != nil {
return err
}
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.KeyUsageContentCommitment != 0 {
invalidKeyUsages = append(invalidKeyUsages, `"ContentCommitment"`)
}
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 validateKeyUsagePresent(cert *x509.Certificate) error {
keyUsageExtensionOid := []int{2, 5, 29, 15}
var hasKeyUsageExtension bool
for _, ext := range cert.Extensions {
if ext.Id.Equal(keyUsageExtensionOid) {
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 validateExtendedKeyUsage(cert *x509.Certificate, expectedEku x509.ExtKeyUsage) error {
if len(cert.ExtKeyUsage) <= 0 {
return nil
}
excludedEkus := []x509.ExtKeyUsage{
x509.ExtKeyUsageAny,
x509.ExtKeyUsageServerAuth,
x509.ExtKeyUsageClientAuth,
x509.ExtKeyUsageEmailProtection,
x509.ExtKeyUsageOCSPSigning,
}
if expectedEku == 0 {
excludedEkus = append(excludedEkus, x509.ExtKeyUsageTimeStamping)
} else if expectedEku == x509.ExtKeyUsageTimeStamping {
excludedEkus = append(excludedEkus, x509.ExtKeyUsageCodeSigning)
}
var hasExpectedEku bool
for _, certEku := range cert.ExtKeyUsage {
if certEku == expectedEku {
hasExpectedEku = true
continue
}
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))
}
}
}
if expectedEku != 0 && !hasExpectedEku {
return fmt.Errorf("certificate with subject %q: extended key usage must contain %s eku", cert.Subject, ekuToString(expectedEku))
}
return nil
}
func validateKeyLength(cert *x509.Certificate) error {
switch key := cert.PublicKey.(type) {
case *rsa.PublicKey:
if key.N.BitLen() < 2048 {
return fmt.Errorf("certificate with subject %q: rsa public key length must be 2048 bits or higher", cert.Subject)
}
case *ecdsa.PublicKey:
if key.Params().N.BitLen() < 256 {
return fmt.Errorf("certificate with subject %q: ecdsa public key length must be 256 bits or higher", cert.Subject)
}
}
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))
}
}

View File

@ -0,0 +1,179 @@
// 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"
"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]
// check self-signed
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)
}
// check self-issued
if !bytes.Equal(cert.RawSubject, cert.RawIssuer) {
return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: issuer(%s) and subject(%s) are not the same", cert.Subject, cert.Issuer, cert.Subject)
}
if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil {
return signedTimeError
}
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
}

View File

@ -17,10 +17,13 @@ import (
"crypto/x509"
"crypto/x509/pkix"
_ "embed"
"encoding/asn1"
"errors"
"os"
"strings"
"testing"
"time"
"github.com/notaryproject/notation-core-go/internal/oid"
"github.com/notaryproject/notation-core-go/testhelper"
)
@ -100,25 +103,6 @@ var codeSigningLeafPem = "-----BEGIN CERTIFICATE-----\n" +
"cwtsQn/iENuvFcfRHcFhvRjEFrIP+Ugx\n" +
"-----END CERTIFICATE-----"
var timeStampingLeafPem = "-----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 unrelatedCertPem = "-----BEGIN CERTIFICATE-----\n" +
"MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" +
"BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" +
@ -183,7 +167,6 @@ var rootCert = parseCertificateFromString(rootCertPem)
var intermediateCert1 = parseCertificateFromString(intermediateCertPem1)
var intermediateCert2 = parseCertificateFromString(intermediateCertPem2)
var codeSigningCert = parseCertificateFromString(codeSigningLeafPem)
var timeStampingCert = parseCertificateFromString(timeStampingLeafPem)
var unrelatedCert = parseCertificateFromString(unrelatedCertPem)
var intermediateCertInvalidPathLen = parseCertificateFromString(intermediateCertInvalidPathLenPem)
var codeSigningLeafInvalidPathLen = parseCertificateFromString(codeSigningLeafInvalidPathLenPem)
@ -211,15 +194,6 @@ func TestValidCodeSigningChain(t *testing.T) {
}
}
func TestValidTimeStampingChain(t *testing.T) {
certChain := []*x509.Certificate{timeStampingCert, intermediateCert2, intermediateCert1, rootCert}
signingTime := time.Now()
if err := ValidateTimeStampingCertChain(certChain, &signingTime); err != nil {
t.Fatal(err)
}
}
func TestFailEmptyChain(t *testing.T) {
signingTime := time.Now()
err := ValidateCodeSigningCertChain(nil, &signingTime)
@ -227,12 +201,71 @@ func TestFailEmptyChain(t *testing.T) {
assertErrorEqual("certificate chain must contain at least one certificate", err, t)
}
func TestFailNonSelfSignedLeafCert(t *testing.T) {
signingTime := time.Now()
err := ValidateCodeSigningCertChain([]*x509.Certificate{codeSigningCert}, &signingTime)
assertErrorEqual("invalid self-signed certificate. subject: \"CN=CodeSigningLeaf\". Error: crypto/rsa: verification error", err, t)
}
func TestFailSelfIssuedCodeSigningCert(t *testing.T) {
chainTuple := testhelper.GetRevokableRSATimestampChain(2)
// the leaf certiifcate and the root certificate share the same private key
// so the leaf is also self-signed but issuer and subject are different
chain := []*x509.Certificate{chainTuple[0].Cert}
signingTime := time.Now()
err := ValidateCodeSigningCertChain(chain, &signingTime)
assertErrorEqual("invalid self-signed certificate. subject: \"CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US\". Error: issuer(CN=Notation Test Revokable RSA Chain Cert Root,O=Notary,L=Seattle,ST=WA,C=US) and subject(CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US) are not the same", err, t)
}
func TestInvalidCodeSigningCertSigningTime(t *testing.T) {
chainTuple := testhelper.GetRevokableRSATimestampChain(2)
chain := []*x509.Certificate{chainTuple[1].Cert}
signingTime := time.Date(2021, 7, 7, 20, 48, 42, 0, time.UTC)
expectPrefix := "certificate with subject \"CN=Notation Test Revokable RSA Chain Cert Root,O=Notary,L=Seattle,ST=WA,C=US\" was invalid at signing time of 2021-07-07 20:48:42 +0000 UTC"
err := ValidateCodeSigningCertChain(chain, &signingTime)
if !strings.HasPrefix(err.Error(), expectPrefix) {
t.Errorf("expected error to start with %q, got %q", expectPrefix, err)
}
}
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 not valid at signing time of 2021-07-07 20:48:42 +0000 UTC", err, t)
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) {
@ -310,13 +343,13 @@ func TestInvalidSelfSignedSigningCertificate(t *testing.T) {
// ---------------- CA Validations ----------------
func TestValidCa(t *testing.T) {
if err := validateCACertificate(rootCert, 2); err != nil {
if err := validateCodeSigningCACertificate(rootCert, 2); err != nil {
t.Fatal(err)
}
}
func TestFailInvalidPathLenCa(t *testing.T) {
err := validateCACertificate(rootCert, 3)
err := validateCodeSigningCACertificate(rootCert, 3)
assertErrorEqual("certificate with subject \"CN=Root\": expected path length of 3 but certificate has path length 2 instead", err, t)
}
@ -340,7 +373,7 @@ var noBasicConstraintsCaPem = "-----BEGIN CERTIFICATE-----\n" +
var noBasicConstraintsCa = parseCertificateFromString(noBasicConstraintsCaPem)
func TestFailNoBasicConstraintsCa(t *testing.T) {
err := validateCACertificate(noBasicConstraintsCa, 3)
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)
}
@ -364,7 +397,7 @@ var basicConstraintsNotCaPem = "-----BEGIN CERTIFICATE-----\n" +
var basicConstraintsNotCa = parseCertificateFromString(basicConstraintsNotCaPem)
func TestFailBasicConstraintsNotCa(t *testing.T) {
err := validateCACertificate(basicConstraintsNotCa, 3)
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)
}
@ -388,7 +421,7 @@ var kuNotCriticalCertSignCaPem = "-----BEGIN CERTIFICATE-----\n" +
var kuNotCriticalCertSignCa = parseCertificateFromString(kuNotCriticalCertSignCaPem)
func TestFailKuNotCriticalCertSignCa(t *testing.T) {
err := validateCACertificate(kuNotCriticalCertSignCa, 3)
err := validateCodeSigningCACertificate(kuNotCriticalCertSignCa, 3)
assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be marked critical", err, t)
}
@ -412,7 +445,7 @@ var kuMissingCaPem = "-----BEGIN CERTIFICATE-----\n" +
var kuMissingCa = parseCertificateFromString(kuMissingCaPem)
func TestFailKuMissingCa(t *testing.T) {
err := validateCACertificate(kuMissingCa, 3)
err := validateCodeSigningCACertificate(kuMissingCa, 3)
assertErrorEqual("certificate with subject \"CN=Hello\": key usage extension must be present", err, t)
}
@ -436,11 +469,11 @@ var kuNotCertSignCaPem = "-----BEGIN CERTIFICATE-----\n" +
var kuNotCertSignCa = parseCertificateFromString(kuNotCertSignCaPem)
func TestFailKuNotCertSignCa(t *testing.T) {
err := validateCACertificate(kuNotCertSignCa, 3)
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 + Time-Stamping Leaf Validations ----------------
// ---------------- Code-Signing Validations ----------------
var validNoOptionsLeafPem = "-----BEGIN CERTIFICATE-----\n" +
"MIICtzCCAZ+gAwIBAgIJAL+FUPhO8J8cMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" +
@ -462,7 +495,7 @@ var validNoOptionsLeafPem = "-----BEGIN CERTIFICATE-----\n" +
var validNoOptionsLeaf = parseCertificateFromString(validNoOptionsLeafPem)
func TestValidNoOptionsLeaf(t *testing.T) {
if err := validateLeafCertificate(validNoOptionsLeaf, x509.ExtKeyUsageCodeSigning); err != nil {
if err := validateCodeSigningLeafCertificate(validNoOptionsLeaf); err != nil {
t.Fatal(err)
}
}
@ -487,7 +520,7 @@ var caTrueLeafPem = "-----BEGIN CERTIFICATE-----\n" +
var caTrueLeaf = parseCertificateFromString(caTrueLeafPem)
func TestFailCaTrueLeaf(t *testing.T) {
err := validateLeafCertificate(caTrueLeaf, x509.ExtKeyUsageCodeSigning)
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)
}
@ -511,8 +544,8 @@ var kuNoDigitalSignatureLeafPem = "-----BEGIN CERTIFICATE-----\n" +
var kuNoDigitalSignatureLeaf = parseCertificateFromString(kuNoDigitalSignatureLeafPem)
func TestFailKuNoDigitalSignatureLeaf(t *testing.T) {
err := validateLeafCertificate(kuNoDigitalSignatureLeaf, x509.ExtKeyUsageCodeSigning)
assertErrorEqual("The certificate with subject \"CN=Hello\" is invalid. The key usage must have the bit positions for \"Digital Signature\" set", err, 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" +
@ -535,8 +568,8 @@ var kuWrongValuesLeafPem = "-----BEGIN CERTIFICATE-----\n" +
var kuWrongValuesLeaf = parseCertificateFromString(kuWrongValuesLeafPem)
func TestFailKuWrongValuesLeaf(t *testing.T) {
err := validateLeafCertificate(kuWrongValuesLeaf, x509.ExtKeyUsageCodeSigning)
assertErrorEqual("The certificate with subject \"CN=Hello\" is invalid. The key usage must be \"Digital Signature\" only, but found \"CertSign\", \"CRLSign\"", err, 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" +
@ -554,8 +587,8 @@ var rsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" +
var rsaKeyTooSmallLeaf = parseCertificateFromString(rsaKeyTooSmallLeafPem)
func TestFailRsaKeyTooSmallLeaf(t *testing.T) {
err := validateLeafCertificate(rsaKeyTooSmallLeaf, x509.ExtKeyUsageCodeSigning)
assertErrorEqual("certificate with subject \"CN=Hello\": rsa public key length must be 2048 bits or higher", err, 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" +
@ -570,141 +603,21 @@ var ecdsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" +
var ecdsaKeyTooSmallLeaf = parseCertificateFromString(ecdsaKeyTooSmallLeafPem)
func TestFailEcdsaKeyTooSmallLeaf(t *testing.T) {
err := validateLeafCertificate(ecdsaKeyTooSmallLeaf, x509.ExtKeyUsageCodeSigning)
assertErrorEqual("certificate with subject \"CN=Hello\": ecdsa public key length must be 256 bits or higher", err, 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 := validateLeafCertificate(codeSigningCert, x509.ExtKeyUsageCodeSigning); err != nil {
if err := validateCodeSigningLeafCertificate(codeSigningCert); err != nil {
t.Fatal(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 := validateLeafCertificate(ekuWrongValuesCodeLeaf, x509.ExtKeyUsageCodeSigning)
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 := validateLeafCertificate(ekuMissingCodeSigningLeaf, x509.ExtKeyUsageCodeSigning)
assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain OCSPSigning eku", err, t)
}
// ---------------- Time-Stamping Leaf Validations ----------------
func TestValidFullOptionsTimeLeaf(t *testing.T) {
if err := validateLeafCertificate(timeStampingCert, x509.ExtKeyUsageTimeStamping); err != nil {
t.Fatal(err)
}
}
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 := validateLeafCertificate(ekuWrongValuesTimeLeaf, x509.ExtKeyUsageTimeStamping)
assertErrorEqual("certificate with subject \"CN=Hello\": extended key usage must not contain ServerAuth eku", 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 := validateLeafCertificate(ekuMissingTimeStampingLeaf, x509.ExtKeyUsageTimeStamping)
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 TestValidateLeafKeyUsage(t *testing.T) {
extensions := []pkix.Extension{{
Id: asn1.ObjectIdentifier{2, 5, 29, 15}, // OID for KeyUsage
Id: oid.KeyUsage,
Critical: true,
}}
@ -729,7 +642,7 @@ func TestValidateLeafKeyUsage(t *testing.T) {
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageContentCommitment,
Extensions: extensions,
},
expectedErrMsg: "The certificate with subject \"CN=Test CN\" is invalid. The key usage must be \"Digital Signature\" only, but found \"ContentCommitment\"",
expectedErrMsg: "",
},
{
name: "Missing DigitalSignature usage",
@ -738,7 +651,7 @@ func TestValidateLeafKeyUsage(t *testing.T) {
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",
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",
@ -747,7 +660,7 @@ func TestValidateLeafKeyUsage(t *testing.T) {
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\"",
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",
@ -756,7 +669,7 @@ func TestValidateLeafKeyUsage(t *testing.T) {
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\"",
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\"",
},
}
@ -773,3 +686,81 @@ func TestValidateLeafKeyUsage(t *testing.T) {
})
}
}
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
}

124
x509/helper.go Normal file
View File

@ -0,0 +1,124 @@
// 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/internal/algorithm"
)
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 {
_, err := algorithm.ExtractKeySpec(cert)
if err != nil {
return fmt.Errorf("certificate with subject %q: %w", cert.Subject, err)
}
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))
}
}

BIN
x509/testdata/timestamp_intermediate.crt vendored Normal file

Binary file not shown.

BIN
x509/testdata/timestamp_leaf.crt vendored Normal file

Binary file not shown.

BIN
x509/testdata/timestamp_root.crt vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,167 @@
// 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"
"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]
// check self-signed
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)
}
// check self-issued
if !bytes.Equal(cert.RawSubject, cert.RawIssuer) {
return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: issuer (%s) and subject (%s) are not the same", cert.Subject, cert.Issuer, cert.Subject)
}
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
}

View File

@ -0,0 +1,306 @@
// 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/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"math/big"
"strings"
"testing"
"time"
"github.com/notaryproject/notation-core-go/internal/oid"
"github.com/notaryproject/notation-core-go/testhelper"
)
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 TestFailSelfIssuedTimestampingCert(t *testing.T) {
chainTuple := testhelper.GetRevokableRSATimestampChain(2)
// the leaf certiifcate and the root certificate share the same private key
// so the leaf is also self-signed but issuer and subject are different
chain := []*x509.Certificate{chainTuple[0].Cert}
err := ValidateTimestampingCertChain(chain)
assertErrorEqual("invalid self-signed certificate. subject: \"CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US\". Error: issuer (CN=Notation Test Revokable RSA Chain Cert Root,O=Notary,L=Seattle,ST=WA,C=US) and subject (CN=Notation Test Revokable RSA Chain Cert 2,O=Notary,L=Seattle,ST=WA,C=US) are not the same", err, t)
}
func TestInvalidTimestampSelfSignedCert(t *testing.T) {
cert, err := createSelfSignedCert("valid cert", "valid cert", false)
if err != nil {
t.Error(err)
}
certChain := []*x509.Certificate{cert}
expectPrefix := "invalid self-signed certificate. Error: timestamp signing certificate with subject \"CN=valid cert\" must have and only have Timestamping as extended key usage"
err = ValidateTimestampingCertChain(certChain)
if !strings.HasPrefix(err.Error(), expectPrefix) {
t.Errorf("expected error to start with %q, got %q", expectPrefix, err)
}
}
func TestValidTimestampSelfSignedCert(t *testing.T) {
cert, err := createSelfSignedCert("valid cert", "valid cert", true)
if err != nil {
t.Error(err)
}
certChain := []*x509.Certificate{cert}
err = ValidateTimestampingCertChain(certChain)
if err != nil {
t.Error(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")
}
}
func createSelfSignedCert(subject string, issuer string, isTimestamp bool) (*x509.Certificate, error) {
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: subject},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
if isTimestamp {
oids := []asn1.ObjectIdentifier{{1, 3, 6, 1, 5, 5, 7, 3, 8}}
value, err := asn1.Marshal(oids)
if err != nil {
return nil, err
}
template.ExtraExtensions = []pkix.Extension{{
Id: oid.ExtKeyUsage,
Critical: true,
Value: value,
}}
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}
}
parentTemplate := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{CommonName: issuer},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign,
}
certDER, err := x509.CreateCertificate(rand.Reader, template, parentTemplate, &priv.PublicKey, priv)
if err != nil {
return nil, err
}
return x509.ParseCertificate(certDER)
}