Compare commits
64 Commits
Author | SHA1 | Date |
---|---|---|
|
a9c5e3f1a5 | |
|
ef8789627f | |
|
46726d8697 | |
|
b85e8f7e65 | |
|
4d73532534 | |
|
6a378d5686 | |
|
ea37e4e6c3 | |
|
fcf45123a3 | |
|
9c4662fe2b | |
|
441bbe882a | |
|
751008360b | |
|
b07b0ef090 | |
|
2bf73c7f71 | |
|
0c60ea723a | |
|
030abc293c | |
|
2cd55de6f4 | |
|
95d89543c9 | |
|
ce88e08466 | |
|
f0b6f89f03 | |
|
33af15a189 | |
|
49a6aaa61a | |
|
9524e15b88 | |
|
199c9a93f5 | |
|
0c651a46c7 | |
|
1683ddbfc5 | |
|
4819e1d242 | |
|
3067ab1223 | |
|
e90546bd90 | |
|
0786f51de7 | |
|
5d9b2edc6a | |
|
695ea0c1ad | |
|
55e3568687 | |
|
235910ba8e | |
|
453a5ebcbf | |
|
004b86dbf2 | |
|
45dcf46937 | |
|
f45197cbd5 | |
|
e18808c298 | |
|
faac9b7f3f | |
|
a1c0af6584 | |
|
c85a3d9f65 | |
|
a3100ce024 | |
|
4211b09afe | |
|
6f8b75c6b0 | |
|
ff5e5b8d44 | |
|
f624dfd1a3 | |
|
356b30e954 | |
|
9f13c9ec46 | |
|
66ff8c2adb | |
|
807a338673 | |
|
9a2ff9ea5f | |
|
93218d92c1 | |
|
c71a505669 | |
|
f6d353ba92 | |
|
2bc927beb6 | |
|
d9131c7fec | |
|
90383555c1 | |
|
db1ee20431 | |
|
bdbd23000c | |
|
383d314fe7 | |
|
38b8f1a73b | |
|
483aabaade | |
|
d957bed16d | |
|
96d9a63946 |
|
@ -16,3 +16,6 @@ coverage:
|
||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
target: 89%
|
target: 89%
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: 90%
|
|
@ -22,3 +22,5 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
uses: ./.github/workflows/reusable-build.yml
|
uses: ./.github/workflows/reusable-build.yml
|
||||||
|
secrets:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
@ -15,23 +15,28 @@ name: Reusable build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
|
secrets:
|
||||||
|
CODECOV_TOKEN:
|
||||||
|
required: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: ["1.20", "1.21"]
|
go-version: ["1.23", "1.24"]
|
||||||
fail-fast: true
|
fail-fast: true
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Set up Go ${{ matrix.go-version }} environment
|
- name: Set up Go ${{ matrix.go-version }} environment
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: make test
|
run: make test
|
||||||
- name: Upload coverage to codecov.io
|
- name: Upload coverage to codecov.io
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v5
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
|
@ -26,19 +26,19 @@ jobs:
|
||||||
security-events: write
|
security-events: write
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
go-version: ["1.20", "1.21"]
|
go-version: ["1.23", "1.24"]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Set up Go ${{ matrix.go-version }} environment
|
- name: Set up Go ${{ matrix.go-version }} environment
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: go
|
languages: go
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
|
@ -23,11 +23,14 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Check license header
|
- name: Check license header
|
||||||
uses: apache/skywalking-eyes/header@v0.4.0
|
uses: apache/skywalking-eyes/header@5c5b974209f0de5d905f37deb69369068ebfc15c
|
||||||
|
|
||||||
with:
|
with:
|
||||||
mode: check
|
mode: check
|
||||||
config: .github/licenserc.yml
|
config: .github/licenserc.yml
|
||||||
- name: Check dependencies license
|
- name: Check dependencies license
|
||||||
uses: apache/skywalking-eyes/dependency@v0.4.0
|
uses: apache/skywalking-eyes/dependency@5c5b974209f0de5d905f37deb69369068ebfc15c
|
||||||
with:
|
with:
|
||||||
config: .github/licenserc.yml
|
config: .github/licenserc.yml
|
||||||
|
flags:
|
||||||
|
--weak-compatible=true
|
|
@ -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
|
|
@ -1,3 +1,3 @@
|
||||||
# Repo-Level Owners (in alphabetical order)
|
# Repo-Level Owners (in alphabetical order)
|
||||||
# Note: This is only for the notaryproject/notation-core-go repo
|
# 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
|
||||||
|
|
18
MAINTAINERS
18
MAINTAINERS
|
@ -1,14 +1,22 @@
|
||||||
# Org-Level Maintainers (in alphabetical order)
|
# Org-Level Maintainers (in alphabetical order)
|
||||||
# Pattern: [First Name] [Last Name] <[Email Address]> ([GitHub Handle])
|
# Pattern: [First Name] [Last Name] <[Email Address]> ([GitHub Handle])
|
||||||
Justin Cormack <justin.cormack@docker.com> (@justincormack)
|
|
||||||
Niaz Khan <niazfk@amazon.com> (@niazfk)
|
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)
|
# Repo-Level Maintainers (in alphabetical order)
|
||||||
# Note: This is for the notaryproject/notation-core-go repo
|
# Note: This is for the notaryproject/notation-core-go repo
|
||||||
Junjie Gao <junjiegao@microsoft.com> (@JeyJeyGao)
|
|
||||||
Milind Gokarn <gokarnm@amazon.com> (@gokarnm)
|
Milind Gokarn <gokarnm@amazon.com> (@gokarnm)
|
||||||
Patrick Zheng <patrickzheng@microsoft.com> (@Two-Hearts)
|
Patrick Zheng <patrickzheng@microsoft.com> (@Two-Hearts)
|
||||||
Pritesh Bandi <priteshbandi@gmail.com> (@priteshbandi)
|
|
||||||
Rakesh Gariganti <garigant@amazon.com> (@rgnote)
|
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)
|
2
Makefile
2
Makefile
|
@ -29,7 +29,7 @@ clean:
|
||||||
.PHONY: check-line-endings
|
.PHONY: check-line-endings
|
||||||
check-line-endings: ## check line endings
|
check-line-endings: ## check line endings
|
||||||
! find . -name "*.go" -type f -exec file "{}" ";" | grep CRLF
|
! 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
|
.PHONY: fix-line-endings
|
||||||
fix-line-endings: ## fix line endings
|
fix-line-endings: ## fix line endings
|
||||||
|
|
26
README.md
26
README.md
|
@ -4,4 +4,28 @@
|
||||||
[](https://codecov.io/gh/notaryproject/notation-core-go)
|
[](https://codecov.io/gh/notaryproject/notation-core-go)
|
||||||
[](https://pkg.go.dev/github.com/notaryproject/notation-core-go@main)
|
[](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
11
go.mod
|
@ -1,12 +1,13 @@
|
||||||
module github.com/notaryproject/notation-core-go
|
module github.com/notaryproject/notation-core-go
|
||||||
|
|
||||||
go 1.20
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/fxamacker/cbor/v2 v2.5.0
|
github.com/fxamacker/cbor/v2 v2.8.0
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||||
github.com/veraison/go-cose v1.1.0
|
github.com/notaryproject/tspclient-go v1.0.0
|
||||||
golang.org/x/crypto v0.14.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
|
require github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
|
18
go.sum
18
go.sum
|
@ -1,10 +1,12 @@
|
||||||
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
|
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||||
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o=
|
github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4=
|
||||||
github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4=
|
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 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
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.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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")
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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.
Binary file not shown.
|
@ -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
|
@ -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())
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
|
}
|
|
@ -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]
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -11,58 +11,27 @@
|
||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// 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
|
package ocsp
|
||||||
|
|
||||||
import (
|
import "github.com/notaryproject/notation-core-go/revocation/internal/ocsp"
|
||||||
"fmt"
|
|
||||||
"time"
|
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())
|
|
||||||
}
|
|
||||||
|
|
|
@ -16,41 +16,31 @@
|
||||||
package ocsp
|
package ocsp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"context"
|
||||||
"crypto"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/asn1"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"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"
|
"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
|
// Options specifies values that are needed to check OCSP revocation
|
||||||
type Options struct {
|
type Options struct {
|
||||||
CertChain []*x509.Certificate
|
CertChain []*x509.Certificate
|
||||||
|
|
||||||
|
// CertChainPurpose is the purpose of the certificate chain. Supported
|
||||||
|
// values are CodeSigning and Timestamping.
|
||||||
|
// When not provided, the default value is CodeSigning.
|
||||||
|
CertChainPurpose purpose.Purpose
|
||||||
SigningTime time.Time
|
SigningTime time.Time
|
||||||
HTTPClient *http.Client
|
HTTPClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
pkixNoCheckOID string = "1.3.6.1.5.5.7.48.1.5"
|
|
||||||
invalidityDateOID string = "2.5.29.24"
|
|
||||||
// Max size determined from https://www.ibm.com/docs/en/sva/9.0.6?topic=stanza-ocsp-max-response-size.
|
|
||||||
// Typical size is ~4 KB
|
|
||||||
ocspMaxResponseSize int64 = 20480 //bytes
|
|
||||||
)
|
|
||||||
|
|
||||||
// CheckStatus checks OCSP based on the passed options and returns an array of
|
// CheckStatus checks OCSP based on the passed options and returns an array of
|
||||||
// result.CertRevocationResult objects that contains the results and error. The
|
// result.CertRevocationResult objects that contains the results and error. The
|
||||||
// length of this array will always be equal to the length of the certificate
|
// length of this array will always be equal to the length of the certificate
|
||||||
|
@ -60,24 +50,25 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) {
|
||||||
return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")}
|
return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate cert chain structure
|
if err := x509util.ValidateChain(opts.CertChain, opts.CertChainPurpose); err != nil {
|
||||||
// Since this is using authentic signing time, signing time may be zero.
|
return nil, err
|
||||||
// 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}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
certResults := make([]*result.CertRevocationResult, len(opts.CertChain))
|
certResults := make([]*result.CertRevocationResult, len(opts.CertChain))
|
||||||
|
certCheckStatusOptions := ocsp.CertCheckStatusOptions{
|
||||||
|
SigningTime: opts.SigningTime,
|
||||||
|
HTTPClient: opts.HTTPClient,
|
||||||
|
}
|
||||||
|
|
||||||
// Check status for each cert in cert chain
|
// Check status for each cert in cert chain
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
ctx := context.Background()
|
||||||
for i, cert := range opts.CertChain[:len(opts.CertChain)-1] {
|
for i, cert := range opts.CertChain[:len(opts.CertChain)-1] {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
// Assume cert chain is accurate and next cert in chain is the issuer
|
// Assume cert chain is accurate and next cert in chain is the issuer
|
||||||
go func(i int, cert *x509.Certificate) {
|
go func(i int, cert *x509.Certificate) {
|
||||||
defer wg.Done()
|
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)
|
}(i, cert)
|
||||||
}
|
}
|
||||||
// Last is root cert, which will never be revoked by OCSP
|
// Last is root cert, which will never be revoked by OCSP
|
||||||
|
@ -92,182 +83,3 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
return certResults, nil
|
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/notaryproject/notation-core-go/revocation/purpose"
|
||||||
"github.com/notaryproject/notation-core-go/revocation/result"
|
"github.com/notaryproject/notation-core-go/revocation/result"
|
||||||
"github.com/notaryproject/notation-core-go/testhelper"
|
"github.com/notaryproject/notation-core-go/testhelper"
|
||||||
"golang.org/x/crypto/ocsp"
|
"golang.org/x/crypto/ocsp"
|
||||||
|
@ -78,79 +79,12 @@ 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) {
|
func TestCheckStatusForSelfSignedCert(t *testing.T) {
|
||||||
selfSignedTuple := testhelper.GetRSASelfSignedSigningCertTuple("Notation revocation test self-signed cert")
|
selfSignedTuple := testhelper.GetRSASelfSignedSigningCertTuple("Notation revocation test self-signed cert")
|
||||||
client := testhelper.MockClient([]testhelper.RSACertTuple{selfSignedTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
client := testhelper.MockClient([]testhelper.RSACertTuple{selfSignedTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: []*x509.Certificate{selfSignedTuple.Cert},
|
CertChain: []*x509.Certificate{selfSignedTuple.Cert},
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -168,6 +102,7 @@ func TestCheckStatusForRootCert(t *testing.T) {
|
||||||
client := testhelper.MockClient([]testhelper.RSACertTuple{rootTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
client := testhelper.MockClient([]testhelper.RSACertTuple{rootTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: []*x509.Certificate{rootTuple.Cert},
|
CertChain: []*x509.Certificate{rootTuple.Cert},
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -187,6 +122,7 @@ func TestCheckStatusForNonSelfSignedSingleCert(t *testing.T) {
|
||||||
client := testhelper.MockClient([]testhelper.RSACertTuple{certTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
client := testhelper.MockClient([]testhelper.RSACertTuple{certTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: []*x509.Certificate{certTuple.Cert},
|
CertChain: []*x509.Certificate{certTuple.Cert},
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -213,6 +149,7 @@ func TestCheckStatusForChain(t *testing.T) {
|
||||||
t.Run("empty chain", func(t *testing.T) {
|
t.Run("empty chain", func(t *testing.T) {
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: []*x509.Certificate{},
|
CertChain: []*x509.Certificate{},
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: http.DefaultClient,
|
HTTPClient: http.DefaultClient,
|
||||||
}
|
}
|
||||||
|
@ -229,6 +166,7 @@ func TestCheckStatusForChain(t *testing.T) {
|
||||||
client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: revokableChain,
|
CertChain: revokableChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -252,6 +190,7 @@ func TestCheckStatusForChain(t *testing.T) {
|
||||||
// 3rd cert will be unknown, the rest will be good
|
// 3rd cert will be unknown, the rest will be good
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: revokableChain,
|
CertChain: revokableChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -280,6 +219,7 @@ func TestCheckStatusForChain(t *testing.T) {
|
||||||
// 3rd cert will be revoked, the rest will be good
|
// 3rd cert will be revoked, the rest will be good
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: revokableChain,
|
CertChain: revokableChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -308,6 +248,7 @@ func TestCheckStatusForChain(t *testing.T) {
|
||||||
// 3rd cert will be unknown, 5th will be revoked, the rest will be good
|
// 3rd cert will be unknown, 5th will be revoked, the rest will be good
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: revokableChain,
|
CertChain: revokableChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -342,6 +283,7 @@ func TestCheckStatusForChain(t *testing.T) {
|
||||||
// 3rd cert will be revoked, the rest will be good
|
// 3rd cert will be revoked, the rest will be good
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: revokableChain,
|
CertChain: revokableChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -366,6 +308,7 @@ func TestCheckStatusForChain(t *testing.T) {
|
||||||
// 3rd cert will be unknown, 5th will be revoked, the rest will be good
|
// 3rd cert will be unknown, 5th will be revoked, the rest will be good
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: revokableChain,
|
CertChain: revokableChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -394,6 +337,7 @@ func TestCheckStatusForChain(t *testing.T) {
|
||||||
// 3rd cert will be revoked, the rest will be good
|
// 3rd cert will be revoked, the rest will be good
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: revokableChain,
|
CertChain: revokableChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now().Add(time.Hour),
|
SigningTime: time.Now().Add(time.Hour),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -423,6 +367,7 @@ func TestCheckStatusForChain(t *testing.T) {
|
||||||
// 3rd cert will be revoked, the rest will be good
|
// 3rd cert will be revoked, the rest will be good
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: revokableChain,
|
CertChain: revokableChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: zeroTime,
|
SigningTime: zeroTime,
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -465,15 +410,16 @@ func TestCheckStatusErrors(t *testing.T) {
|
||||||
expiredLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw)
|
expiredLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw)
|
||||||
expiredLeaf.IsCA = false
|
expiredLeaf.IsCA = false
|
||||||
expiredLeaf.KeyUsage = x509.KeyUsageDigitalSignature
|
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}
|
expiredChain := []*x509.Certificate{expiredLeaf, revokableTuples[1].Cert, revokableTuples[2].Cert}
|
||||||
|
|
||||||
noHTTPLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw)
|
noHTTPLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw)
|
||||||
noHTTPLeaf.IsCA = false
|
noHTTPLeaf.IsCA = false
|
||||||
noHTTPLeaf.KeyUsage = x509.KeyUsageDigitalSignature
|
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}
|
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")}
|
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")}
|
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")}
|
expiredRespErr := GenericError{Err: errors.New("expired OCSP response")}
|
||||||
|
@ -482,6 +428,7 @@ func TestCheckStatusErrors(t *testing.T) {
|
||||||
t.Run("no OCSPServer specified", func(t *testing.T) {
|
t.Run("no OCSPServer specified", func(t *testing.T) {
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: noOCSPChain,
|
CertChain: noOCSPChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: http.DefaultClient,
|
HTTPClient: http.DefaultClient,
|
||||||
}
|
}
|
||||||
|
@ -504,6 +451,7 @@ func TestCheckStatusErrors(t *testing.T) {
|
||||||
t.Run("chain missing root", func(t *testing.T) {
|
t.Run("chain missing root", func(t *testing.T) {
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: noRootChain,
|
CertChain: noRootChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: http.DefaultClient,
|
HTTPClient: http.DefaultClient,
|
||||||
}
|
}
|
||||||
|
@ -519,6 +467,7 @@ func TestCheckStatusErrors(t *testing.T) {
|
||||||
t.Run("backwards chain", func(t *testing.T) {
|
t.Run("backwards chain", func(t *testing.T) {
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: backwardsChain,
|
CertChain: backwardsChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: http.DefaultClient,
|
HTTPClient: http.DefaultClient,
|
||||||
}
|
}
|
||||||
|
@ -531,10 +480,55 @@ func TestCheckStatusErrors(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("check codesigning cert with PurposeTimestamping", func(t *testing.T) {
|
||||||
|
opts := Options{
|
||||||
|
CertChain: okChain,
|
||||||
|
CertChainPurpose: purpose.Timestamping,
|
||||||
|
SigningTime: time.Now(),
|
||||||
|
HTTPClient: http.DefaultClient,
|
||||||
|
}
|
||||||
|
certResults, err := CheckStatus(opts)
|
||||||
|
if err == nil || err.Error() != timestampSigningCertErr.Error() {
|
||||||
|
t.Errorf("Expected CheckStatus to fail with %v, but got: %v", timestampSigningCertErr, err)
|
||||||
|
}
|
||||||
|
if certResults != nil {
|
||||||
|
t.Error("Expected certResults to be nil when there is an error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check with default CertChainPurpose", func(t *testing.T) {
|
||||||
|
opts := Options{
|
||||||
|
CertChain: okChain,
|
||||||
|
SigningTime: time.Now(),
|
||||||
|
HTTPClient: http.DefaultClient,
|
||||||
|
}
|
||||||
|
_, err := CheckStatus(opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check with unknwon CertChainPurpose", func(t *testing.T) {
|
||||||
|
opts := Options{
|
||||||
|
CertChain: okChain,
|
||||||
|
CertChainPurpose: -1,
|
||||||
|
SigningTime: time.Now(),
|
||||||
|
HTTPClient: http.DefaultClient,
|
||||||
|
}
|
||||||
|
certResults, err := CheckStatus(opts)
|
||||||
|
if err == nil || err.Error() != "invalid chain: expected chain to be correct and complete: unsupported certificate chain purpose -1" {
|
||||||
|
t.Errorf("Expected CheckStatus to fail with %v, but got: %v", timestampSigningCertErr, err)
|
||||||
|
}
|
||||||
|
if certResults != nil {
|
||||||
|
t.Error("Expected certResults to be nil when there is an error")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("timeout", func(t *testing.T) {
|
t.Run("timeout", func(t *testing.T) {
|
||||||
timeoutClient := &http.Client{Timeout: 1 * time.Nanosecond}
|
timeoutClient := &http.Client{Timeout: 1 * time.Nanosecond}
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: okChain,
|
CertChain: okChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: timeoutClient,
|
HTTPClient: timeoutClient,
|
||||||
}
|
}
|
||||||
|
@ -564,6 +558,7 @@ func TestCheckStatusErrors(t *testing.T) {
|
||||||
client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: expiredChain,
|
CertChain: expiredChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -588,6 +583,7 @@ func TestCheckStatusErrors(t *testing.T) {
|
||||||
client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, false)
|
client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, false)
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: okChain,
|
CertChain: okChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -608,6 +604,7 @@ func TestCheckStatusErrors(t *testing.T) {
|
||||||
client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: noHTTPChain,
|
CertChain: noHTTPChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -636,7 +633,7 @@ func TestCheckOCSPInvalidChain(t *testing.T) {
|
||||||
for i, cert := range misorderedIntermediateChain {
|
for i, cert := range misorderedIntermediateChain {
|
||||||
if i != (len(misorderedIntermediateChain) - 1) {
|
if i != (len(misorderedIntermediateChain) - 1) {
|
||||||
// Skip root which won't have an OCSP Server
|
// 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 {
|
for i, cert := range missingIntermediateChain {
|
||||||
if i != (len(missingIntermediateChain) - 1) {
|
if i != (len(missingIntermediateChain) - 1) {
|
||||||
// Skip root which won't have an OCSP Server
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -655,6 +652,7 @@ func TestCheckOCSPInvalidChain(t *testing.T) {
|
||||||
client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
client := testhelper.MockClient(revokableTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: missingIntermediateChain,
|
CertChain: missingIntermediateChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
@ -671,6 +669,7 @@ func TestCheckOCSPInvalidChain(t *testing.T) {
|
||||||
client := testhelper.MockClient(misorderedIntermediateTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
client := testhelper.MockClient(misorderedIntermediateTuples, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
|
||||||
opts := Options{
|
opts := Options{
|
||||||
CertChain: misorderedIntermediateChain,
|
CertChain: misorderedIntermediateChain,
|
||||||
|
CertChainPurpose: purpose.CodeSigning,
|
||||||
SigningTime: time.Now(),
|
SigningTime: time.Now(),
|
||||||
HTTPClient: client,
|
HTTPClient: client,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
|
@ -16,23 +16,27 @@ package result
|
||||||
|
|
||||||
import "strconv"
|
import "strconv"
|
||||||
|
|
||||||
// Result is a type of enumerated value to help characterize errors. It can be
|
// Result is a type of enumerated value to help characterize revocation result.
|
||||||
// OK, Unknown, or Revoked
|
// It can be OK, Unknown, NonRevokable, or Revoked
|
||||||
type Result int
|
type Result int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ResultUnknown is a Result that indicates that some error other than a
|
// 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
|
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
|
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
|
// ResultNonRevokable is a Result that indicates that the certificate cannot
|
||||||
// specified, the cert is a root certificate, or other related situations.
|
// 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
|
ResultNonRevokable
|
||||||
|
|
||||||
// ResultRevoked is a Result that indicates that at least one certificate was
|
// 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
|
ResultRevoked
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,8 +56,45 @@ func (r Result) String() string {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerResult encapsulates the result for a single server for a single
|
// RevocationMethod defines the method used to check the revocation status of a
|
||||||
// certificate in the chain
|
// 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 {
|
type ServerResult struct {
|
||||||
// Result of revocation for this server (Unknown if there is an error which
|
// Result of revocation for this server (Unknown if there is an error which
|
||||||
// prevents the retrieval of a valid status)
|
// 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
|
// Error is set if there is an error associated with the revocation check
|
||||||
// to this server
|
// to this server
|
||||||
Error error
|
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
|
// 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
|
// chain as well as the results from individual servers associated with this
|
||||||
// certificate
|
// certificate
|
||||||
type CertRevocationResult struct {
|
type CertRevocationResult struct {
|
||||||
// Result of revocation for a specific cert in the chain
|
// Result of revocation for a specific certificate in the chain.
|
||||||
//
|
|
||||||
// If there are multiple ServerResults, this is because no responses were
|
|
||||||
// able to be retrieved, leaving each ServerResult with a Result of Unknown.
|
|
||||||
// Thus, in the case of more than one ServerResult, this will be ResultUnknown
|
|
||||||
Result Result
|
Result Result
|
||||||
|
|
||||||
// An array of results for each server associated with the certificate.
|
// ServerResults is an array of results for each server associated with the
|
||||||
// The length will be either 1 or the number of OCSPServers for the cert.
|
// 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
|
// this server result is contained. Any errors for other servers are
|
||||||
// discarded in favor of this valid response.
|
// discarded in favor of this valid response.
|
||||||
//
|
|
||||||
// Otherwise, every server specified had some error that prevented the
|
// 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
|
ServerResults []*ServerResult
|
||||||
|
|
||||||
|
// RevocationMethod is the method used to check the revocation status of the
|
||||||
|
// certificate, including RevocationMethodUnknown, RevocationMethodOCSP,
|
||||||
|
// RevocationMethodCRL and RevocationMethodOCSPFallbackCRL
|
||||||
|
RevocationMethod RevocationMethod
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
func TestNewServerResult(t *testing.T) {
|
||||||
expectedR := &ServerResult{
|
expectedR := &ServerResult{
|
||||||
Result: ResultNonRevokable,
|
Result: ResultNonRevokable,
|
||||||
|
|
|
@ -16,50 +16,256 @@
|
||||||
package revocation
|
package revocation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"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"
|
"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 {
|
type Revocation interface {
|
||||||
// Validate checks the revocation status for a certificate chain using OCSP
|
// 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 returns an array of CertRevocationResults that contain the results
|
||||||
// and any errors that are encountered during the process
|
// 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
|
// revocation is an internal struct used for revocation checking
|
||||||
type revocation struct {
|
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) {
|
func New(httpClient *http.Client) (Revocation, error) {
|
||||||
if httpClient == nil {
|
if httpClient == nil {
|
||||||
return nil, errors.New("invalid input: a non-nil httpClient must be specified")
|
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{
|
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
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks the revocation status for a certificate chain using OCSP and
|
// Validate checks the revocation status for a certificate chain using OCSP and
|
||||||
// returns an array of CertRevocationResults that contain the results and any
|
// CRL if OCSP is not available. It returns an array of CertRevocationResults
|
||||||
// errors that are encountered during the process
|
// that contain the results and any errors that are encountered during the
|
||||||
|
// process.
|
||||||
//
|
//
|
||||||
// TODO: add CRL support
|
// This function tries OCSP and falls back to CRL when:
|
||||||
// https://github.com/notaryproject/notation-core-go/issues/125
|
// - 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) {
|
func (r *revocation) Validate(certChain []*x509.Certificate, signingTime time.Time) ([]*result.CertRevocationResult, error) {
|
||||||
return ocsp.CheckStatus(ocsp.Options{
|
return r.ValidateContext(context.Background(), ValidateContextOptions{
|
||||||
CertChain: certChain,
|
CertChain: certChain,
|
||||||
SigningTime: signingTime,
|
AuthenticSigningTime: signingTime,
|
||||||
HTTPClient: r.httpClient,
|
|
||||||
})
|
})
|
||||||
// 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,14 +14,23 @@
|
||||||
package revocation
|
package revocation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"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/revocation/result"
|
||||||
"github.com/notaryproject/notation-core-go/testhelper"
|
"github.com/notaryproject/notation-core-go/testhelper"
|
||||||
"golang.org/x/crypto/ocsp"
|
"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)
|
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{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultOK, server, nil),
|
result.NewServerResult(result.ResultOK, server, nil),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,11 +107,34 @@ func TestNew(t *testing.T) {
|
||||||
revR, ok := r.(*revocation)
|
revR, ok := r.(*revocation)
|
||||||
if !ok {
|
if !ok {
|
||||||
t.Error("Expected New to create an object matching the internal revocation struct")
|
t.Error("Expected New to create an object matching the internal revocation struct")
|
||||||
} else if revR.httpClient != client {
|
} else if revR.ocspHTTPClient != client {
|
||||||
t.Errorf("Expected New to set client to %v, but it was set to %v", client, revR.httpClient)
|
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) {
|
func TestCheckRevocationStatusForSingleCert(t *testing.T) {
|
||||||
revokableCertTuple := testhelper.GetRevokableRSALeafCertificate()
|
revokableCertTuple := testhelper.GetRevokableRSALeafCertificate()
|
||||||
revokableIssuerTuple := testhelper.GetRSARootCertificate()
|
revokableIssuerTuple := testhelper.GetRSARootCertificate()
|
||||||
|
@ -136,6 +172,7 @@ func TestCheckRevocationStatusForSingleCert(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultUnknown, revokableChain[0].OCSPServer[0], revocationocsp.UnknownStatusError{}),
|
result.NewServerResult(result.ResultUnknown, revokableChain[0].OCSPServer[0], revocationocsp.UnknownStatusError{}),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getRootCertResult(),
|
getRootCertResult(),
|
||||||
}
|
}
|
||||||
|
@ -158,6 +195,7 @@ func TestCheckRevocationStatusForSingleCert(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultRevoked, revokableChain[0].OCSPServer[0], revocationocsp.RevokedError{}),
|
result.NewServerResult(result.ResultRevoked, revokableChain[0].OCSPServer[0], revocationocsp.RevokedError{}),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getRootCertResult(),
|
getRootCertResult(),
|
||||||
}
|
}
|
||||||
|
@ -280,6 +318,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}),
|
result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getOKCertResult(revokableChain[3].OCSPServer[0]),
|
getOKCertResult(revokableChain[3].OCSPServer[0]),
|
||||||
getOKCertResult(revokableChain[4].OCSPServer[0]),
|
getOKCertResult(revokableChain[4].OCSPServer[0]),
|
||||||
|
@ -307,6 +346,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}),
|
result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getOKCertResult(revokableChain[3].OCSPServer[0]),
|
getOKCertResult(revokableChain[3].OCSPServer[0]),
|
||||||
getOKCertResult(revokableChain[4].OCSPServer[0]),
|
getOKCertResult(revokableChain[4].OCSPServer[0]),
|
||||||
|
@ -334,6 +374,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}),
|
result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getOKCertResult(revokableChain[3].OCSPServer[0]),
|
getOKCertResult(revokableChain[3].OCSPServer[0]),
|
||||||
{
|
{
|
||||||
|
@ -341,6 +382,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], revocationocsp.RevokedError{}),
|
result.NewServerResult(result.ResultRevoked, revokableChain[4].OCSPServer[0], revocationocsp.RevokedError{}),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getRootCertResult(),
|
getRootCertResult(),
|
||||||
}
|
}
|
||||||
|
@ -390,6 +432,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}),
|
result.NewServerResult(result.ResultUnknown, revokableChain[2].OCSPServer[0], revocationocsp.UnknownStatusError{}),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getOKCertResult(revokableChain[3].OCSPServer[0]),
|
getOKCertResult(revokableChain[3].OCSPServer[0]),
|
||||||
getOKCertResult(revokableChain[4].OCSPServer[0]),
|
getOKCertResult(revokableChain[4].OCSPServer[0]),
|
||||||
|
@ -417,6 +460,7 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}),
|
result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getOKCertResult(revokableChain[3].OCSPServer[0]),
|
getOKCertResult(revokableChain[3].OCSPServer[0]),
|
||||||
getOKCertResult(revokableChain[4].OCSPServer[0]),
|
getOKCertResult(revokableChain[4].OCSPServer[0]),
|
||||||
|
@ -450,6 +494,313 @@ func TestCheckRevocationStatusForChain(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultRevoked, revokableChain[2].OCSPServer[0], revocationocsp.RevokedError{}),
|
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[3].OCSPServer[0]),
|
||||||
getOKCertResult(revokableChain[4].OCSPServer[0]),
|
getOKCertResult(revokableChain[4].OCSPServer[0]),
|
||||||
|
@ -472,13 +823,13 @@ func TestCheckRevocationErrors(t *testing.T) {
|
||||||
expiredLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw)
|
expiredLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw)
|
||||||
expiredLeaf.IsCA = false
|
expiredLeaf.IsCA = false
|
||||||
expiredLeaf.KeyUsage = x509.KeyUsageDigitalSignature
|
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}
|
expiredChain := []*x509.Certificate{expiredLeaf, revokableTuples[1].Cert, revokableTuples[2].Cert}
|
||||||
|
|
||||||
noHTTPLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw)
|
noHTTPLeaf, _ := x509.ParseCertificate(revokableTuples[0].Cert.Raw)
|
||||||
noHTTPLeaf.IsCA = false
|
noHTTPLeaf.IsCA = false
|
||||||
noHTTPLeaf.KeyUsage = x509.KeyUsageDigitalSignature
|
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}
|
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")}
|
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{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultUnknown, okChain[0].OCSPServer[0], revocationocsp.TimeoutError{}),
|
result.NewServerResult(result.ResultUnknown, okChain[0].OCSPServer[0], revocationocsp.TimeoutError{}),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Result: result.ResultUnknown,
|
Result: result.ResultUnknown,
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultUnknown, okChain[1].OCSPServer[0], revocationocsp.TimeoutError{}),
|
result.NewServerResult(result.ResultUnknown, okChain[1].OCSPServer[0], revocationocsp.TimeoutError{}),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getRootCertResult(),
|
getRootCertResult(),
|
||||||
}
|
}
|
||||||
|
@ -572,6 +925,7 @@ func TestCheckRevocationErrors(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultUnknown, expiredChain[0].OCSPServer[0], expiredRespErr),
|
result.NewServerResult(result.ResultUnknown, expiredChain[0].OCSPServer[0], expiredRespErr),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getOKCertResult(expiredChain[1].OCSPServer[0]),
|
getOKCertResult(expiredChain[1].OCSPServer[0]),
|
||||||
getRootCertResult(),
|
getRootCertResult(),
|
||||||
|
@ -614,6 +968,7 @@ func TestCheckRevocationErrors(t *testing.T) {
|
||||||
ServerResults: []*result.ServerResult{
|
ServerResults: []*result.ServerResult{
|
||||||
result.NewServerResult(result.ResultUnknown, noHTTPChain[0].OCSPServer[0], noHTTPErr),
|
result.NewServerResult(result.ResultUnknown, noHTTPChain[0].OCSPServer[0], noHTTPErr),
|
||||||
},
|
},
|
||||||
|
RevocationMethod: result.RevocationMethodOCSP,
|
||||||
},
|
},
|
||||||
getOKCertResult(noHTTPChain[1].OCSPServer[0]),
|
getOKCertResult(noHTTPChain[1].OCSPServer[0]),
|
||||||
getRootCertResult(),
|
getRootCertResult(),
|
||||||
|
@ -629,7 +984,7 @@ func TestCheckRevocationInvalidChain(t *testing.T) {
|
||||||
for i, cert := range misorderedIntermediateChain {
|
for i, cert := range misorderedIntermediateChain {
|
||||||
if i != (len(misorderedIntermediateChain) - 1) {
|
if i != (len(misorderedIntermediateChain) - 1) {
|
||||||
// Skip root which won't have an OCSP Server
|
// 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 {
|
for i, cert := range missingIntermediateChain {
|
||||||
if i != (len(missingIntermediateChain) - 1) {
|
if i != (len(missingIntermediateChain) - 1) {
|
||||||
// Skip root which won't have an OCSP Server
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,112 +14,44 @@
|
||||||
package signature
|
package signature
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"fmt"
|
|
||||||
|
"github.com/notaryproject/notation-core-go/internal/algorithm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Algorithm defines the signature algorithm.
|
// Algorithm defines the signature algorithm.
|
||||||
type Algorithm int
|
type Algorithm = algorithm.Algorithm
|
||||||
|
|
||||||
// Signature algorithms supported by this library.
|
// Signature algorithms supported by this library.
|
||||||
//
|
//
|
||||||
// Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
// Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
|
||||||
const (
|
const (
|
||||||
AlgorithmPS256 Algorithm = 1 + iota // RSASSA-PSS with SHA-256
|
AlgorithmPS256 = algorithm.AlgorithmPS256 // RSASSA-PSS with SHA-256
|
||||||
AlgorithmPS384 // RSASSA-PSS with SHA-384
|
AlgorithmPS384 = algorithm.AlgorithmPS384 // RSASSA-PSS with SHA-384
|
||||||
AlgorithmPS512 // RSASSA-PSS with SHA-512
|
AlgorithmPS512 = algorithm.AlgorithmPS512 // RSASSA-PSS with SHA-512
|
||||||
AlgorithmES256 // ECDSA on secp256r1 with SHA-256
|
AlgorithmES256 = algorithm.AlgorithmES256 // ECDSA on secp256r1 with SHA-256
|
||||||
AlgorithmES384 // ECDSA on secp384r1 with SHA-384
|
AlgorithmES384 = algorithm.AlgorithmES384 // ECDSA on secp384r1 with SHA-384
|
||||||
AlgorithmES512 // ECDSA on secp521r1 with SHA-512
|
AlgorithmES512 = algorithm.AlgorithmES512 // ECDSA on secp521r1 with SHA-512
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeyType defines the key type.
|
// KeyType defines the key type.
|
||||||
type KeyType int
|
type KeyType = algorithm.KeyType
|
||||||
|
|
||||||
const (
|
const (
|
||||||
KeyTypeRSA KeyType = 1 + iota // KeyType RSA
|
KeyTypeRSA = algorithm.KeyTypeRSA // KeyType RSA
|
||||||
KeyTypeEC // KeyType EC
|
KeyTypeEC = algorithm.KeyTypeEC // KeyType EC
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeySpec defines a key type and size.
|
// KeySpec defines a key type and size.
|
||||||
type KeySpec struct {
|
type KeySpec = algorithm.KeySpec
|
||||||
// KeyType is the type of the key.
|
|
||||||
Type KeyType
|
|
||||||
|
|
||||||
// KeySize is the size of the key in bits.
|
|
||||||
Size int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash returns the hash function of the algorithm.
|
|
||||||
func (alg Algorithm) Hash() crypto.Hash {
|
|
||||||
switch alg {
|
|
||||||
case AlgorithmPS256, AlgorithmES256:
|
|
||||||
return crypto.SHA256
|
|
||||||
case AlgorithmPS384, AlgorithmES384:
|
|
||||||
return crypto.SHA384
|
|
||||||
case AlgorithmPS512, AlgorithmES512:
|
|
||||||
return crypto.SHA512
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExtractKeySpec extracts KeySpec from the signing certificate.
|
// ExtractKeySpec extracts KeySpec from the signing certificate.
|
||||||
func ExtractKeySpec(signingCert *x509.Certificate) (KeySpec, error) {
|
func ExtractKeySpec(signingCert *x509.Certificate) (KeySpec, error) {
|
||||||
switch key := signingCert.PublicKey.(type) {
|
ks, err := algorithm.ExtractKeySpec(signingCert)
|
||||||
case *rsa.PublicKey:
|
if err != nil {
|
||||||
switch bitSize := key.Size() << 3; bitSize {
|
|
||||||
case 2048, 3072, 4096:
|
|
||||||
return KeySpec{
|
|
||||||
Type: KeyTypeRSA,
|
|
||||||
Size: bitSize,
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return KeySpec{}, &UnsupportedSigningKeyError{
|
return KeySpec{}, &UnsupportedSigningKeyError{
|
||||||
Msg: fmt.Sprintf("rsa key size %d bits is not supported", bitSize),
|
Msg: err.Error(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case *ecdsa.PublicKey:
|
return ks, nil
|
||||||
switch bitSize := key.Curve.Params().BitSize; bitSize {
|
|
||||||
case 256, 384, 521:
|
|
||||||
return KeySpec{
|
|
||||||
Type: KeyTypeEC,
|
|
||||||
Size: bitSize,
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return KeySpec{}, &UnsupportedSigningKeyError{
|
|
||||||
Msg: fmt.Sprintf("ecdsa key size %d bits is not supported", bitSize),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return KeySpec{}, &UnsupportedSigningKeyError{
|
|
||||||
Msg: "unsupported public key type",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SignatureAlgorithm returns the signing algorithm associated with the KeySpec.
|
|
||||||
func (k KeySpec) SignatureAlgorithm() Algorithm {
|
|
||||||
switch k.Type {
|
|
||||||
case KeyTypeEC:
|
|
||||||
switch k.Size {
|
|
||||||
case 256:
|
|
||||||
return AlgorithmES256
|
|
||||||
case 384:
|
|
||||||
return AlgorithmES384
|
|
||||||
case 521:
|
|
||||||
return AlgorithmES512
|
|
||||||
}
|
|
||||||
case KeyTypeRSA:
|
|
||||||
switch k.Size {
|
|
||||||
case 2048:
|
|
||||||
return AlgorithmPS256
|
|
||||||
case 3072:
|
|
||||||
return AlgorithmPS384
|
|
||||||
case 4096:
|
|
||||||
return AlgorithmPS512
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
package signature
|
package signature
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/elliptic"
|
"crypto/elliptic"
|
||||||
|
@ -28,59 +27,6 @@ import (
|
||||||
"github.com/notaryproject/notation-core-go/testhelper"
|
"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) {
|
func TestExtractKeySpec(t *testing.T) {
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
name string
|
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -57,7 +57,7 @@ func TestConformance(t *testing.T) {
|
||||||
|
|
||||||
// testSign does conformance check on COSE_Sign1_Tagged
|
// testSign does conformance check on COSE_Sign1_Tagged
|
||||||
func testSign(t *testing.T, sign1 *sign1) {
|
func testSign(t *testing.T, sign1 *sign1) {
|
||||||
signRequest, err := getSignReq(sign1)
|
signRequest, err := getSignReq()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("getSignReq() failed. Error = %s", err)
|
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
|
// testVerify does conformance check by decoding COSE_Sign1_Tagged object
|
||||||
// into Sign1Message
|
// into Sign1Message
|
||||||
func testVerify(t *testing.T, sign1 *sign1) {
|
func testVerify(t *testing.T, sign1 *sign1) {
|
||||||
signRequest, err := getSignReq(sign1)
|
signRequest, err := getSignReq()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("getSignReq() failed. Error = %s", err)
|
t.Fatalf("getSignReq() failed. Error = %s", err)
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ func testVerify(t *testing.T, sign1 *sign1) {
|
||||||
verifySignerInfo(&content.SignerInfo, signRequest, t)
|
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}
|
certs := []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert}
|
||||||
signer, err := signature.NewLocalSigner(certs, testhelper.GetRSALeafCertificate().PrivateKey)
|
signer, err := signature.NewLocalSigner(certs, testhelper.GetRSALeafCertificate().PrivateKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -24,8 +24,10 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fxamacker/cbor/v2"
|
"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"
|
||||||
"github.com/notaryproject/notation-core-go/signature/internal/base"
|
"github.com/notaryproject/notation-core-go/signature/internal/base"
|
||||||
|
"github.com/notaryproject/tspclient-go"
|
||||||
"github.com/veraison/go-cose"
|
"github.com/veraison/go-cose"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -76,7 +78,7 @@ const (
|
||||||
// Unprotected Headers
|
// Unprotected Headers
|
||||||
// https://github.com/notaryproject/notaryproject/blob/cose-envelope/signature-envelope-cose.md
|
// https://github.com/notaryproject/notaryproject/blob/cose-envelope/signature-envelope-cose.md
|
||||||
const (
|
const (
|
||||||
headerLabelTimeStampSignature = "io.cncf.notary.timestampSignature"
|
headerLabelTimestampSignature = "io.cncf.notary.timestampSignature"
|
||||||
headerLabelSigningAgent = "io.cncf.notary.signingAgent"
|
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()}
|
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)
|
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
|
// encode Sign1Message into COSE_Sign1_Tagged object
|
||||||
encoded, err := msg.MarshalCBOR()
|
encoded, err := msg.MarshalCBOR()
|
||||||
|
@ -368,7 +386,10 @@ func (e *envelope) signerInfo() (*signature.SignerInfo, error) {
|
||||||
signerInfo.UnsignedAttributes.SigningAgent = h
|
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
|
return &signerInfo, nil
|
||||||
}
|
}
|
||||||
|
@ -701,3 +722,17 @@ func generateRawProtectedCBORMap(rawProtected cbor.RawMessage) (map[any]cbor.Raw
|
||||||
|
|
||||||
return headerMap, nil
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
package cose
|
package cose
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -25,11 +26,15 @@ import (
|
||||||
"github.com/notaryproject/notation-core-go/signature"
|
"github.com/notaryproject/notation-core-go/signature"
|
||||||
"github.com/notaryproject/notation-core-go/signature/internal/signaturetest"
|
"github.com/notaryproject/notation-core-go/signature/internal/signaturetest"
|
||||||
"github.com/notaryproject/notation-core-go/testhelper"
|
"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"
|
"github.com/veraison/go-cose"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
payloadString = "{\"targetArtifact\":{\"mediaType\":\"application/vnd.oci.image.manifest.v1+json\",\"digest\":\"sha256:73c803930ea3ba1e54bc25c2bdc53edd0284c62ed651fe7b00369da519a3c333\",\"size\":16724,\"annotations\":{\"io.wabbit-networks.buildId\":\"123\"}}}"
|
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 (
|
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) {
|
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.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, ×tampErr) {
|
||||||
|
t.Fatal("expected signature.TimestampError")
|
||||||
|
}
|
||||||
|
if encoded != nil {
|
||||||
|
t.Fatal("expected nil signature envelope")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVerifyErrors(t *testing.T) {
|
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) {
|
func newSignRequest(signingScheme string, keyType signature.KeyType, size int) (*signature.SignRequest, error) {
|
||||||
signer, err := signaturetest.GetTestLocalSigner(keyType, size)
|
signer, err := signaturetest.GetTestLocalSigner(keyType, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -993,3 +1099,9 @@ func generateTestRawMessage(raw cbor.RawMessage, label string, unmarshalError bo
|
||||||
|
|
||||||
return resRaw
|
return resRaw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type dummyTimestamper tspclient.Timestamp
|
||||||
|
|
||||||
|
func (dts *dummyTimestamper) Timestamp(context.Context, *tspclient.Request) (*tspclient.Response, error) {
|
||||||
|
return nil, errors.New("failed to timestamp")
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -67,7 +67,7 @@ type SignatureAuthenticityError struct{}
|
||||||
|
|
||||||
// Error returns the default error message.
|
// Error returns the default error message.
|
||||||
func (e *SignatureAuthenticityError) Error() string {
|
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.
|
// UnsupportedSigningKeyError is used when a signing key is not supported.
|
||||||
|
@ -142,3 +142,28 @@ type DuplicateKeyError struct {
|
||||||
func (e *DuplicateKeyError) Error() string {
|
func (e *DuplicateKeyError) Error() string {
|
||||||
return fmt.Sprintf("repeated key: %q exists.", e.Key)
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -162,7 +162,7 @@ func TestSignatureEnvelopeNotFoundError(t *testing.T) {
|
||||||
|
|
||||||
func TestSignatureAuthenticityError(t *testing.T) {
|
func TestSignatureAuthenticityError(t *testing.T) {
|
||||||
err := &SignatureAuthenticityError{}
|
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 {
|
if err.Error() != expectMsg {
|
||||||
t.Errorf("Expected %v but got %v", expectMsg, err.Error())
|
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())
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -53,7 +53,6 @@ func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := validateCertificateChain(
|
if err := validateCertificateChain(
|
||||||
content.SignerInfo.CertificateChain,
|
content.SignerInfo.CertificateChain,
|
||||||
&content.SignerInfo.SignedAttributes.SigningTime,
|
&content.SignerInfo.SignedAttributes.SigningTime,
|
||||||
|
@ -62,7 +61,9 @@ func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store the raw signature
|
||||||
e.Raw = raw
|
e.Raw = raw
|
||||||
|
|
||||||
return e.Raw, nil
|
return e.Raw, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,10 +22,12 @@ import (
|
||||||
|
|
||||||
"github.com/notaryproject/notation-core-go/signature"
|
"github.com/notaryproject/notation-core-go/signature"
|
||||||
"github.com/notaryproject/notation-core-go/testhelper"
|
"github.com/notaryproject/notation-core-go/testhelper"
|
||||||
|
"github.com/notaryproject/tspclient-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errMsg = "error msg"
|
errMsg = "error msg"
|
||||||
|
invalidTimestamper tspclient.Timestamper
|
||||||
invalidSigningAgent = "test/1"
|
invalidSigningAgent = "test/1"
|
||||||
validSigningAgent = "test/0"
|
validSigningAgent = "test/0"
|
||||||
invalidContentType = "text/plain"
|
invalidContentType = "text/plain"
|
||||||
|
@ -97,11 +99,33 @@ var (
|
||||||
},
|
},
|
||||||
SigningAgent: invalidSigningAgent,
|
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() {
|
func init() {
|
||||||
time08_02, _ = time.Parse(timeLayout, "2020-08-02")
|
time08_02, _ = time.Parse(timeLayout, "2020-08-02")
|
||||||
time08_03, _ = time.Parse(timeLayout, "2020-08-03")
|
time08_03, _ = time.Parse(timeLayout, "2020-08-03")
|
||||||
|
invalidTimestamper, _ = tspclient.NewHTTPTimestamper(nil, "invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock an internal envelope that implements signature.Envelope.
|
// Mock an internal envelope that implements signature.Envelope.
|
||||||
|
@ -109,6 +133,7 @@ type mockEnvelope struct {
|
||||||
payload *signature.Payload
|
payload *signature.Payload
|
||||||
signerInfo *signature.SignerInfo
|
signerInfo *signature.SignerInfo
|
||||||
content *signature.EnvelopeContent
|
content *signature.EnvelopeContent
|
||||||
|
failTimestamp bool
|
||||||
failVerify bool
|
failVerify bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +143,9 @@ func (e mockEnvelope) Sign(req *signature.SignRequest) ([]byte, error) {
|
||||||
case invalidSigningAgent:
|
case invalidSigningAgent:
|
||||||
return nil, errors.New(errMsg)
|
return nil, errors.New(errMsg)
|
||||||
case validSigningAgent:
|
case validSigningAgent:
|
||||||
|
if e.failTimestamp {
|
||||||
|
return validBytes, &signature.TimestampError{}
|
||||||
|
}
|
||||||
return validBytes, nil
|
return validBytes, nil
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
@ -234,6 +262,21 @@ func TestSign(t *testing.T) {
|
||||||
expect: validBytes,
|
expect: validBytes,
|
||||||
expectErr: false,
|
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 {
|
for _, tt := range tests {
|
||||||
|
@ -246,6 +289,17 @@ func TestSign(t *testing.T) {
|
||||||
if !reflect.DeepEqual(sig, tt.expect) {
|
if !reflect.DeepEqual(sig, tt.expect) {
|
||||||
t.Errorf("expect %+v, got %+v", tt.expect, sig)
|
t.Errorf("expect %+v, got %+v", tt.expect, sig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tt.name == "failed to timestamp" {
|
||||||
|
var timestampErr *signature.TimestampError
|
||||||
|
if !errors.As(err, ×tampErr) {
|
||||||
|
t.Fatal("expecting error to be signature.TimestampError")
|
||||||
|
}
|
||||||
|
expectedErrMsg := "timestamp error"
|
||||||
|
if timestampErr.Error() != expectedErrMsg {
|
||||||
|
t.Fatalf("expected error %s, but got %v", expectedErrMsg, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/golang-jwt/jwt/v4"
|
"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"
|
||||||
"github.com/notaryproject/notation-core-go/signature/internal/base"
|
"github.com/notaryproject/notation-core-go/signature/internal/base"
|
||||||
|
"github.com/notaryproject/tspclient-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MediaTypeEnvelope defines the media type name of JWS envelope.
|
// 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()}
|
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)
|
encoded, err := json.Marshal(env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &signature.InvalidSignatureError{Msg: err.Error()}
|
return nil, &signature.InvalidSignatureError{Msg: err.Error()}
|
||||||
}
|
}
|
||||||
e.base = env
|
e.base = env
|
||||||
|
|
||||||
return encoded, nil
|
return encoded, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -220,3 +228,34 @@ func sign(payload jwt.MapClaims, headers map[string]interface{}, method signingM
|
||||||
}
|
}
|
||||||
return compact, certs, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
package jws
|
package jws
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/ecdsa"
|
"crypto/ecdsa"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
@ -32,8 +33,12 @@ import (
|
||||||
"github.com/notaryproject/notation-core-go/signature"
|
"github.com/notaryproject/notation-core-go/signature"
|
||||||
"github.com/notaryproject/notation-core-go/signature/internal/signaturetest"
|
"github.com/notaryproject/notation-core-go/signature/internal/signaturetest"
|
||||||
"github.com/notaryproject/notation-core-go/testhelper"
|
"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
|
// remoteMockSigner is used to mock remote signer
|
||||||
type remoteMockSigner struct {
|
type remoteMockSigner struct {
|
||||||
privateKey crypto.PrivateKey
|
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) {
|
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) {
|
t.Run("extended attribute conflict with protected header keys", func(t *testing.T) {
|
||||||
_, err := getEncodedMessage(signature.SigningSchemeX509, true, extSignedAttrRepeated)
|
_, err := getEncodedMessage(signature.SigningSchemeX509, true, extSignedAttrRepeated)
|
||||||
checkErrorEqual(t, "attribute key:cty repeated", err.Error())
|
checkErrorEqual(t, "attribute key:cty repeated", err.Error())
|
||||||
|
@ -253,6 +258,29 @@ func TestSignFailed(t *testing.T) {
|
||||||
_, err = e.Sign(signReq)
|
_, err = e.Sign(signReq)
|
||||||
checkErrorEqual(t, `signature algorithm "#0" is not supported`, err.Error())
|
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, ×tampErr) {
|
||||||
|
t.Fatal("expected signature.TimestampError")
|
||||||
|
}
|
||||||
|
if encoded != nil {
|
||||||
|
t.Fatal("expected nil signature envelope")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSigningScheme(t *testing.T) {
|
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) {
|
func TestVerify(t *testing.T) {
|
||||||
t.Run("break json format", func(t *testing.T) {
|
t.Run("break json format", func(t *testing.T) {
|
||||||
encoded, err := getEncodedMessage(signature.SigningSchemeX509, true, extSignedAttr)
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -73,7 +73,7 @@ type jwsProtectedHeader struct {
|
||||||
|
|
||||||
// jwsUnprotectedHeader contains the set of unprotected headers.
|
// jwsUnprotectedHeader contains the set of unprotected headers.
|
||||||
type jwsUnprotectedHeader struct {
|
type jwsUnprotectedHeader struct {
|
||||||
// RFC3161 time stamp token Base64-encoded.
|
// RFC3161 timestamp token Base64-encoded.
|
||||||
TimestampSignature []byte `json:"io.cncf.notary.timestampSignature,omitempty"`
|
TimestampSignature []byte `json:"io.cncf.notary.timestampSignature,omitempty"`
|
||||||
|
|
||||||
// List of X.509 Base64-DER-encoded certificates
|
// List of X.509 Base64-DER-encoded certificates
|
||||||
|
|
|
@ -122,22 +122,21 @@ func (s *localSigner) PrivateKey() crypto.PrivateKey {
|
||||||
return s.key
|
return s.key
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyAuthenticity verifies the certificate chain in the given SignerInfo
|
// VerifyAuthenticity iterates the certificate chain in signerInfo, for each
|
||||||
// with one of the trusted certificates and returns a certificate that matches
|
// certificate in the chain, it checks if the certificate matches with one of
|
||||||
// with one of the certificates in the SignerInfo.
|
// 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
|
// 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) {
|
func VerifyAuthenticity(signerInfo *SignerInfo, trustedCerts []*x509.Certificate) (*x509.Certificate, error) {
|
||||||
if len(trustedCerts) == 0 {
|
if len(trustedCerts) == 0 {
|
||||||
return nil, &InvalidArgumentError{Param: "trustedCerts"}
|
return nil, &InvalidArgumentError{Param: "trustedCerts"}
|
||||||
}
|
}
|
||||||
|
|
||||||
if signerInfo == nil {
|
if signerInfo == nil {
|
||||||
return nil, &InvalidArgumentError{Param: "signerInfo"}
|
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) {
|
if trust.Equal(cert) {
|
||||||
return trust, nil
|
return trust, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,14 @@
|
||||||
package signature
|
package signature
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/notaryproject/notation-core-go/revocation"
|
||||||
|
"github.com/notaryproject/tspclient-go"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SignatureMediaType list the supported media-type for signatures.
|
// 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 defines the Notary Project Signing Scheme used by the signature.
|
||||||
SigningScheme SigningScheme
|
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
|
// 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")
|
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) {
|
func (signerInfo *SignerInfo) AuthenticSigningTime() (time.Time, error) {
|
||||||
switch signerInfo.SignedAttributes.SigningScheme {
|
switch signingScheme := signerInfo.SignedAttributes.SigningScheme; signingScheme {
|
||||||
case SigningSchemeX509SigningAuthority:
|
case SigningSchemeX509SigningAuthority:
|
||||||
return signerInfo.SignedAttributes.SigningTime, nil
|
signingTime := signerInfo.SignedAttributes.SigningTime
|
||||||
case SigningSchemeX509:
|
if signingTime.IsZero() {
|
||||||
if len(signerInfo.UnsignedAttributes.TimestampSignature) > 0 {
|
return time.Time{}, fmt.Errorf("authentic signing time must be present under signing scheme %q", signingScheme)
|
||||||
// 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")
|
|
||||||
}
|
}
|
||||||
|
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")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,12 +22,15 @@ import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/big"
|
"math/big"
|
||||||
mrand "math/rand"
|
mrand "math/rand"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/notaryproject/notation-core-go/internal/oid"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -72,16 +75,46 @@ func GetRevokableRSALeafCertificate() RSACertTuple {
|
||||||
return revokableRSALeaf
|
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
|
// GetRevokableRSAChain returns a chain of certificates that specify a local OCSP server signed using RSA algorithm
|
||||||
func GetRevokableRSAChain(size int) []RSACertTuple {
|
func GetRevokableRSAChain(size int) []RSACertTuple {
|
||||||
setupCertificates()
|
setupCertificates()
|
||||||
chain := make([]RSACertTuple, size)
|
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-- {
|
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 {
|
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
|
return chain
|
||||||
}
|
}
|
||||||
|
@ -148,42 +181,56 @@ func getRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple {
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRevokableRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple {
|
func getRevokableRSACertTuple(cn string, issuer *RSACertTuple) RSACertTuple {
|
||||||
template := getCertTemplate(issuer == nil, true, cn)
|
template := getCertTemplate(issuer == nil, true, false, cn)
|
||||||
template.OCSPServer = []string{"http://example.com/ocsp"}
|
template.OCSPServer = []string{"http://localhost.test/ocsp"}
|
||||||
return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer)
|
return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int) RSACertTuple {
|
func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int, enabledOCSP, enabledCRL bool) RSACertTuple {
|
||||||
template := getCertTemplate(previous == nil, true, cn)
|
template := getCertTemplate(previous == nil, true, false, cn)
|
||||||
template.BasicConstraintsValid = true
|
template.BasicConstraintsValid = true
|
||||||
template.IsCA = true
|
template.IsCA = true
|
||||||
template.KeyUsage = x509.KeyUsageCertSign
|
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)
|
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)
|
pk, _ := rsa.GenerateKey(rand.Reader, 3072)
|
||||||
template := getCertTemplate(true, true, cn)
|
template := getCertTemplate(true, true, false, cn)
|
||||||
template.BasicConstraintsValid = true
|
template.BasicConstraintsValid = true
|
||||||
template.IsCA = true
|
template.IsCA = true
|
||||||
template.KeyUsage = x509.KeyUsageCertSign
|
template.KeyUsage = x509.KeyUsageCertSign
|
||||||
|
if enabledCRL {
|
||||||
|
template.KeyUsage |= x509.KeyUsageCRLSign
|
||||||
|
}
|
||||||
template.MaxPathLen = pathLen
|
template.MaxPathLen = pathLen
|
||||||
return getRSACertTupleWithTemplate(template, pk, nil)
|
return getRSACertTupleWithTemplate(template, pk, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int) RSACertTuple {
|
func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index int, codesign, timestamp, enabledOCSP, enabledCRL bool) RSACertTuple {
|
||||||
template := getCertTemplate(false, true, cn)
|
template := getCertTemplate(false, codesign, timestamp, cn)
|
||||||
template.BasicConstraintsValid = true
|
template.BasicConstraintsValid = true
|
||||||
template.IsCA = false
|
template.IsCA = false
|
||||||
template.KeyUsage = x509.KeyUsageDigitalSignature
|
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)
|
return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRSACertWithoutEKUTuple(cn string, issuer *RSACertTuple) RSACertTuple {
|
func getRSACertWithoutEKUTuple(cn string, issuer *RSACertTuple) RSACertTuple {
|
||||||
pk, _ := rsa.GenerateKey(rand.Reader, 3072)
|
pk, _ := rsa.GenerateKey(rand.Reader, 3072)
|
||||||
template := getCertTemplate(issuer == nil, false, cn)
|
template := getCertTemplate(issuer == nil, false, false, cn)
|
||||||
return getRSACertTupleWithTemplate(template, pk, issuer)
|
return getRSACertTupleWithTemplate(template, pk, issuer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,18 +247,18 @@ func getECCertTuple(cn string, issuer *ECCertTuple) ECCertTuple {
|
||||||
func GetRSASelfSignedSigningCertTuple(cn string) RSACertTuple {
|
func GetRSASelfSignedSigningCertTuple(cn string) RSACertTuple {
|
||||||
// Even though we are creating self-signed root, we are using false for 'isRoot' to not
|
// 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.
|
// 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)
|
privKey, _ := rsa.GenerateKey(rand.Reader, 3072)
|
||||||
return getRSACertTupleWithTemplate(template, privKey, nil)
|
return getRSACertTupleWithTemplate(template, privKey, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRSACertTupleWithPK(privKey *rsa.PrivateKey, cn string, issuer *RSACertTuple) RSACertTuple {
|
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)
|
return getRSACertTupleWithTemplate(template, privKey, issuer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRSASelfSignedCertTupleWithPK(privKey *rsa.PrivateKey, cn string) RSACertTuple {
|
func GetRSASelfSignedCertTupleWithPK(privKey *rsa.PrivateKey, cn string) RSACertTuple {
|
||||||
template := getCertTemplate(false, true, cn)
|
template := getCertTemplate(false, true, false, cn)
|
||||||
return getRSACertTupleWithTemplate(template, privKey, nil)
|
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 {
|
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
|
var certBytes []byte
|
||||||
if issuer != nil {
|
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{
|
template := &x509.Certificate{
|
||||||
Subject: pkix.Name{
|
Subject: pkix.Name{
|
||||||
Organization: []string{"Notary"},
|
Organization: []string{"Notary"},
|
||||||
|
@ -262,6 +309,15 @@ func getCertTemplate(isRoot bool, setCodeSignEKU bool, cn string) *x509.Certific
|
||||||
|
|
||||||
if setCodeSignEKU {
|
if setCodeSignEKU {
|
||||||
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}
|
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 {
|
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.SerialNumber = big.NewInt(int64(mrand.Intn(200)))
|
||||||
template.NotAfter = time.Now().AddDate(0, 0, 1)
|
template.NotAfter = time.Now().AddDate(0, 0, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return template
|
return template
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
|
@ -17,10 +17,13 @@ import (
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/asn1"
|
"errors"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/notaryproject/notation-core-go/internal/oid"
|
||||||
"github.com/notaryproject/notation-core-go/testhelper"
|
"github.com/notaryproject/notation-core-go/testhelper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -100,25 +103,6 @@ var codeSigningLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
"cwtsQn/iENuvFcfRHcFhvRjEFrIP+Ugx\n" +
|
"cwtsQn/iENuvFcfRHcFhvRjEFrIP+Ugx\n" +
|
||||||
"-----END CERTIFICATE-----"
|
"-----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" +
|
var unrelatedCertPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
"MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" +
|
"MIIC6jCCAdKgAwIBAgIJAJOlT2AUbsZiMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" +
|
||||||
"BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" +
|
"BAMMBUhlbGxvMCAXDTIyMDYyNTAzMTcyM1oYDzIxMjIwNjAxMDMxNzIzWjAQMQ4w\n" +
|
||||||
|
@ -183,7 +167,6 @@ var rootCert = parseCertificateFromString(rootCertPem)
|
||||||
var intermediateCert1 = parseCertificateFromString(intermediateCertPem1)
|
var intermediateCert1 = parseCertificateFromString(intermediateCertPem1)
|
||||||
var intermediateCert2 = parseCertificateFromString(intermediateCertPem2)
|
var intermediateCert2 = parseCertificateFromString(intermediateCertPem2)
|
||||||
var codeSigningCert = parseCertificateFromString(codeSigningLeafPem)
|
var codeSigningCert = parseCertificateFromString(codeSigningLeafPem)
|
||||||
var timeStampingCert = parseCertificateFromString(timeStampingLeafPem)
|
|
||||||
var unrelatedCert = parseCertificateFromString(unrelatedCertPem)
|
var unrelatedCert = parseCertificateFromString(unrelatedCertPem)
|
||||||
var intermediateCertInvalidPathLen = parseCertificateFromString(intermediateCertInvalidPathLenPem)
|
var intermediateCertInvalidPathLen = parseCertificateFromString(intermediateCertInvalidPathLenPem)
|
||||||
var codeSigningLeafInvalidPathLen = parseCertificateFromString(codeSigningLeafInvalidPathLenPem)
|
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) {
|
func TestFailEmptyChain(t *testing.T) {
|
||||||
signingTime := time.Now()
|
signingTime := time.Now()
|
||||||
err := ValidateCodeSigningCertChain(nil, &signingTime)
|
err := ValidateCodeSigningCertChain(nil, &signingTime)
|
||||||
|
@ -227,12 +201,71 @@ func TestFailEmptyChain(t *testing.T) {
|
||||||
assertErrorEqual("certificate chain must contain at least one certificate", err, 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) {
|
func TestFailInvalidSigningTime(t *testing.T) {
|
||||||
certChain := []*x509.Certificate{codeSigningCert, intermediateCert2, intermediateCert1, rootCert}
|
certChain := []*x509.Certificate{codeSigningCert, intermediateCert2, intermediateCert1, rootCert}
|
||||||
|
|
||||||
st := time.Unix(1625690922, 0)
|
st := time.Unix(1625690922, 0)
|
||||||
err := ValidateCodeSigningCertChain(certChain, &st)
|
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) {
|
func TestFailChainNotEndingInRoot(t *testing.T) {
|
||||||
|
@ -310,13 +343,13 @@ func TestInvalidSelfSignedSigningCertificate(t *testing.T) {
|
||||||
// ---------------- CA Validations ----------------
|
// ---------------- CA Validations ----------------
|
||||||
|
|
||||||
func TestValidCa(t *testing.T) {
|
func TestValidCa(t *testing.T) {
|
||||||
if err := validateCACertificate(rootCert, 2); err != nil {
|
if err := validateCodeSigningCACertificate(rootCert, 2); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFailInvalidPathLenCa(t *testing.T) {
|
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)
|
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)
|
var noBasicConstraintsCa = parseCertificateFromString(noBasicConstraintsCaPem)
|
||||||
|
|
||||||
func TestFailNoBasicConstraintsCa(t *testing.T) {
|
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)
|
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)
|
var basicConstraintsNotCa = parseCertificateFromString(basicConstraintsNotCaPem)
|
||||||
|
|
||||||
func TestFailBasicConstraintsNotCa(t *testing.T) {
|
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)
|
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)
|
var kuNotCriticalCertSignCa = parseCertificateFromString(kuNotCriticalCertSignCaPem)
|
||||||
|
|
||||||
func TestFailKuNotCriticalCertSignCa(t *testing.T) {
|
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)
|
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)
|
var kuMissingCa = parseCertificateFromString(kuMissingCaPem)
|
||||||
|
|
||||||
func TestFailKuMissingCa(t *testing.T) {
|
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)
|
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)
|
var kuNotCertSignCa = parseCertificateFromString(kuNotCertSignCaPem)
|
||||||
|
|
||||||
func TestFailKuNotCertSignCa(t *testing.T) {
|
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)
|
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" +
|
var validNoOptionsLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
"MIICtzCCAZ+gAwIBAgIJAL+FUPhO8J8cMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" +
|
"MIICtzCCAZ+gAwIBAgIJAL+FUPhO8J8cMA0GCSqGSIb3DQEBCwUAMBAxDjAMBgNV\n" +
|
||||||
|
@ -462,7 +495,7 @@ var validNoOptionsLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
var validNoOptionsLeaf = parseCertificateFromString(validNoOptionsLeafPem)
|
var validNoOptionsLeaf = parseCertificateFromString(validNoOptionsLeafPem)
|
||||||
|
|
||||||
func TestValidNoOptionsLeaf(t *testing.T) {
|
func TestValidNoOptionsLeaf(t *testing.T) {
|
||||||
if err := validateLeafCertificate(validNoOptionsLeaf, x509.ExtKeyUsageCodeSigning); err != nil {
|
if err := validateCodeSigningLeafCertificate(validNoOptionsLeaf); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -487,7 +520,7 @@ var caTrueLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
var caTrueLeaf = parseCertificateFromString(caTrueLeafPem)
|
var caTrueLeaf = parseCertificateFromString(caTrueLeafPem)
|
||||||
|
|
||||||
func TestFailCaTrueLeaf(t *testing.T) {
|
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)
|
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)
|
var kuNoDigitalSignatureLeaf = parseCertificateFromString(kuNoDigitalSignatureLeafPem)
|
||||||
|
|
||||||
func TestFailKuNoDigitalSignatureLeaf(t *testing.T) {
|
func TestFailKuNoDigitalSignatureLeaf(t *testing.T) {
|
||||||
err := validateLeafCertificate(kuNoDigitalSignatureLeaf, x509.ExtKeyUsageCodeSigning)
|
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)
|
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" +
|
var kuWrongValuesLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
|
@ -535,8 +568,8 @@ var kuWrongValuesLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
var kuWrongValuesLeaf = parseCertificateFromString(kuWrongValuesLeafPem)
|
var kuWrongValuesLeaf = parseCertificateFromString(kuWrongValuesLeafPem)
|
||||||
|
|
||||||
func TestFailKuWrongValuesLeaf(t *testing.T) {
|
func TestFailKuWrongValuesLeaf(t *testing.T) {
|
||||||
err := validateLeafCertificate(kuWrongValuesLeaf, x509.ExtKeyUsageCodeSigning)
|
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)
|
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" +
|
var rsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
|
@ -554,8 +587,8 @@ var rsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
var rsaKeyTooSmallLeaf = parseCertificateFromString(rsaKeyTooSmallLeafPem)
|
var rsaKeyTooSmallLeaf = parseCertificateFromString(rsaKeyTooSmallLeafPem)
|
||||||
|
|
||||||
func TestFailRsaKeyTooSmallLeaf(t *testing.T) {
|
func TestFailRsaKeyTooSmallLeaf(t *testing.T) {
|
||||||
err := validateLeafCertificate(rsaKeyTooSmallLeaf, x509.ExtKeyUsageCodeSigning)
|
err := validateCodeSigningLeafCertificate(rsaKeyTooSmallLeaf)
|
||||||
assertErrorEqual("certificate with subject \"CN=Hello\": rsa public key length must be 2048 bits or higher", err, t)
|
assertErrorEqual("certificate with subject \"CN=Hello\": rsa key size 1024 bits is not supported", err, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ecdsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
var ecdsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
|
@ -570,141 +603,21 @@ var ecdsaKeyTooSmallLeafPem = "-----BEGIN CERTIFICATE-----\n" +
|
||||||
var ecdsaKeyTooSmallLeaf = parseCertificateFromString(ecdsaKeyTooSmallLeafPem)
|
var ecdsaKeyTooSmallLeaf = parseCertificateFromString(ecdsaKeyTooSmallLeafPem)
|
||||||
|
|
||||||
func TestFailEcdsaKeyTooSmallLeaf(t *testing.T) {
|
func TestFailEcdsaKeyTooSmallLeaf(t *testing.T) {
|
||||||
err := validateLeafCertificate(ecdsaKeyTooSmallLeaf, x509.ExtKeyUsageCodeSigning)
|
err := validateCodeSigningLeafCertificate(ecdsaKeyTooSmallLeaf)
|
||||||
assertErrorEqual("certificate with subject \"CN=Hello\": ecdsa public key length must be 256 bits or higher", err, t)
|
assertErrorEqual("certificate with subject \"CN=Hello\": ecdsa key size 224 bits is not supported", err, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Code-Signing Leaf Validations ----------------
|
// ---------------- Code-Signing Leaf Validations ----------------
|
||||||
|
|
||||||
func TestValidFullOptionsCodeLeaf(t *testing.T) {
|
func TestValidFullOptionsCodeLeaf(t *testing.T) {
|
||||||
if err := validateLeafCertificate(codeSigningCert, x509.ExtKeyUsageCodeSigning); err != nil {
|
if err := validateCodeSigningLeafCertificate(codeSigningCert); err != nil {
|
||||||
t.Fatal(err)
|
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) {
|
func TestValidateLeafKeyUsage(t *testing.T) {
|
||||||
extensions := []pkix.Extension{{
|
extensions := []pkix.Extension{{
|
||||||
Id: asn1.ObjectIdentifier{2, 5, 29, 15}, // OID for KeyUsage
|
Id: oid.KeyUsage,
|
||||||
Critical: true,
|
Critical: true,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
@ -729,7 +642,7 @@ func TestValidateLeafKeyUsage(t *testing.T) {
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageContentCommitment,
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageContentCommitment,
|
||||||
Extensions: extensions,
|
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",
|
name: "Missing DigitalSignature usage",
|
||||||
|
@ -738,7 +651,7 @@ func TestValidateLeafKeyUsage(t *testing.T) {
|
||||||
KeyUsage: x509.KeyUsageCertSign,
|
KeyUsage: x509.KeyUsageCertSign,
|
||||||
Extensions: extensions,
|
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",
|
name: "Invalid KeyEncipherment usage",
|
||||||
|
@ -747,7 +660,7 @@ func TestValidateLeafKeyUsage(t *testing.T) {
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
Extensions: extensions,
|
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",
|
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,
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageDataEncipherment | x509.KeyUsageKeyAgreement | x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageEncipherOnly | x509.KeyUsageDecipherOnly | x509.KeyUsageEncipherOnly | x509.KeyUsageDecipherOnly,
|
||||||
Extensions: extensions,
|
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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue