Compare commits

...

27 Commits

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

Test:
- added more test cases

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

---------

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

---------

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

---------

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

Resolves #126

---------

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

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

---------

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

Signed-off-by: Patrick Zheng <patrickzheng@microsoft.com>
2024-10-11 09:22:03 -07:00
53 changed files with 2244 additions and 608 deletions

View File

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

View File

@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
go-version: ["1.22", "1.23"] go-version: ["1.23", "1.24"]
fail-fast: true fail-fast: true
steps: steps:
- name: Checkout - name: Checkout
@ -37,6 +37,6 @@ jobs:
- 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@v4 uses: codecov/codecov-action@v5
env: env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -26,7 +26,7 @@ jobs:
security-events: write security-events: write
strategy: strategy:
matrix: matrix:
go-version: ["1.22", "1.23"] go-version: ["1.23", "1.24"]
fail-fast: false fail-fast: false
steps: steps:
- name: Checkout repository - name: Checkout repository

View File

@ -23,13 +23,13 @@ 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@cd7b195c51fd3d6ad52afceb760719ddc6b3ee91 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@cd7b195c51fd3d6ad52afceb760719ddc6b3ee91 uses: apache/skywalking-eyes/dependency@5c5b974209f0de5d905f37deb69369068ebfc15c
with: with:
config: .github/licenserc.yml config: .github/licenserc.yml
flags: flags:

View File

@ -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 @niazfk @priteshbandi @rgnote @shizhMSFT @toddysm @Two-Hearts @vaninrao10 @yizha1 * @gokarnm @niazfk @priteshbandi @rgnote @shizhMSFT @toddysm @Two-Hearts @vaninrao10 @yizha1

View File

@ -10,11 +10,13 @@ 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)
Rakesh Gariganti <garigant@amazon.com> (@rgnote) Rakesh Gariganti <garigant@amazon.com> (@rgnote)
# Emeritus Org Maintainers (in alphabetical order) # Emeritus Org Maintainers (in alphabetical order)
Justin Cormack <justin.cormack@docker.com> (@justincormack) Justin Cormack <justin.cormack@docker.com> (@justincormack)
Steve Lasker <StevenLasker@hotmail.com> (@stevelasker) Steve Lasker <StevenLasker@hotmail.com> (@stevelasker)
# Emeritus Repo-Level Maintainers (in alphabetical order)
Junjie Gao <junjiegao@microsoft.com> (@JeyJeyGao)

View File

@ -29,7 +29,7 @@ clean:
.PHONY: check-line-endings .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

12
go.mod
View File

@ -1,13 +1,13 @@
module github.com/notaryproject/notation-core-go module github.com/notaryproject/notation-core-go
go 1.22 go 1.23.0
require ( require (
github.com/fxamacker/cbor/v2 v2.7.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/notaryproject/tspclient-go v0.2.0 github.com/notaryproject/tspclient-go v1.0.0
github.com/veraison/go-cose v1.1.0 github.com/veraison/go-cose v1.3.0
golang.org/x/crypto v0.27.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

20
go.sum
View File

@ -1,12 +1,12 @@
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 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/notaryproject/tspclient-go v0.2.0 h1:g/KpQGmyk/h7j60irIRG1mfWnibNOzJ8WhLqAzuiQAQ= github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4=
github.com/notaryproject/tspclient-go v0.2.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs= github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs=
github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o= github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7rk=
github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= 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.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=

View File

@ -0,0 +1,121 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package algorithm includes signature algorithms accepted by Notary Project
package algorithm
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"errors"
"fmt"
)
// Algorithm defines the signature algorithm.
type Algorithm int
// Signature algorithms supported by this library.
//
// Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection
const (
AlgorithmPS256 Algorithm = 1 + iota // RSASSA-PSS with SHA-256
AlgorithmPS384 // RSASSA-PSS with SHA-384
AlgorithmPS512 // RSASSA-PSS with SHA-512
AlgorithmES256 // ECDSA on secp256r1 with SHA-256
AlgorithmES384 // ECDSA on secp384r1 with SHA-384
AlgorithmES512 // ECDSA on secp521r1 with SHA-512
)
// Hash returns the hash function of the algorithm.
func (alg Algorithm) Hash() crypto.Hash {
switch alg {
case AlgorithmPS256, AlgorithmES256:
return crypto.SHA256
case AlgorithmPS384, AlgorithmES384:
return crypto.SHA384
case AlgorithmPS512, AlgorithmES512:
return crypto.SHA512
}
return 0
}
// KeyType defines the key type.
type KeyType int
const (
KeyTypeRSA KeyType = 1 + iota // KeyType RSA
KeyTypeEC // KeyType EC
)
// KeySpec defines a key type and size.
type KeySpec struct {
// KeyType is the type of the key.
Type KeyType
// KeySize is the size of the key in bits.
Size int
}
// SignatureAlgorithm returns the signing algorithm associated with the KeySpec.
func (k KeySpec) SignatureAlgorithm() Algorithm {
switch k.Type {
case KeyTypeEC:
switch k.Size {
case 256:
return AlgorithmES256
case 384:
return AlgorithmES384
case 521:
return AlgorithmES512
}
case KeyTypeRSA:
switch k.Size {
case 2048:
return AlgorithmPS256
case 3072:
return AlgorithmPS384
case 4096:
return AlgorithmPS512
}
}
return 0
}
// ExtractKeySpec extracts KeySpec from the signing certificate.
func ExtractKeySpec(signingCert *x509.Certificate) (KeySpec, error) {
switch key := signingCert.PublicKey.(type) {
case *rsa.PublicKey:
switch bitSize := key.Size() << 3; bitSize {
case 2048, 3072, 4096:
return KeySpec{
Type: KeyTypeRSA,
Size: bitSize,
}, nil
default:
return KeySpec{}, fmt.Errorf("rsa key size %d bits is not supported", bitSize)
}
case *ecdsa.PublicKey:
switch bitSize := key.Curve.Params().BitSize; bitSize {
case 256, 384, 521:
return KeySpec{
Type: KeyTypeEC,
Size: bitSize,
}, nil
default:
return KeySpec{}, fmt.Errorf("ecdsa key size %d bits is not supported", bitSize)
}
}
return KeySpec{}, errors.New("unsupported public key type")
}

View File

@ -0,0 +1,244 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package algorithm
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"reflect"
"strconv"
"testing"
"github.com/notaryproject/notation-core-go/testhelper"
)
func TestHash(t *testing.T) {
tests := []struct {
name string
alg Algorithm
expect crypto.Hash
}{
{
name: "PS256",
alg: AlgorithmPS256,
expect: crypto.SHA256,
},
{
name: "ES256",
alg: AlgorithmES256,
expect: crypto.SHA256,
},
{
name: "PS384",
alg: AlgorithmPS384,
expect: crypto.SHA384,
},
{
name: "ES384",
alg: AlgorithmES384,
expect: crypto.SHA384,
},
{
name: "PS512",
alg: AlgorithmPS512,
expect: crypto.SHA512,
},
{
name: "ES512",
alg: AlgorithmES512,
expect: crypto.SHA512,
},
{
name: "UnsupportedAlgorithm",
alg: 0,
expect: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
hash := tt.alg.Hash()
if hash != tt.expect {
t.Fatalf("Expected %v, got %v", tt.expect, hash)
}
})
}
}
func TestSignatureAlgorithm(t *testing.T) {
tests := []struct {
name string
keySpec KeySpec
expect Algorithm
}{
{
name: "EC 256",
keySpec: KeySpec{
Type: KeyTypeEC,
Size: 256,
},
expect: AlgorithmES256,
},
{
name: "EC 384",
keySpec: KeySpec{
Type: KeyTypeEC,
Size: 384,
},
expect: AlgorithmES384,
},
{
name: "EC 521",
keySpec: KeySpec{
Type: KeyTypeEC,
Size: 521,
},
expect: AlgorithmES512,
},
{
name: "RSA 2048",
keySpec: KeySpec{
Type: KeyTypeRSA,
Size: 2048,
},
expect: AlgorithmPS256,
},
{
name: "RSA 3072",
keySpec: KeySpec{
Type: KeyTypeRSA,
Size: 3072,
},
expect: AlgorithmPS384,
},
{
name: "RSA 4096",
keySpec: KeySpec{
Type: KeyTypeRSA,
Size: 4096,
},
expect: AlgorithmPS512,
},
{
name: "Unsupported key spec",
keySpec: KeySpec{
Type: 0,
Size: 0,
},
expect: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
alg := tt.keySpec.SignatureAlgorithm()
if alg != tt.expect {
t.Errorf("unexpected signature algorithm: %v, expect: %v", alg, tt.expect)
}
})
}
}
func TestExtractKeySpec(t *testing.T) {
type testCase struct {
name string
cert *x509.Certificate
expect KeySpec
expectErr bool
}
// invalid cases
tests := []testCase{
{
name: "RSA wrong size",
cert: testhelper.GetUnsupportedRSACert().Cert,
expect: KeySpec{},
expectErr: true,
},
{
name: "ECDSA wrong size",
cert: testhelper.GetUnsupportedECCert().Cert,
expect: KeySpec{},
expectErr: true,
},
{
name: "Unsupported type",
cert: &x509.Certificate{
PublicKey: ed25519.PublicKey{},
},
expect: KeySpec{},
expectErr: true,
},
}
// append valid RSA cases
for _, k := range []int{2048, 3072, 4096} {
rsaRoot := testhelper.GetRSARootCertificate()
priv, _ := rsa.GenerateKey(rand.Reader, k)
certTuple := testhelper.GetRSACertTupleWithPK(
priv,
"Test RSA_"+strconv.Itoa(priv.Size()),
&rsaRoot,
)
tests = append(tests, testCase{
name: "RSA " + strconv.Itoa(k),
cert: certTuple.Cert,
expect: KeySpec{
Type: KeyTypeRSA,
Size: k,
},
expectErr: false,
})
}
// append valid EDCSA cases
for _, curve := range []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} {
ecdsaRoot := testhelper.GetECRootCertificate()
priv, _ := ecdsa.GenerateKey(curve, rand.Reader)
bitSize := priv.Params().BitSize
certTuple := testhelper.GetECDSACertTupleWithPK(
priv,
"Test EC_"+strconv.Itoa(bitSize),
&ecdsaRoot,
)
tests = append(tests, testCase{
name: "EC " + strconv.Itoa(bitSize),
cert: certTuple.Cert,
expect: KeySpec{
Type: KeyTypeEC,
Size: bitSize,
},
expectErr: false,
})
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
keySpec, err := ExtractKeySpec(tt.cert)
if (err != nil) != tt.expectErr {
t.Errorf("error = %v, expectErr = %v", err, tt.expectErr)
}
if !reflect.DeepEqual(keySpec, tt.expect) {
t.Errorf("expect %+v, got %+v", tt.expect, keySpec)
}
})
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -16,7 +16,11 @@ package timestamp
import ( import (
"crypto/x509" "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" "github.com/notaryproject/notation-core-go/signature"
nx509 "github.com/notaryproject/notation-core-go/x509" nx509 "github.com/notaryproject/notation-core-go/x509"
"github.com/notaryproject/tspclient-go" "github.com/notaryproject/tspclient-go"
@ -43,17 +47,8 @@ func Timestamp(req *signature.SignRequest, opts tspclient.RequestOptions) ([]byt
if err != nil { if err != nil {
return nil, err return nil, err
} }
info, err := token.Info()
if err != nil {
return nil, err
}
timestamp, err := info.Validate(opts.Content)
if err != nil {
return nil, err
}
tsaCertChain, err := token.Verify(ctx, x509.VerifyOptions{ tsaCertChain, err := token.Verify(ctx, x509.VerifyOptions{
CurrentTime: timestamp.Value, Roots: req.TSARootCAs,
Roots: req.TSARootCAs,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -61,5 +56,54 @@ func Timestamp(req *signature.SignRequest, opts tspclient.RequestOptions) ([]byt
if err := nx509.ValidateTimestampingCertChain(tsaCertChain); err != nil { if err := nx509.ValidateTimestampingCertChain(tsaCertChain); err != nil {
return nil, err 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 return resp.TimestampToken.FullBytes, nil
} }
// revocationResult returns an error if any cert in the cert chain has
// a revocation status other than ResultOK or ResultNonRevokable.
// When ResultRevoked presents, always return the revoked error.
func revocationResult(certResults []*result.CertRevocationResult, certChain []*x509.Certificate) error {
//sanity check
if len(certResults) == 0 {
return errors.New("certificate revocation result cannot be empty")
}
if len(certResults) != len(certChain) {
return fmt.Errorf("length of certificate revocation result %d does not match length of the certificate chain %d", len(certResults), len(certChain))
}
numOKResults := 0
var problematicCertSubject string
var hasUnknownResult bool
for i := len(certResults) - 1; i >= 0; i-- {
cert := certChain[i]
certResult := certResults[i]
if certResult.Result == result.ResultOK || certResult.Result == result.ResultNonRevokable {
numOKResults++
} else {
if certResult.Result == result.ResultRevoked { // revoked
return fmt.Errorf("timestamping certificate with subject %q is revoked", cert.Subject.String())
}
if !hasUnknownResult { // unknown
// not returning because a following cert can be revoked
problematicCertSubject = cert.Subject.String()
hasUnknownResult = true
}
}
}
if numOKResults != len(certResults) {
return fmt.Errorf("timestamping certificate with subject %q revocation status is unknown", problematicCertSubject)
}
return nil
}

View File

@ -17,22 +17,25 @@ import (
"context" "context"
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix"
"encoding/asn1" "encoding/asn1"
"errors" "errors"
"os" "os"
"strings" "strings"
"testing" "testing"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/notation-core-go/revocation/result"
"github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/signature"
nx509 "github.com/notaryproject/notation-core-go/x509" nx509 "github.com/notaryproject/notation-core-go/x509"
"github.com/notaryproject/tspclient-go" "github.com/notaryproject/tspclient-go"
"github.com/notaryproject/tspclient-go/pki" "github.com/notaryproject/tspclient-go/pki"
) )
const rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" const rfc3161TSAurl = "http://timestamp.digicert.com"
func TestTimestamp(t *testing.T) { func TestTimestamp(t *testing.T) {
rootCerts, err := nx509.ReadCertificateFile("testdata/tsaRootCert.crt") rootCerts, err := nx509.ReadCertificateFile("testdata/tsaRootCert.cer")
if err != nil || len(rootCerts) == 0 { if err != nil || len(rootCerts) == 0 {
t.Fatal("failed to read root CA certificate:", err) t.Fatal("failed to read root CA certificate:", err)
} }
@ -41,96 +44,261 @@ func TestTimestamp(t *testing.T) {
rootCAs.AddCert(rootCert) rootCAs.AddCert(rootCert)
// --------------- Success case ---------------------------------- // --------------- Success case ----------------------------------
timestamper, err := tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl) t.Run("Timestamping success", func(t *testing.T) {
if err != nil { timestamper, err := tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl)
t.Fatal(err) if err != nil {
} t.Fatal(err)
req := &signature.SignRequest{ }
Timestamper: timestamper, req := &signature.SignRequest{
TSARootCAs: rootCAs, Timestamper: timestamper,
} TSARootCAs: rootCAs,
opts := tspclient.RequestOptions{ }
Content: []byte("notation"), opts := tspclient.RequestOptions{
HashAlgorithm: crypto.SHA256, Content: []byte("notation"),
} HashAlgorithm: crypto.SHA256,
_, err = Timestamp(req, opts) }
if err != nil { _, err = Timestamp(req, opts)
t.Fatal(err) if err != nil {
} t.Fatal(err)
}
})
// ------------- Failure cases ------------------------ // ------------- Failure cases ------------------------
opts = tspclient.RequestOptions{ t.Run("Timestamping SHA-1", func(t *testing.T) {
Content: []byte("notation"), timestamper, err := tspclient.NewHTTPTimestamper(nil, rfc3161TSAurl)
HashAlgorithm: crypto.SHA1, if err != nil {
} t.Fatal(err)
expectedErr := "malformed timestamping request: unsupported hashing algorithm: SHA-1" }
_, err = Timestamp(req, opts) req := &signature.SignRequest{
assertErrorEqual(expectedErr, err, t) 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)
})
req = &signature.SignRequest{ t.Run("Timestamping failed", func(t *testing.T) {
Timestamper: dummyTimestamper{}, req := &signature.SignRequest{
TSARootCAs: rootCAs, Timestamper: dummyTimestamper{},
} TSARootCAs: rootCAs,
opts = tspclient.RequestOptions{ }
Content: []byte("notation"), opts := tspclient.RequestOptions{
HashAlgorithm: crypto.SHA256, Content: []byte("notation"),
NoNonce: true, HashAlgorithm: crypto.SHA256,
} }
expectedErr = "failed to timestamp" expectedErr := "failed to timestamp"
_, err = Timestamp(req, opts) _, err = Timestamp(req, opts)
if err == nil || !strings.Contains(err.Error(), expectedErr) { if err == nil || !strings.Contains(err.Error(), expectedErr) {
t.Fatalf("expected error message to contain %s, but got %v", expectedErr, err) t.Fatalf("expected error message to contain %s, but got %v", expectedErr, err)
} }
})
req = &signature.SignRequest{ t.Run("Timestamping rejected", func(t *testing.T) {
Timestamper: dummyTimestamper{ req := &signature.SignRequest{
respWithRejectedStatus: true, 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
}, },
TSARootCAs: rootCAs, {
} Result: result.ResultNonRevokable,
expectedErr = "invalid timestamping response: invalid response with status code 2: rejected" ServerResults: []*result.ServerResult{
_, err = Timestamp(req, opts) {
assertErrorEqual(expectedErr, err, t) Result: result.ResultNonRevokable,
},
req = &signature.SignRequest{ },
Timestamper: dummyTimestamper{
invalidTSTInfo: true,
}, },
TSARootCAs: rootCAs,
} }
expectedErr = "cannot unmarshal TSTInfo from timestamp token: asn1: structure error: tags don't match (23 vs {class:0 tag:16 length:3 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:24 set:false omitEmpty:false} Time @89" certChain := []*x509.Certificate{
_, err = Timestamp(req, opts) {
assertErrorEqual(expectedErr, err, t) Subject: pkix.Name{
CommonName: "leafCert",
opts = tspclient.RequestOptions{ },
Content: []byte("mismatch"),
HashAlgorithm: crypto.SHA256,
NoNonce: true,
}
req = &signature.SignRequest{
Timestamper: dummyTimestamper{
failValidate: true,
}, },
TSARootCAs: rootCAs, {
} Subject: pkix.Name{
expectedErr = "invalid TSTInfo: mismatched message" CommonName: "rootCert",
_, err = Timestamp(req, opts) },
assertErrorEqual(expectedErr, err, t)
opts = tspclient.RequestOptions{
Content: []byte("notation"),
HashAlgorithm: crypto.SHA256,
NoNonce: true,
}
req = &signature.SignRequest{
Timestamper: dummyTimestamper{
invalidSignature: true,
}, },
TSARootCAs: rootCAs,
} }
expectedErr = "failed to verify signed token: cms verification failure: crypto/rsa: verification error" t.Run("OCSP error without fallback", func(t *testing.T) {
_, err = Timestamp(req, opts) certResult[0] = &result.CertRevocationResult{
assertErrorEqual(expectedErr, err, t) 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) { func assertErrorEqual(expected string, err error, t *testing.T) {
@ -141,8 +309,6 @@ func assertErrorEqual(expected string, err error, t *testing.T) {
type dummyTimestamper struct { type dummyTimestamper struct {
respWithRejectedStatus bool respWithRejectedStatus bool
invalidTSTInfo bool
failValidate bool
invalidSignature bool invalidSignature bool
} }
@ -154,34 +320,6 @@ func (d dummyTimestamper) Timestamp(context.Context, *tspclient.Request) (*tspcl
}, },
}, nil }, nil
} }
if d.invalidTSTInfo {
token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidTSTInfo.p7s")
if err != nil {
return nil, err
}
return &tspclient.Response{
Status: pki.StatusInfo{
Status: pki.StatusGranted,
},
TimestampToken: asn1.RawValue{
FullBytes: token,
},
}, nil
}
if d.failValidate {
token, err := os.ReadFile("testdata/TimeStampToken.p7s")
if err != nil {
return nil, err
}
return &tspclient.Response{
Status: pki.StatusInfo{
Status: pki.StatusGranted,
},
TimestampToken: asn1.RawValue{
FullBytes: token,
},
}, nil
}
if d.invalidSignature { if d.invalidSignature {
token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidSignature.p7s") token, err := os.ReadFile("testdata/TimeStampTokenWithInvalidSignature.p7s")
if err != nil { if err != nil {
@ -198,3 +336,48 @@ func (d dummyTimestamper) Timestamp(context.Context, *tspclient.Request) (*tspcl
} }
return nil, errors.New("failed to timestamp") 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
}

View File

@ -17,3 +17,6 @@ import "errors"
// ErrCacheMiss is returned when a cache miss occurs. // ErrCacheMiss is returned when a cache miss occurs.
var ErrCacheMiss = errors.New("cache miss") var ErrCacheMiss = errors.New("cache miss")
// errDeltaCRLNotFound is returned when a delta CRL is not found.
var errDeltaCRLNotFound = errors.New("delta CRL not found")

View File

@ -18,6 +18,7 @@ package crl
import ( import (
"context" "context"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix"
"encoding/asn1" "encoding/asn1"
"errors" "errors"
"fmt" "fmt"
@ -25,6 +26,10 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"time" "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 // oidFreshestCRL is the object identifier for the distribution point
@ -84,9 +89,8 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) {
if f.Cache != nil { if f.Cache != nil {
bundle, err := f.Cache.Get(ctx, url) bundle, err := f.Cache.Get(ctx, url)
if err == nil { if err == nil {
// check expiry // check expiry of base CRL and delta CRL
nextUpdate := bundle.BaseCRL.NextUpdate if isEffective(bundle.BaseCRL) && (bundle.DeltaCRL == nil || isEffective(bundle.DeltaCRL)) {
if !nextUpdate.IsZero() && !time.Now().After(nextUpdate) {
return bundle, nil return bundle, nil
} }
} else if !errors.Is(err, ErrCacheMiss) && !f.DiscardCacheError { } else if !errors.Is(err, ErrCacheMiss) && !f.DiscardCacheError {
@ -109,6 +113,11 @@ func (f *HTTPFetcher) Fetch(ctx context.Context, url string) (*Bundle, error) {
return bundle, nil 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. // fetch downloads the CRL from the given URL.
func (f *HTTPFetcher) fetch(ctx context.Context, url string) (*Bundle, error) { func (f *HTTPFetcher) fetch(ctx context.Context, url string) (*Bundle, error) {
// fetch base CRL // fetch base CRL
@ -117,19 +126,109 @@ func (f *HTTPFetcher) fetch(ctx context.Context, url string) (*Bundle, error) {
return nil, err return nil, err
} }
// check delta CRL // fetch delta CRL from base CRL extension
// TODO: support delta CRL https://github.com/notaryproject/notation-core-go/issues/228 deltaCRL, err := f.fetchDeltaCRL(ctx, base.Extensions)
for _, ext := range base.Extensions { if err != nil && !errors.Is(err, errDeltaCRLNotFound) {
if ext.Id.Equal(oidFreshestCRL) { return nil, err
return nil, errors.New("delta CRL is not supported")
}
} }
return &Bundle{ return &Bundle{
BaseCRL: base, BaseCRL: base,
DeltaCRL: deltaCRL,
}, nil }, 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) { func fetchCRL(ctx context.Context, crlURL string, client *http.Client) (*x509.RevocationList, error) {
// validate URL // validate URL
parsedURL, err := url.Parse(crlURL) parsedURL, err := url.Parse(crlURL)
@ -150,7 +249,7 @@ func fetchCRL(ctx context.Context, crlURL string, client *http.Client) (*x509.Re
return nil, fmt.Errorf("request failed: %w", err) return nil, fmt.Errorf("request failed: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode) return nil, fmt.Errorf("failed to download with status code: %d", resp.StatusCode)
} }
// read with size limit // read with size limit

View File

@ -19,11 +19,13 @@ import (
"crypto/rand" "crypto/rand"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/pem"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"math/big" "math/big"
"net/http" "net/http"
"os"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@ -55,8 +57,8 @@ func TestFetch(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("failed to parse base CRL: %v", err) t.Fatalf("failed to parse base CRL: %v", err)
} }
const exampleURL = "http://example.com" const exampleURL = "http://localhost.test"
const uncachedURL = "http://uncached.com" const uncachedURL = "http://uncached.test"
bundle := &Bundle{ bundle := &Bundle{
BaseCRL: baseCRL, BaseCRL: baseCRL,
@ -78,7 +80,7 @@ func TestFetch(t *testing.T) {
t.Run("fetch without cache", func(t *testing.T) { t.Run("fetch without cache", func(t *testing.T) {
httpClient := &http.Client{ httpClient := &http.Client{
Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
} }
f, err := NewHTTPFetcher(httpClient) f, err := NewHTTPFetcher(httpClient)
if err != nil { if err != nil {
@ -134,7 +136,7 @@ func TestFetch(t *testing.T) {
t.Run("cache miss", func(t *testing.T) { t.Run("cache miss", func(t *testing.T) {
c := &memoryCache{} c := &memoryCache{}
httpClient := &http.Client{ httpClient := &http.Client{
Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
} }
f, err := NewHTTPFetcher(httpClient) f, err := NewHTTPFetcher(httpClient)
if err != nil { if err != nil {
@ -167,7 +169,7 @@ func TestFetch(t *testing.T) {
t.Fatalf("failed to parse base CRL: %v", err) t.Fatalf("failed to parse base CRL: %v", err)
} }
// store the expired CRL // store the expired CRL
const expiredCRLURL = "http://example.com/expired" const expiredCRLURL = "http://localhost.test/expired"
bundle := &Bundle{ bundle := &Bundle{
BaseCRL: expiredCRL, BaseCRL: expiredCRL,
} }
@ -177,7 +179,7 @@ func TestFetch(t *testing.T) {
// fetch the expired CRL // fetch the expired CRL
httpClient := &http.Client{ httpClient := &http.Client{
Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
} }
f, err := NewHTTPFetcher(httpClient) f, err := NewHTTPFetcher(httpClient)
if err != nil { if err != nil {
@ -195,46 +197,13 @@ func TestFetch(t *testing.T) {
} }
}) })
t.Run("delta CRL is not supported", func(t *testing.T) {
c := &memoryCache{}
// prepare a CRL with refresh CRL extension
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),
ExtraExtensions: []pkix.Extension{
{
Id: oidFreshestCRL,
Value: []byte{0x01, 0x02, 0x03},
},
},
}, certChain[1].Cert, certChain[1].PrivateKey)
if err != nil {
t.Fatalf("failed to create base CRL: %v", err)
}
httpClient := &http.Client{
Transport: expectedRoundTripperMock{Body: expiredCRLBytes},
}
f, err := NewHTTPFetcher(httpClient)
if err != nil {
t.Errorf("NewHTTPFetcher() error = %v, want nil", err)
}
f.Cache = c
f.DiscardCacheError = true
_, err = f.Fetch(context.Background(), uncachedURL)
if !strings.Contains(err.Error(), "delta CRL is not supported") {
t.Errorf("Fetcher.Fetch() error = %v, want delta CRL is not supported", err)
}
})
t.Run("Set cache error", func(t *testing.T) { t.Run("Set cache error", func(t *testing.T) {
c := &errorCache{ c := &errorCache{
GetError: ErrCacheMiss, GetError: ErrCacheMiss,
SetError: errors.New("cache error"), SetError: errors.New("cache error"),
} }
httpClient := &http.Client{ httpClient := &http.Client{
Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
} }
f, err := NewHTTPFetcher(httpClient) f, err := NewHTTPFetcher(httpClient)
if err != nil { if err != nil {
@ -256,7 +225,7 @@ func TestFetch(t *testing.T) {
GetError: errors.New("cache error"), GetError: errors.New("cache error"),
} }
httpClient := &http.Client{ httpClient := &http.Client{
Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
} }
f, err := NewHTTPFetcher(httpClient) f, err := NewHTTPFetcher(httpClient)
if err != nil { if err != nil {
@ -276,7 +245,7 @@ func TestFetch(t *testing.T) {
SetError: errors.New("cache error"), SetError: errors.New("cache error"),
} }
httpClient := &http.Client{ httpClient := &http.Client{
Transport: expectedRoundTripperMock{Body: baseCRL.Raw}, Transport: &expectedRoundTripperMock{Body: baseCRL.Raw},
} }
f, err := NewHTTPFetcher(httpClient) f, err := NewHTTPFetcher(httpClient)
if err != nil { if err != nil {
@ -289,6 +258,217 @@ func TestFetch(t *testing.T) {
t.Errorf("Fetcher.Fetch() error = %v, want failed to store CRL to cache:", err) 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) { func TestDownload(t *testing.T) {
@ -299,7 +479,7 @@ func TestDownload(t *testing.T) {
} }
}) })
t.Run("https download", func(t *testing.T) { t.Run("https download", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "https://example.com", http.DefaultClient) _, err := fetchCRL(context.Background(), "https://localhost.test", http.DefaultClient)
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
@ -307,14 +487,14 @@ func TestDownload(t *testing.T) {
t.Run("http.NewRequestWithContext error", func(t *testing.T) { t.Run("http.NewRequestWithContext error", func(t *testing.T) {
var ctx context.Context = nil var ctx context.Context = nil
_, err := fetchCRL(ctx, "http://example.com", &http.Client{}) _, err := fetchCRL(ctx, "http://localhost.test", &http.Client{})
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
}) })
t.Run("client.Do error", func(t *testing.T) { t.Run("client.Do error", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "http://example.com", &http.Client{ _, err := fetchCRL(context.Background(), "http://localhost.test", &http.Client{
Transport: errorRoundTripperMock{}, Transport: errorRoundTripperMock{},
}) })
@ -324,7 +504,7 @@ func TestDownload(t *testing.T) {
}) })
t.Run("status code is not 2xx", func(t *testing.T) { t.Run("status code is not 2xx", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "http://example.com", &http.Client{ _, err := fetchCRL(context.Background(), "http://localhost.test", &http.Client{
Transport: serverErrorRoundTripperMock{}, Transport: serverErrorRoundTripperMock{},
}) })
if err == nil { if err == nil {
@ -333,7 +513,7 @@ func TestDownload(t *testing.T) {
}) })
t.Run("readAll error", func(t *testing.T) { t.Run("readAll error", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "http://example.com", &http.Client{ _, err := fetchCRL(context.Background(), "http://localhost.test", &http.Client{
Transport: readFailedRoundTripperMock{}, Transport: readFailedRoundTripperMock{},
}) })
if err == nil { if err == nil {
@ -342,8 +522,8 @@ func TestDownload(t *testing.T) {
}) })
t.Run("exceed the size limit", func(t *testing.T) { t.Run("exceed the size limit", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "http://example.com", &http.Client{ _, err := fetchCRL(context.Background(), "http://localhost.test", &http.Client{
Transport: expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)}, Transport: &expectedRoundTripperMock{Body: make([]byte, maxCRLSize+1)},
}) })
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
@ -351,8 +531,8 @@ func TestDownload(t *testing.T) {
}) })
t.Run("invalid crl", func(t *testing.T) { t.Run("invalid crl", func(t *testing.T) {
_, err := fetchCRL(context.Background(), "http://example.com", &http.Client{ _, err := fetchCRL(context.Background(), "http://localhost.test", &http.Client{
Transport: expectedRoundTripperMock{Body: []byte("invalid crl")}, Transport: &expectedRoundTripperMock{Body: []byte("invalid crl")},
}) })
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
@ -395,14 +575,24 @@ func (r errorReaderMock) Close() error {
} }
type expectedRoundTripperMock struct { type expectedRoundTripperMock struct {
Body []byte Body []byte
SecondRoundBody []byte
count int
} }
func (rt expectedRoundTripperMock) RoundTrip(req *http.Request) (*http.Response, error) { 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{ return &http.Response{
Request: req, Request: req,
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewBuffer(rt.Body)), Body: io.NopCloser(bytes.NewBuffer(rt.SecondRoundBody)),
}, nil }, nil
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

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

Binary file not shown.

View File

@ -21,10 +21,32 @@ import (
"encoding/asn1" "encoding/asn1"
"errors" "errors"
"fmt" "fmt"
"math/big"
"time" "time"
"github.com/notaryproject/notation-core-go/revocation/crl" "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" "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 ( var (
@ -36,6 +58,10 @@ var (
// distribution point CRL extension. (See RFC 5280, Section 5.2.5) // distribution point CRL extension. (See RFC 5280, Section 5.2.5)
oidIssuingDistributionPoint = asn1.ObjectIdentifier{2, 5, 29, 28} 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 // oidInvalidityDate is the object identifier for the invalidity date
// CRL entry extension. (See RFC 5280, Section 5.3.2) // CRL entry extension. (See RFC 5280, Section 5.3.2)
oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24} oidInvalidityDate = asn1.ObjectIdentifier{2, 5, 29, 24}
@ -86,9 +112,10 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C
} }
var ( var (
serverResults = make([]*result.ServerResult, 0, len(cert.CRLDistributionPoints)) serverResults = make([]*result.ServerResult, 0, len(cert.CRLDistributionPoints))
lastErr error lastErr error
crlURL string crlURL string
hasFreshestCRLInCertificate = x509util.FindExtensionByOID(cert.Extensions, oidFreshestCRL) != nil
) )
// The CRLDistributionPoints contains the URIs of all the CRL distribution // The CRLDistributionPoints contains the URIs of all the CRL distribution
@ -105,12 +132,28 @@ func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts C
break break
} }
if err = validate(bundle.BaseCRL, issuer); err != nil { 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) lastErr = fmt.Errorf("failed to validate CRL from %s: %w", crlURL, err)
break break
} }
crlResult, err := checkRevocation(cert, bundle.BaseCRL, opts.SigningTime, crlURL) crlResult, err := checkRevocation(cert, bundle, opts.SigningTime, crlURL)
if err != nil { if err != nil {
lastErr = fmt.Errorf("failed to check revocation status from %s: %w", crlURL, err) lastErr = fmt.Errorf("failed to check revocation status from %s: %w", crlURL, err)
break break
@ -153,7 +196,44 @@ func Supported(cert *x509.Certificate) bool {
return cert != nil && len(cert.CRLDistributionPoints) > 0 return cert != nil && len(cert.CRLDistributionPoints) > 0
} }
func validate(crl *x509.RevocationList, issuer *x509.Certificate) error { 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 // check signature
if err := crl.CheckSignatureFrom(issuer); err != nil { if err := crl.CheckSignatureFrom(issuer); err != nil {
return fmt.Errorf("CRL is not signed by CA %s: %w,", issuer.Subject, err) return fmt.Errorf("CRL is not signed by CA %s: %w,", issuer.Subject, err)
@ -170,12 +250,12 @@ func validate(crl *x509.RevocationList, issuer *x509.Certificate) error {
for _, ext := range crl.Extensions { for _, ext := range crl.Extensions {
switch { switch {
case ext.Id.Equal(oidFreshestCRL):
return errors.New("delta CRL is not supported")
case ext.Id.Equal(oidIssuingDistributionPoint): case ext.Id.Equal(oidIssuingDistributionPoint):
// IssuingDistributionPoint is a critical extension that identifies // IssuingDistributionPoint is a critical extension that identifies
// the scope of the CRL. Since we will check all the CRL // the scope of the CRL. Since we will check all the CRL
// distribution points, it is not necessary to check this extension. // distribution points, it is not necessary to check this extension.
case ext.Id.Equal(oidDeltaCRLIndicator):
// will be checked in validate()
default: default:
if ext.Critical { if ext.Critical {
// unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2) // unsupported critical extensions is not allowed. (See RFC 5280, Section 5.2)
@ -188,16 +268,43 @@ func validate(crl *x509.RevocationList, issuer *x509.Certificate) error {
} }
// checkRevocation checks if the certificate is revoked or not // checkRevocation checks if the certificate is revoked or not
func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signingTime time.Time, crlURL string) (*result.ServerResult, error) { func checkRevocation(cert *x509.Certificate, b *crl.Bundle, signingTime time.Time, crlURL string) (*result.ServerResult, error) {
if cert == nil { if cert == nil {
return nil, errors.New("certificate cannot be nil") return nil, errors.New("certificate cannot be nil")
} }
if b == nil {
if baseCRL == nil { return nil, errors.New("CRL bundle cannot be nil")
}
if b.BaseCRL == nil {
return nil, errors.New("baseCRL cannot be nil") return nil, errors.New("baseCRL cannot be nil")
} }
for _, revocationEntry := range baseCRL.RevokedCertificateEntries { // 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 { if revocationEntry.SerialNumber.Cmp(cert.SerialNumber) == 0 {
extensions, err := parseEntryExtensions(revocationEntry) extensions, err := parseEntryExtensions(revocationEntry)
if err != nil { if err != nil {
@ -209,17 +316,38 @@ func checkRevocation(cert *x509.Certificate, baseCRL *x509.RevocationList, signi
signingTime.Before(extensions.invalidityDate) { signingTime.Before(extensions.invalidityDate) {
// signing time is before the invalidity date which means the // signing time is before the invalidity date which means the
// certificate is not revoked at the time of signing. // certificate is not revoked at the time of signing.
break return &result.ServerResult{
Result: result.ResultOK,
Server: crlURL,
RevocationMethod: result.RevocationMethodCRL,
}, nil
} }
// revoked switch revocationEntry.ReasonCode {
return &result.ServerResult{ case reasonCodeCertificateHold, reasonCodeRemoveFromCRL:
Result: result.ResultRevoked, // temporarily revoked or unrevoked
Server: crlURL, if latestTempRevokedEntry == nil || latestTempRevokedEntry.RevocationTime.Before(revocationEntry.RevocationTime) {
RevocationMethod: result.RevocationMethodCRL, // the revocation status depends on the most recent reason
}, nil 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{ return &result.ServerResult{
Result: result.ResultOK, Result: result.ResultOK,
@ -233,7 +361,7 @@ type entryExtensions struct {
invalidityDate time.Time invalidityDate time.Time
} }
func parseEntryExtensions(entry x509.RevocationListEntry) (entryExtensions, error) { func parseEntryExtensions(entry *x509.RevocationListEntry) (entryExtensions, error) {
var extensions entryExtensions var extensions entryExtensions
for _, ext := range entry.Extensions { for _, ext := range entry.Extensions {
switch { switch {

View File

@ -45,7 +45,7 @@ func TestCertCheckStatus(t *testing.T) {
t.Run("fetcher is nil", func(t *testing.T) { t.Run("fetcher is nil", func(t *testing.T) {
cert := &x509.Certificate{ cert := &x509.Certificate{
CRLDistributionPoints: []string{"http://example.com"}, CRLDistributionPoints: []string{"http://localhost.test"},
} }
r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{}) r := CertCheckStatus(context.Background(), cert, &x509.Certificate{}, CertCheckStatusOptions{})
if r.ServerResults[0].Error.Error() != "CRL fetcher cannot be nil" { if r.ServerResults[0].Error.Error() != "CRL fetcher cannot be nil" {
@ -57,7 +57,7 @@ func TestCertCheckStatus(t *testing.T) {
memoryCache := &memoryCache{} memoryCache := &memoryCache{}
cert := &x509.Certificate{ cert := &x509.Certificate{
CRLDistributionPoints: []string{"http://example.com"}, CRLDistributionPoints: []string{"http://localhost.test"},
} }
fetcher, err := crlutils.NewHTTPFetcher( fetcher, err := crlutils.NewHTTPFetcher(
&http.Client{Transport: errorRoundTripperMock{}}, &http.Client{Transport: errorRoundTripperMock{}},
@ -80,7 +80,7 @@ func TestCertCheckStatus(t *testing.T) {
memoryCache := &memoryCache{} memoryCache := &memoryCache{}
cert := &x509.Certificate{ cert := &x509.Certificate{
CRLDistributionPoints: []string{"http://example.com"}, CRLDistributionPoints: []string{"http://localhost.test"},
} }
fetcher, err := crlutils.NewHTTPFetcher( fetcher, err := crlutils.NewHTTPFetcher(
&http.Client{Transport: expiredCRLRoundTripperMock{}}, &http.Client{Transport: expiredCRLRoundTripperMock{}},
@ -203,38 +203,6 @@ func TestCertCheckStatus(t *testing.T) {
} }
}) })
t.Run("CRL with delta CRL is not checked", func(t *testing.T) {
memoryCache := &memoryCache{}
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
NextUpdate: time.Now().Add(time.Hour),
Number: big.NewInt(20240720),
ExtraExtensions: []pkix.Extension{
{
Id: oidFreshestCRL,
Critical: false,
},
},
}, issuerCert, issuerKey)
if err != nil {
t.Fatal(err)
}
fetcher, err := crlutils.NewHTTPFetcher(
&http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}},
)
if err != nil {
t.Fatal(err)
}
fetcher.Cache = memoryCache
fetcher.DiscardCacheError = true
r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{
Fetcher: fetcher,
})
if !strings.Contains(r.ServerResults[0].Error.Error(), "delta CRL is not supported") {
t.Fatalf("unexpected error, got %v, expected %v", r.ServerResults[0].Error, "delta CRL is not supported")
}
})
memoryCache := &memoryCache{} memoryCache := &memoryCache{}
// create a stale CRL // create a stale CRL
@ -253,11 +221,11 @@ func TestCertCheckStatus(t *testing.T) {
BaseCRL: base, BaseCRL: base,
} }
chain[0].Cert.CRLDistributionPoints = []string{"http://example.com"} chain[0].Cert.CRLDistributionPoints = []string{"http://localhost.test"}
t.Run("invalid stale CRL cache, and re-download failed", func(t *testing.T) { t.Run("invalid stale CRL cache, and re-download failed", func(t *testing.T) {
// save to cache // save to cache
if err := memoryCache.Set(context.Background(), "http://example.com", bundle); err != nil { if err := memoryCache.Set(context.Background(), "http://localhost.test", bundle); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -279,7 +247,7 @@ func TestCertCheckStatus(t *testing.T) {
t.Run("invalid stale CRL cache, re-download and still validate failed", func(t *testing.T) { t.Run("invalid stale CRL cache, re-download and still validate failed", func(t *testing.T) {
// save to cache // save to cache
if err := memoryCache.Set(context.Background(), "http://example.com", bundle); err != nil { if err := memoryCache.Set(context.Background(), "http://localhost.test", bundle); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -301,7 +269,7 @@ func TestCertCheckStatus(t *testing.T) {
t.Run("invalid stale CRL cache, re-download and validate seccessfully", func(t *testing.T) { t.Run("invalid stale CRL cache, re-download and validate seccessfully", func(t *testing.T) {
// save to cache // save to cache
if err := memoryCache.Set(context.Background(), "http://example.com", bundle); err != nil { if err := memoryCache.Set(context.Background(), "http://localhost.test", bundle); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -328,6 +296,40 @@ func TestCertCheckStatus(t *testing.T) {
t.Fatalf("expected OK, got %s", r.Result) t.Fatalf("expected OK, got %s", r.Result)
} }
}) })
t.Run("freshest CRL from certificate extension is not supported", func(t *testing.T) {
chain[0].Cert.Extensions = []pkix.Extension{
{
Id: oidFreshestCRL,
},
}
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
NextUpdate: time.Now().Add(time.Hour),
Number: big.NewInt(20240720),
}, issuerCert, issuerKey)
if err != nil {
t.Fatal(err)
}
fetcher, err := crlutils.NewHTTPFetcher(
&http.Client{Transport: expectedRoundTripperMock{Body: crlBytes}},
)
if err != nil {
t.Fatal(err)
}
fetcher.DiscardCacheError = true
r := CertCheckStatus(context.Background(), chain[0].Cert, issuerCert, CertCheckStatusOptions{
Fetcher: fetcher,
})
if r.Result != result.ResultUnknown {
t.Fatalf("expected Unknown, got %s", r.Result)
}
expectedErrorMsg := "freshest CRL from certificate extension is not supported"
if r.ServerResults[0].Error == nil || r.ServerResults[0].Error.Error() != expectedErrorMsg {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, r.ServerResults[0].Error)
}
})
} }
func TestValidate(t *testing.T) { func TestValidate(t *testing.T) {
@ -349,7 +351,7 @@ func TestValidate(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if err := validate(crl, issuerCert); err == nil { if err := validateCRL(crl, issuerCert); err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
}) })
@ -359,7 +361,7 @@ func TestValidate(t *testing.T) {
NextUpdate: time.Now().Add(time.Hour), NextUpdate: time.Now().Add(time.Hour),
} }
if err := validate(crl, &x509.Certificate{}); err == nil { if err := validateCRL(crl, &x509.Certificate{}); err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
}) })
@ -390,7 +392,7 @@ func TestValidate(t *testing.T) {
}, },
} }
if err := validate(crl, issuerCert); err == nil { if err := validateCRL(crl, issuerCert); err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
}) })
@ -419,37 +421,215 @@ func TestValidate(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if err := validate(crl, issuerCert); err != nil { if err := validateCRL(crl, issuerCert); err != nil {
t.Fatal(err) t.Fatal(err)
} }
}) })
t.Run("delta CRL is not supported", func(t *testing.T) { chain := testhelper.GetRevokableRSAChainWithRevocations(1, false, true)
chain := testhelper.GetRevokableRSAChainWithRevocations(1, false, true) issuerCert := chain[0].Cert
issuerCert := chain[0].Cert issuerKey := chain[0].PrivateKey
issuerKey := chain[0].PrivateKey
crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{ crlBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
NextUpdate: time.Now().Add(time.Hour),
Number: big.NewInt(20240720),
}, issuerCert, issuerKey)
if err != nil {
t.Fatal(err)
}
crl, err := x509.ParseRevocationList(crlBytes)
if err != nil {
t.Fatal(err)
}
t.Run("valid crl and delta crl", func(t *testing.T) {
deltaCRLIndicator := big.NewInt(20240720)
deltaCRLIndicatorBytes, err := asn1.Marshal(deltaCRLIndicator)
if err != nil {
t.Fatal(err)
}
deltaCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
NextUpdate: time.Now().Add(time.Hour), NextUpdate: time.Now().Add(time.Hour),
Number: big.NewInt(20240720), Number: big.NewInt(20240721),
ExtraExtensions: []pkix.Extension{ ExtraExtensions: []pkix.Extension{
{ {
Id: oidFreshestCRL, Id: oidDeltaCRLIndicator,
Critical: false, Critical: true,
Value: deltaCRLIndicatorBytes,
}, },
}, },
}, issuerCert, issuerKey) }, issuerCert, issuerKey)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
deltaCRL, err := x509.ParseRevocationList(deltaCRLBytes)
crl, err := x509.ParseRevocationList(crlBytes)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
bundle := &crlutils.Bundle{
BaseCRL: crl,
DeltaCRL: deltaCRL,
}
if err := validate(bundle, issuerCert); err != nil {
t.Fatal(err)
}
})
if err := validate(crl, issuerCert); err.Error() != "delta CRL is not supported" { t.Run("invalid delta crl", func(t *testing.T) {
t.Fatalf("got %v, expected delta CRL is not supported", err) deltaCRLIndicator := big.NewInt(20240720)
deltaCRLIndicatorBytes, err := asn1.Marshal(deltaCRLIndicator)
if err != nil {
t.Fatal(err)
}
deltaCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
Number: big.NewInt(20240721),
ExtraExtensions: []pkix.Extension{
{
Id: oidDeltaCRLIndicator,
Critical: true,
Value: deltaCRLIndicatorBytes,
},
},
}, issuerCert, issuerKey)
if err != nil {
t.Fatal(err)
}
deltaCRL, err := x509.ParseRevocationList(deltaCRLBytes)
if err != nil {
t.Fatal(err)
}
bundle := &crlutils.Bundle{
BaseCRL: crl,
DeltaCRL: deltaCRL,
}
err = validate(bundle, issuerCert)
expectedErrorMsg := "failed to validate delta CRL: CRL NextUpdate is not set"
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, err)
}
})
t.Run("invalid delta crl number", func(t *testing.T) {
deltaCRLIndicator := big.NewInt(20240720)
deltaCRLIndicatorBytes, err := asn1.Marshal(deltaCRLIndicator)
if err != nil {
t.Fatal(err)
}
deltaCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
NextUpdate: time.Now().Add(time.Hour),
Number: big.NewInt(20240719),
ExtraExtensions: []pkix.Extension{
{
Id: oidDeltaCRLIndicator,
Critical: true,
Value: deltaCRLIndicatorBytes,
},
},
}, issuerCert, issuerKey)
if err != nil {
t.Fatal(err)
}
deltaCRL, err := x509.ParseRevocationList(deltaCRLBytes)
if err != nil {
t.Fatal(err)
}
bundle := &crlutils.Bundle{
BaseCRL: crl,
DeltaCRL: deltaCRL,
}
err = validate(bundle, issuerCert)
expectedErrorMsg := "delta CRL number 20240719 is not greater than the base CRL number 20240720"
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, err)
}
})
t.Run("delta crl without delta crl indicator", func(t *testing.T) {
deltaCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
NextUpdate: time.Now().Add(time.Hour),
Number: big.NewInt(20240721),
}, issuerCert, issuerKey)
if err != nil {
t.Fatal(err)
}
deltaCRL, err := x509.ParseRevocationList(deltaCRLBytes)
if err != nil {
t.Fatal(err)
}
bundle := &crlutils.Bundle{
BaseCRL: crl,
DeltaCRL: deltaCRL,
}
err = validate(bundle, issuerCert)
expectedErrorMsg := "delta CRL indicator extension is not found"
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, err)
}
})
t.Run("delta crl minimum base crl number is greater than base crl", func(t *testing.T) {
deltaCRLIndicator := big.NewInt(20240721)
deltaCRLIndicatorBytes, err := asn1.Marshal(deltaCRLIndicator)
if err != nil {
t.Fatal(err)
}
deltaCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
NextUpdate: time.Now().Add(time.Hour),
Number: big.NewInt(20240722),
ExtraExtensions: []pkix.Extension{
{
Id: oidDeltaCRLIndicator,
Critical: true,
Value: deltaCRLIndicatorBytes,
},
},
}, issuerCert, issuerKey)
if err != nil {
t.Fatal(err)
}
deltaCRL, err := x509.ParseRevocationList(deltaCRLBytes)
if err != nil {
t.Fatal(err)
}
bundle := &crlutils.Bundle{
BaseCRL: crl,
DeltaCRL: deltaCRL,
}
err = validate(bundle, issuerCert)
expectedErrorMsg := "delta CRL indicator 20240721 is not less than or equal to the base CRL number 20240720"
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, err)
}
})
t.Run("delta crl with invalid delta indicator extension", func(t *testing.T) {
deltaCRLBytes, err := x509.CreateRevocationList(rand.Reader, &x509.RevocationList{
NextUpdate: time.Now().Add(time.Hour),
Number: big.NewInt(20240722),
ExtraExtensions: []pkix.Extension{
{
Id: oidDeltaCRLIndicator,
Critical: true,
Value: []byte("invalid"),
},
},
}, issuerCert, issuerKey)
if err != nil {
t.Fatal(err)
}
deltaCRL, err := x509.ParseRevocationList(deltaCRLBytes)
if err != nil {
t.Fatal(err)
}
bundle := &crlutils.Bundle{
BaseCRL: crl,
DeltaCRL: deltaCRL,
}
err = validate(bundle, issuerCert)
expectedErrorMsg := "failed to parse delta CRL indicator extension"
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, err)
} }
}) })
} }
@ -461,14 +641,22 @@ func TestCheckRevocation(t *testing.T) {
signingTime := time.Now() signingTime := time.Now()
t.Run("certificate is nil", func(t *testing.T) { t.Run("certificate is nil", func(t *testing.T) {
_, err := checkRevocation(nil, &x509.RevocationList{}, signingTime, "") _, err := checkRevocation(nil, &crlutils.Bundle{BaseCRL: &x509.RevocationList{}}, signingTime, "")
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
}) })
t.Run("CRL is nil", func(t *testing.T) { t.Run("bundle is nil", func(t *testing.T) {
_, err := checkRevocation(cert, nil, signingTime, "") _, err := checkRevocation(cert, nil, signingTime, "")
expectedErrorMsg := "CRL bundle cannot be nil"
if err == nil || err.Error() != expectedErrorMsg {
t.Fatalf("expected error %q, got %v", expectedErrorMsg, err)
}
})
t.Run("CRL is nil", func(t *testing.T) {
_, err := checkRevocation(cert, &crlutils.Bundle{}, signingTime, "")
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
@ -482,7 +670,7 @@ func TestCheckRevocation(t *testing.T) {
}, },
}, },
} }
r, err := checkRevocation(cert, baseCRL, signingTime, "") r, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL}, signingTime, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -500,7 +688,26 @@ func TestCheckRevocation(t *testing.T) {
}, },
}, },
} }
r, err := checkRevocation(cert, baseCRL, signingTime, "") r, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL}, signingTime, "")
if err != nil {
t.Fatal(err)
}
if r.Result != result.ResultRevoked {
t.Fatalf("expected revoked, got %s", r.Result)
}
})
t.Run("revoked in delta CRL", func(t *testing.T) {
baseCRL := &x509.RevocationList{}
deltaCRL := &x509.RevocationList{
RevokedCertificateEntries: []x509.RevocationListEntry{
{
SerialNumber: big.NewInt(1),
RevocationTime: time.Now().Add(-time.Hour),
},
},
}
r, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL, DeltaCRL: deltaCRL}, signingTime, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -533,7 +740,7 @@ func TestCheckRevocation(t *testing.T) {
}, },
}, },
} }
r, err := checkRevocation(cert, baseCRL, signingTime, "") r, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL}, signingTime, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -566,7 +773,7 @@ func TestCheckRevocation(t *testing.T) {
}, },
}, },
} }
r, err := checkRevocation(cert, baseCRL, signingTime, "") r, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL}, signingTime, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -584,7 +791,7 @@ func TestCheckRevocation(t *testing.T) {
}, },
}, },
} }
r, err := checkRevocation(cert, baseCRL, time.Time{}, "") r, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL}, time.Time{}, "")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -607,16 +814,129 @@ func TestCheckRevocation(t *testing.T) {
}, },
}, },
} }
_, err := checkRevocation(cert, baseCRL, signingTime, "") _, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL}, signingTime, "")
if err == nil { if err == nil {
t.Fatal("expected error") t.Fatal("expected error")
} }
}) })
t.Run("delta crl with certificate hold", func(t *testing.T) {
baseCRL := &x509.RevocationList{}
deltaCRL := &x509.RevocationList{
RevokedCertificateEntries: []x509.RevocationListEntry{
{
SerialNumber: big.NewInt(1),
ReasonCode: reasonCodeCertificateHold,
},
},
}
r, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL, DeltaCRL: deltaCRL}, signingTime, "")
if err != nil {
t.Fatal(err)
}
if r.Result != result.ResultRevoked {
t.Fatalf("expected revoked, got %s", r.Result)
}
})
t.Run("certificate hold and remove hold", func(t *testing.T) {
baseCRL := &x509.RevocationList{
RevokedCertificateEntries: []x509.RevocationListEntry{
{
SerialNumber: big.NewInt(1),
ReasonCode: reasonCodeCertificateHold,
RevocationTime: time.Now().Add(-time.Hour),
},
},
}
deltaCRL := &x509.RevocationList{
RevokedCertificateEntries: []x509.RevocationListEntry{
{
SerialNumber: big.NewInt(1),
ReasonCode: reasonCodeRemoveFromCRL,
RevocationTime: time.Now(),
},
},
}
r, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL, DeltaCRL: deltaCRL}, signingTime, "")
if err != nil {
t.Fatal(err)
}
if r.Result != result.ResultOK {
t.Fatalf("expected OK, got %s", r.Result)
}
})
t.Run("certificate hold and remove hold with other other certificate hold", func(t *testing.T) {
baseCRL := &x509.RevocationList{
RevokedCertificateEntries: []x509.RevocationListEntry{
{
SerialNumber: big.NewInt(1),
ReasonCode: reasonCodeCertificateHold,
RevocationTime: time.Now().Add(-time.Hour),
},
},
}
deltaCRL := &x509.RevocationList{
RevokedCertificateEntries: []x509.RevocationListEntry{
{
SerialNumber: big.NewInt(1),
ReasonCode: reasonCodeRemoveFromCRL,
RevocationTime: time.Now(),
},
{
SerialNumber: big.NewInt(2),
ReasonCode: reasonCodeCertificateHold,
RevocationTime: time.Now(),
},
},
}
r, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL, DeltaCRL: deltaCRL}, signingTime, "")
if err != nil {
t.Fatal(err)
}
if r.Result != result.ResultOK {
t.Fatalf("expected OK, got %s", r.Result)
}
})
t.Run("certificate hold, remove hold and hold again", func(t *testing.T) {
baseCRL := &x509.RevocationList{
RevokedCertificateEntries: []x509.RevocationListEntry{
{
SerialNumber: big.NewInt(1),
ReasonCode: reasonCodeCertificateHold,
RevocationTime: time.Now().Add(-2 * time.Hour),
},
},
}
deltaCRL := &x509.RevocationList{
RevokedCertificateEntries: []x509.RevocationListEntry{
{
SerialNumber: big.NewInt(1),
ReasonCode: reasonCodeRemoveFromCRL,
RevocationTime: time.Now().Add(-time.Hour),
},
{
SerialNumber: big.NewInt(1),
ReasonCode: reasonCodeCertificateHold,
RevocationTime: time.Now(),
},
},
}
r, err := checkRevocation(cert, &crlutils.Bundle{BaseCRL: baseCRL, DeltaCRL: deltaCRL}, signingTime, "")
if err != nil {
t.Fatal(err)
}
if r.Result != result.ResultRevoked {
t.Fatalf("expected revoked, got %s", r.Result)
}
})
} }
func TestParseEntryExtension(t *testing.T) { func TestParseEntryExtension(t *testing.T) {
t.Run("unsupported critical extension", func(t *testing.T) { t.Run("unsupported critical extension", func(t *testing.T) {
entry := x509.RevocationListEntry{ entry := &x509.RevocationListEntry{
Extensions: []pkix.Extension{ Extensions: []pkix.Extension{
{ {
Id: []int{1, 2, 3}, Id: []int{1, 2, 3},
@ -630,7 +950,7 @@ func TestParseEntryExtension(t *testing.T) {
}) })
t.Run("valid extension", func(t *testing.T) { t.Run("valid extension", func(t *testing.T) {
entry := x509.RevocationListEntry{ entry := &x509.RevocationListEntry{
Extensions: []pkix.Extension{ Extensions: []pkix.Extension{
{ {
Id: []int{1, 2, 3}, Id: []int{1, 2, 3},
@ -652,7 +972,7 @@ func TestParseEntryExtension(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
entry := x509.RevocationListEntry{ entry := &x509.RevocationListEntry{
Extensions: []pkix.Extension{ Extensions: []pkix.Extension{
{ {
Id: oidInvalidityDate, Id: oidInvalidityDate,
@ -673,7 +993,7 @@ func TestParseEntryExtension(t *testing.T) {
t.Run("parse invalidityDate with error", func(t *testing.T) { t.Run("parse invalidityDate with error", func(t *testing.T) {
// invalid invalidityDate extension // invalid invalidityDate extension
entry := x509.RevocationListEntry{ entry := &x509.RevocationListEntry{
Extensions: []pkix.Extension{ Extensions: []pkix.Extension{
{ {
Id: oidInvalidityDate, Id: oidInvalidityDate,
@ -695,7 +1015,7 @@ func TestParseEntryExtension(t *testing.T) {
} }
invalidityDateBytes = append(invalidityDateBytes, 0x00) invalidityDateBytes = append(invalidityDateBytes, 0x00)
entry = x509.RevocationListEntry{ entry = &x509.RevocationListEntry{
Extensions: []pkix.Extension{ Extensions: []pkix.Extension{
{ {
Id: oidInvalidityDate, Id: oidInvalidityDate,
@ -722,7 +1042,7 @@ func marshalGeneralizedTimeToBytes(t time.Time) ([]byte, error) {
func TestSupported(t *testing.T) { func TestSupported(t *testing.T) {
t.Run("supported", func(t *testing.T) { t.Run("supported", func(t *testing.T) {
cert := &x509.Certificate{ cert := &x509.Certificate{
CRLDistributionPoints: []string{"http://example.com"}, CRLDistributionPoints: []string{"http://localhost.test"},
} }
if !Supported(cert) { if !Supported(cert) {
t.Fatal("expected supported") t.Fatal("expected supported")

View File

@ -17,6 +17,7 @@ package ocsp
import ( import (
"bytes" "bytes"
"context"
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
@ -52,7 +53,7 @@ const (
) )
// CertCheckStatus checks the revocation status of a certificate using OCSP // CertCheckStatus checks the revocation status of a certificate using OCSP
func CertCheckStatus(cert, issuer *x509.Certificate, opts CertCheckStatusOptions) *result.CertRevocationResult { func CertCheckStatus(ctx context.Context, cert, issuer *x509.Certificate, opts CertCheckStatusOptions) *result.CertRevocationResult {
if !Supported(cert) { if !Supported(cert) {
// OCSP not enabled for this certificate. // OCSP not enabled for this certificate.
return &result.CertRevocationResult{ return &result.CertRevocationResult{
@ -65,7 +66,7 @@ func CertCheckStatus(cert, issuer *x509.Certificate, opts CertCheckStatusOptions
serverResults := make([]*result.ServerResult, len(ocspURLs)) serverResults := make([]*result.ServerResult, len(ocspURLs))
for serverIndex, server := range ocspURLs { for serverIndex, server := range ocspURLs {
serverResult := checkStatusFromServer(cert, issuer, server, opts) serverResult := checkStatusFromServer(ctx, cert, issuer, server, opts)
if serverResult.Result == result.ResultOK || if serverResult.Result == result.ResultOK ||
serverResult.Result == result.ResultRevoked || serverResult.Result == result.ResultRevoked ||
(serverResult.Result == result.ResultUnknown && errors.Is(serverResult.Error, UnknownStatusError{})) { (serverResult.Result == result.ResultUnknown && errors.Is(serverResult.Error, UnknownStatusError{})) {
@ -84,7 +85,7 @@ func Supported(cert *x509.Certificate) bool {
return cert != nil && len(cert.OCSPServer) > 0 return cert != nil && len(cert.OCSPServer) > 0
} }
func checkStatusFromServer(cert, issuer *x509.Certificate, server string, opts CertCheckStatusOptions) *result.ServerResult { func checkStatusFromServer(ctx context.Context, cert, issuer *x509.Certificate, server string, opts CertCheckStatusOptions) *result.ServerResult {
// Check valid server // Check valid server
if serverURL, err := url.Parse(server); err != nil || !strings.EqualFold(serverURL.Scheme, "http") { 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 // This function is only able to check servers that are accessible via HTTP
@ -92,7 +93,7 @@ func checkStatusFromServer(cert, issuer *x509.Certificate, server string, opts C
} }
// Create OCSP Request // Create OCSP Request
resp, err := executeOCSPCheck(cert, issuer, server, opts) resp, err := executeOCSPCheck(ctx, cert, issuer, server, opts)
if err != nil { if err != nil {
// If there is a server error, attempt all servers before determining what to return // If there is a server error, attempt all servers before determining what to return
// to the user // to the user
@ -142,7 +143,7 @@ func extensionsToMap(extensions []pkix.Extension) map[string][]byte {
return extensionMap return extensionMap
} }
func executeOCSPCheck(cert, issuer *x509.Certificate, server string, opts CertCheckStatusOptions) (*ocsp.Response, error) { 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 // TODO: Look into other alternatives for specifying the Hash
// https://github.com/notaryproject/notation-core-go/issues/139 // https://github.com/notaryproject/notation-core-go/issues/139
// The following do not support SHA256 hashes: // The following do not support SHA256 hashes:
@ -168,24 +169,31 @@ func executeOCSPCheck(cert, issuer *x509.Certificate, server string, opts CertCh
if err != nil { if err != nil {
return nil, GenericError{Err: err} return nil, GenericError{Err: err}
} }
resp, err = opts.HTTPClient.Get(reqURL) 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 { } else {
resp, err = postRequest(ocspRequest, server, opts.HTTPClient) resp, err = postRequest(ctx, ocspRequest, server, opts.HTTPClient)
} }
} else { } else {
resp, err = postRequest(ocspRequest, server, opts.HTTPClient) resp, err = postRequest(ctx, ocspRequest, server, opts.HTTPClient)
} }
if err != nil { if err != nil {
var urlErr *url.Error var urlErr *url.Error
if errors.As(err, &urlErr) && urlErr.Timeout() { if errors.As(err, &urlErr) && urlErr.Timeout() {
return nil, TimeoutError{} return nil, TimeoutError{
timeout: opts.HTTPClient.Timeout,
}
} }
return nil, GenericError{Err: err} return nil, GenericError{Err: err}
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to retrieve OCSP: response had status code %d", resp.StatusCode) return nil, fmt.Errorf("failed to retrieve OCSP: response had status code %d", resp.StatusCode)
} }
@ -210,9 +218,13 @@ func executeOCSPCheck(cert, issuer *x509.Certificate, server string, opts CertCh
return ocsp.ParseResponseForCert(body, cert, issuer) return ocsp.ParseResponseForCert(body, cert, issuer)
} }
func postRequest(req []byte, server string, httpClient *http.Client) (*http.Response, error) { func postRequest(ctx context.Context, req []byte, server string, httpClient *http.Client) (*http.Response, error) {
reader := bytes.NewReader(req) httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, server, bytes.NewReader(req))
return httpClient.Post(server, "application/ocsp-request", reader) 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 { func toServerResult(server string, err error) *result.ServerResult {

View File

@ -14,9 +14,11 @@
package ocsp package ocsp
import ( import (
"context"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -75,6 +77,7 @@ func TestCheckStatus(t *testing.T) {
ocspServer := revokableCertTuple.Cert.OCSPServer[0] ocspServer := revokableCertTuple.Cert.OCSPServer[0]
revokableChain := []*x509.Certificate{revokableCertTuple.Cert, revokableIssuerTuple.Cert} revokableChain := []*x509.Certificate{revokableCertTuple.Cert, revokableIssuerTuple.Cert}
testChain := []testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple} testChain := []testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple}
ctx := context.Background()
t.Run("check non-revoked cert", func(t *testing.T) { t.Run("check non-revoked cert", func(t *testing.T) {
client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true) client := testhelper.MockClient(testChain, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
@ -83,7 +86,7 @@ func TestCheckStatus(t *testing.T) {
HTTPClient: client, HTTPClient: client,
} }
certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) certResult := CertCheckStatus(ctx, revokableChain[0], revokableChain[1], opts)
expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)}
validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t)
}) })
@ -94,7 +97,7 @@ func TestCheckStatus(t *testing.T) {
HTTPClient: client, HTTPClient: client,
} }
certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) certResult := CertCheckStatus(ctx, revokableChain[0], revokableChain[1], opts)
expectedCertResults := []*result.CertRevocationResult{{ expectedCertResults := []*result.CertRevocationResult{{
Result: result.ResultUnknown, Result: result.ResultUnknown,
ServerResults: []*result.ServerResult{ ServerResults: []*result.ServerResult{
@ -110,7 +113,7 @@ func TestCheckStatus(t *testing.T) {
HTTPClient: client, HTTPClient: client,
} }
certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) certResult := CertCheckStatus(ctx, revokableChain[0], revokableChain[1], opts)
expectedCertResults := []*result.CertRevocationResult{{ expectedCertResults := []*result.CertRevocationResult{{
Result: result.ResultRevoked, Result: result.ResultRevoked,
ServerResults: []*result.ServerResult{ ServerResults: []*result.ServerResult{
@ -127,13 +130,13 @@ func TestCheckStatus(t *testing.T) {
HTTPClient: client, HTTPClient: client,
} }
certResult := CertCheckStatus(revokableChain[0], revokableChain[1], opts) certResult := CertCheckStatus(ctx, revokableChain[0], revokableChain[1], opts)
expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)} expectedCertResults := []*result.CertRevocationResult{getOKCertResult(ocspServer)}
validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t) validateEquivalentCertResults([]*result.CertRevocationResult{certResult}, expectedCertResults, t)
}) })
t.Run("certificate doesn't support OCSP", func(t *testing.T) { t.Run("certificate doesn't support OCSP", func(t *testing.T) {
ocspResult := CertCheckStatus(&x509.Certificate{}, revokableIssuerTuple.Cert, CertCheckStatusOptions{}) ocspResult := CertCheckStatus(ctx, &x509.Certificate{}, revokableIssuerTuple.Cert, CertCheckStatusOptions{})
expectedResult := &result.CertRevocationResult{ expectedResult := &result.CertRevocationResult{
Result: result.ResultNonRevokable, Result: result.ResultNonRevokable,
ServerResults: []*result.ServerResult{toServerResult("", NoServerError{})}, ServerResults: []*result.ServerResult{toServerResult("", NoServerError{})},
@ -146,10 +149,11 @@ func TestCheckStatus(t *testing.T) {
func TestCheckStatusFromServer(t *testing.T) { func TestCheckStatusFromServer(t *testing.T) {
revokableCertTuple := testhelper.GetRevokableRSALeafCertificate() revokableCertTuple := testhelper.GetRevokableRSALeafCertificate()
revokableIssuerTuple := testhelper.GetRSARootCertificate() revokableIssuerTuple := testhelper.GetRSARootCertificate()
ctx := context.Background()
t.Run("server url is not http", func(t *testing.T) { t.Run("server url is not http", func(t *testing.T) {
server := "https://example.com" server := "https://localhost.test"
serverResult := checkStatusFromServer(revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{}) serverResult := checkStatusFromServer(ctx, revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{})
expectedResult := toServerResult(server, GenericError{Err: fmt.Errorf("OCSPServer protocol %s is not supported", "https")}) expectedResult := toServerResult(server, GenericError{Err: fmt.Errorf("OCSPServer protocol %s is not supported", "https")})
if serverResult.Result != expectedResult.Result { if serverResult.Result != expectedResult.Result {
t.Errorf("Expected Result to be %s, but got %s", expectedResult.Result, serverResult.Result) t.Errorf("Expected Result to be %s, but got %s", expectedResult.Result, serverResult.Result)
@ -165,8 +169,8 @@ func TestCheckStatusFromServer(t *testing.T) {
}) })
t.Run("request error", func(t *testing.T) { t.Run("request error", func(t *testing.T) {
server := "http://example.com" server := "http://localhost.test"
serverResult := checkStatusFromServer(revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{ serverResult := checkStatusFromServer(ctx, revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{
HTTPClient: &http.Client{ HTTPClient: &http.Client{
Transport: &failedTransport{}, Transport: &failedTransport{},
}, },
@ -179,8 +183,8 @@ func TestCheckStatusFromServer(t *testing.T) {
t.Run("ocsp expired", func(t *testing.T) { t.Run("ocsp expired", func(t *testing.T) {
client := testhelper.MockClient([]testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true) client := testhelper.MockClient([]testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
server := "http://example.com/expired_ocsp" server := "http://localhost.test/expired_ocsp"
serverResult := checkStatusFromServer(revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{ serverResult := checkStatusFromServer(ctx, revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{
HTTPClient: client, HTTPClient: client,
}) })
errorMessage := "expired OCSP response" errorMessage := "expired OCSP response"
@ -188,21 +192,105 @@ func TestCheckStatusFromServer(t *testing.T) {
t.Errorf("Expected Error to contain %v, but got %v", errorMessage, serverResult.Error) t.Errorf("Expected Error to contain %v, but got %v", errorMessage, serverResult.Error)
} }
}) })
}
func TestPostRequest(t *testing.T) { t.Run("ocsp request roundtrip failed", func(t *testing.T) {
t.Run("failed to execute request", func(t *testing.T) { client := testhelper.MockClient([]testhelper.RSACertTuple{revokableCertTuple, revokableIssuerTuple}, []ocsp.ResponseStatus{ocsp.Good}, nil, true)
_, err := postRequest(nil, "http://example.com", &http.Client{ server := "http://localhost.test"
Transport: &failedTransport{}, serverResult := checkStatusFromServer(nil, revokableCertTuple.Cert, revokableIssuerTuple.Cert, server, CertCheckStatusOptions{
HTTPClient: client,
}) })
if err == nil { errorMessage := "net/http: nil Context"
t.Errorf("Expected error, but got nil") 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)
} }
}) })
} }
type failedTransport struct{} 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) { 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") return nil, fmt.Errorf("failed to execute request")
} }

View File

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

View File

@ -0,0 +1,58 @@
// Copyright The Notary Project Authors.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package x509util
import (
"crypto/x509/pkix"
"encoding/asn1"
"testing"
)
func TestFindExtensionByOID(t *testing.T) {
oid1 := asn1.ObjectIdentifier{1, 2, 3, 4}
oid2 := asn1.ObjectIdentifier{1, 2, 3, 5}
extensions := []pkix.Extension{
{Id: oid1, Value: []byte("value1")},
{Id: oid2, Value: []byte("value2")},
}
tests := []struct {
name string
oid asn1.ObjectIdentifier
extensions []pkix.Extension
expected *pkix.Extension
}{
{
name: "Extension found",
oid: oid1,
extensions: extensions,
expected: &extensions[0],
},
{
name: "Extension not found",
oid: asn1.ObjectIdentifier{1, 2, 3, 6},
extensions: extensions,
expected: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FindExtensionByOID(tt.extensions, tt.oid)
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}

View File

@ -12,7 +12,8 @@
// limitations under the License. // limitations under the License.
// Package x509util provides the method to validate the certificate chain for a // Package x509util provides the method to validate the certificate chain for a
// specific purpose, including code signing and timestamping. // specific purpose, including code signing and timestamping. It also provides
// the method to find the extension by the given OID.
package x509util package x509util
import ( import (

View File

@ -16,6 +16,7 @@
package ocsp package ocsp
import ( import (
"context"
"crypto/x509" "crypto/x509"
"errors" "errors"
"net/http" "net/http"
@ -61,12 +62,13 @@ func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) {
// 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] = ocsp.CertCheckStatus(cert, opts.CertChain[i+1], certCheckStatusOptions) 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

View File

@ -410,13 +410,13 @@ 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")} 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")}
@ -633,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)
} }
} }
@ -641,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)
} }
} }

View File

@ -209,7 +209,7 @@ func (r *revocation) ValidateContext(ctx context.Context, validateContextOpts Va
} }
}() }()
ocspResult := ocsp.CertCheckStatus(cert, certChain[i+1], ocspOpts) ocspResult := ocsp.CertCheckStatus(ctx, cert, certChain[i+1], ocspOpts)
if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.Supported(cert) { if ocspResult != nil && ocspResult.Result == result.ResultUnknown && crl.Supported(cert) {
// try CRL check if OCSP serverResult is unknown // try CRL check if OCSP serverResult is unknown
serverResult := crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts) serverResult := crl.CertCheckStatus(ctx, cert, certChain[i+1], crlOpts)

View File

@ -823,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")}
@ -984,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)
} }
} }
@ -992,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)
} }
} }
@ -1069,7 +1069,7 @@ func TestCRL(t *testing.T) {
Result: result.ResultOK, Result: result.ResultOK,
ServerResults: []*result.ServerResult{{ ServerResults: []*result.ServerResult{{
Result: result.ResultOK, Result: result.ResultOK,
Server: "http://example.com/chain_crl/0", Server: "http://localhost.test/chain_crl/0",
}}, }},
RevocationMethod: result.RevocationMethodCRL, RevocationMethod: result.RevocationMethodCRL,
}, },
@ -1077,7 +1077,7 @@ func TestCRL(t *testing.T) {
Result: result.ResultOK, Result: result.ResultOK,
ServerResults: []*result.ServerResult{{ ServerResults: []*result.ServerResult{{
Result: result.ResultOK, Result: result.ResultOK,
Server: "http://example.com/chain_crl/1", Server: "http://localhost.test/chain_crl/1",
}}, }},
RevocationMethod: result.RevocationMethodCRL, RevocationMethod: result.RevocationMethodCRL,
}, },
@ -1128,7 +1128,7 @@ func TestCRL(t *testing.T) {
ServerResults: []*result.ServerResult{ ServerResults: []*result.ServerResult{
{ {
Result: result.ResultRevoked, Result: result.ResultRevoked,
Server: "http://example.com/chain_crl/0", Server: "http://localhost.test/chain_crl/0",
}, },
}, },
RevocationMethod: result.RevocationMethodCRL, RevocationMethod: result.RevocationMethodCRL,
@ -1138,7 +1138,7 @@ func TestCRL(t *testing.T) {
ServerResults: []*result.ServerResult{ ServerResults: []*result.ServerResult{
{ {
Result: result.ResultRevoked, Result: result.ResultRevoked,
Server: "http://example.com/chain_crl/1", Server: "http://localhost.test/chain_crl/1",
}, },
}, },
RevocationMethod: result.RevocationMethodCRL, RevocationMethod: result.RevocationMethodCRL,
@ -1164,7 +1164,9 @@ func TestCRL(t *testing.T) {
} }
revocationClient, err := NewWithOptions(Options{ revocationClient, err := NewWithOptions(Options{
OCSPHTTPClient: &http.Client{}, OCSPHTTPClient: &http.Client{
Transport: &serverErrorTransport{},
},
CRLFetcher: fetcher, CRLFetcher: fetcher,
CertChainPurpose: purpose.CodeSigning, CertChainPurpose: purpose.CodeSigning,
}) })
@ -1190,13 +1192,13 @@ func TestCRL(t *testing.T) {
ServerResults: []*result.ServerResult{ ServerResults: []*result.ServerResult{
{ {
Result: result.ResultUnknown, Result: result.ResultUnknown,
Server: "http://example.com/chain_ocsp/0", Server: "http://localhost.test/chain_ocsp/0",
Error: errors.New("failed to retrieve OCSP: response had status code 500"), Error: errors.New("failed to retrieve OCSP: response had status code 500"),
RevocationMethod: result.RevocationMethodOCSP, RevocationMethod: result.RevocationMethodOCSP,
}, },
{ {
Result: result.ResultRevoked, Result: result.ResultRevoked,
Server: "http://example.com/chain_crl/0", Server: "http://localhost.test/chain_crl/0",
RevocationMethod: result.RevocationMethodCRL, RevocationMethod: result.RevocationMethodCRL,
}, },
}, },
@ -1207,13 +1209,13 @@ func TestCRL(t *testing.T) {
ServerResults: []*result.ServerResult{ ServerResults: []*result.ServerResult{
{ {
Result: result.ResultUnknown, Result: result.ResultUnknown,
Server: "http://example.com/chain_ocsp/1", Server: "http://localhost.test/chain_ocsp/1",
Error: errors.New("failed to retrieve OCSP: response had status code 500"), Error: errors.New("failed to retrieve OCSP: response had status code 500"),
RevocationMethod: result.RevocationMethodOCSPFallbackCRL, RevocationMethod: result.RevocationMethodOCSPFallbackCRL,
}, },
{ {
Result: result.ResultRevoked, Result: result.ResultRevoked,
Server: "http://example.com/chain_crl/1", Server: "http://localhost.test/chain_crl/1",
RevocationMethod: result.RevocationMethodCRL, RevocationMethod: result.RevocationMethodCRL,
}, },
}, },
@ -1298,8 +1300,8 @@ type crlRoundTripper struct {
} }
func (rt *crlRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { func (rt *crlRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
// e.g. ocsp URL: http://example.com/chain_ocsp/0 // e.g. ocsp URL: http://localhost.test/chain_ocsp/0
// e.g. crl URL: http://example.com/chain_crl/0 // e.g. crl URL: http://localhost.test/chain_crl/0
parts := strings.Split(req.URL.Path, "/") parts := strings.Split(req.URL.Path, "/")
isOCSP := parts[len(parts)-2] == "chain_ocsp" isOCSP := parts[len(parts)-2] == "chain_ocsp"
@ -1309,7 +1311,7 @@ func (rt *crlRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
} }
// choose the cert suffix based on suffix of request url // choose the cert suffix based on suffix of request url
// e.g. http://example.com/chain_crl/0 -> 0 // e.g. http://localhost.test/chain_crl/0 -> 0
i, err := strconv.Atoi(parts[len(parts)-1]) i, err := strconv.Atoi(parts[len(parts)-1])
if err != nil { if err != nil {
return nil, err return nil, err
@ -1352,6 +1354,15 @@ func (t panicTransport) RoundTrip(req *http.Request) (*http.Response, error) {
panic("panic") 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) { func TestValidateContext(t *testing.T) {
r, err := NewWithOptions(Options{ r, err := NewWithOptions(Options{
OCSPHTTPClient: &http.Client{}, OCSPHTTPClient: &http.Client{},

View File

@ -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 { return KeySpec{}, &UnsupportedSigningKeyError{
case 2048, 3072, 4096: Msg: err.Error(),
return KeySpec{
Type: KeyTypeRSA,
Size: bitSize,
}, nil
default:
return KeySpec{}, &UnsupportedSigningKeyError{
Msg: fmt.Sprintf("rsa key size %d bits is not supported", bitSize),
}
}
case *ecdsa.PublicKey:
switch bitSize := key.Curve.Params().BitSize; bitSize {
case 256, 384, 521:
return KeySpec{
Type: KeyTypeEC,
Size: bitSize,
}, nil
default:
return KeySpec{}, &UnsupportedSigningKeyError{
Msg: fmt.Sprintf("ecdsa key size %d bits is not supported", bitSize),
}
} }
} }
return KeySpec{}, &UnsupportedSigningKeyError{ return ks, nil
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
} }

View File

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

View File

@ -14,6 +14,7 @@
package cose package cose
import ( import (
"context"
"crypto" "crypto"
"crypto/x509" "crypto/x509"
"errors" "errors"
@ -33,7 +34,7 @@ import (
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://rfc3161timestamp.globalsign.com/advanced" rfc3161TSAurl = "http://timestamp.digicert.com"
) )
var ( var (
@ -128,7 +129,7 @@ func TestSign(t *testing.T) {
} }
} }
t.Run("with timestmap countersignature request", func(t *testing.T) { t.Run("with timestamp countersignature request", func(t *testing.T) {
signRequest, err := newSignRequest("notary.x509", signature.KeyTypeRSA, 3072) signRequest, err := newSignRequest("notary.x509", signature.KeyTypeRSA, 3072)
if err != nil { if err != nil {
t.Fatalf("newSignRequest() failed. Error = %s", err) t.Fatalf("newSignRequest() failed. Error = %s", err)
@ -137,7 +138,7 @@ func TestSign(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
rootCerts, err := nx509.ReadCertificateFile("../../internal/timestamp/testdata/tsaRootCert.crt") rootCerts, err := nx509.ReadCertificateFile("../../internal/timestamp/testdata/tsaRootCert.cer")
if err != nil || len(rootCerts) == 0 { if err != nil || len(rootCerts) == 0 {
t.Fatal("failed to read root CA certificate:", err) t.Fatal("failed to read root CA certificate:", err)
} }
@ -341,11 +342,8 @@ func TestSignErrors(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("getSignRequest() failed. Error = %v", err) t.Fatalf("getSignRequest() failed. Error = %v", err)
} }
signRequest.Timestamper, err = tspclient.NewHTTPTimestamper(nil, "invalid") signRequest.Timestamper = &dummyTimestamper{}
if err != nil { expected := errors.New("timestamp: failed to timestamp")
t.Fatal(err)
}
expected := errors.New("timestamp: Post \"invalid\": unsupported protocol scheme \"\"")
encoded, err := env.Sign(signRequest) encoded, err := env.Sign(signRequest)
if !isErrEqual(expected, err) { if !isErrEqual(expected, err) {
t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err)
@ -1101,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")
}

View File

@ -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.

View File

@ -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())

View File

@ -14,6 +14,7 @@
package jws package jws
import ( import (
"context"
"crypto" "crypto"
"crypto/ecdsa" "crypto/ecdsa"
"crypto/rand" "crypto/rand"
@ -36,7 +37,7 @@ import (
"github.com/notaryproject/tspclient-go" "github.com/notaryproject/tspclient-go"
) )
const rfc3161TSAurl = "http://rfc3161timestamp.globalsign.com/advanced" 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 {
@ -266,11 +267,8 @@ func TestSignFailed(t *testing.T) {
signReq, err := getSignReq(signature.SigningSchemeX509, signer, nil) signReq, err := getSignReq(signature.SigningSchemeX509, signer, nil)
checkNoError(t, err) checkNoError(t, err)
signReq.Timestamper, err = tspclient.NewHTTPTimestamper(nil, "invalid") signReq.Timestamper = &dummyTimestamper{}
if err != nil { expected := errors.New("timestamp: failed to timestamp")
t.Fatal(err)
}
expected := errors.New("timestamp: Post \"invalid\": unsupported protocol scheme \"\"")
encoded, err := env.Sign(signReq) encoded, err := env.Sign(signReq)
if !isErrEqual(expected, err) { if !isErrEqual(expected, err) {
t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err) t.Fatalf("Sign() expects error: %v, but got: %v.", expected, err)
@ -343,7 +341,7 @@ func TestSignWithTimestamp(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
rootCerts, err := nx509.ReadCertificateFile("../../internal/timestamp/testdata/tsaRootCert.crt") rootCerts, err := nx509.ReadCertificateFile("../../internal/timestamp/testdata/tsaRootCert.cer")
if err != nil || len(rootCerts) == 0 { if err != nil || len(rootCerts) == 0 {
t.Fatal("failed to read root CA certificate:", err) t.Fatal("failed to read root CA certificate:", err)
} }
@ -687,3 +685,9 @@ func isErrEqual(wanted, got error) bool {
} }
return false return false
} }
type dummyTimestamper tspclient.Timestamp
func (dts *dummyTimestamper) Timestamp(context.Context, *tspclient.Request) (*tspclient.Response, error) {
return nil, errors.New("failed to timestamp")
}

View File

@ -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 _, cert := range signerInfo.CertificateChain {
for _, trust := range trustedCerts { for _, trust := range trustedCerts {
for _, cert := range signerInfo.CertificateChain {
if trust.Equal(cert) { if trust.Equal(cert) {
return trust, nil return trust, nil
} }

View File

@ -20,6 +20,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/notaryproject/notation-core-go/revocation"
"github.com/notaryproject/tspclient-go" "github.com/notaryproject/tspclient-go"
) )
@ -112,6 +113,11 @@ type SignRequest struct {
// TSARootCAs is the set of caller trusted TSA root certificates // TSARootCAs is the set of caller trusted TSA root certificates
TSARootCAs *x509.CertPool 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. // ctx is the caller context. It should only be modified via WithContext.
// It is unexported to prevent people from using Context wrong // It is unexported to prevent people from using Context wrong
// and mutating the contexts held by callers of the same request. // and mutating the contexts held by callers of the same request.

View File

@ -182,7 +182,7 @@ 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, false, 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)
} }
@ -192,11 +192,11 @@ func getRevokableRSAChainCertTuple(cn string, previous *RSACertTuple, index int,
template.IsCA = true template.IsCA = true
template.KeyUsage = x509.KeyUsageCertSign template.KeyUsage = x509.KeyUsageCertSign
if enabledOCSP { if enabledOCSP {
template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} template.OCSPServer = []string{fmt.Sprintf("http://localhost.test/chain_ocsp/%d", index)}
} }
if enabledCRL { if enabledCRL {
template.KeyUsage |= x509.KeyUsageCRLSign template.KeyUsage |= x509.KeyUsageCRLSign
template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d", index)} template.CRLDistributionPoints = []string{fmt.Sprintf("http://localhost.test/chain_crl/%d", index)}
} }
return getRSACertTupleWithTemplate(template, previous.PrivateKey, previous) return getRSACertTupleWithTemplate(template, previous.PrivateKey, previous)
} }
@ -220,10 +220,10 @@ func getRevokableRSALeafChainCertTuple(cn string, issuer *RSACertTuple, index in
template.IsCA = false template.IsCA = false
template.KeyUsage = x509.KeyUsageDigitalSignature template.KeyUsage = x509.KeyUsageDigitalSignature
if enabledOCSP { if enabledOCSP {
template.OCSPServer = []string{fmt.Sprintf("http://example.com/chain_ocsp/%d", index)} template.OCSPServer = []string{fmt.Sprintf("http://localhost.test/chain_ocsp/%d", index)}
} }
if enabledCRL { if enabledCRL {
template.CRLDistributionPoints = []string{fmt.Sprintf("http://example.com/chain_crl/%d", index)} template.CRLDistributionPoints = []string{fmt.Sprintf("http://localhost.test/chain_crl/%d", index)}
} }
return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer) return getRSACertTupleWithTemplate(template, issuer.PrivateKey, issuer)
} }

View File

@ -14,6 +14,7 @@
package x509 package x509
import ( import (
"bytes"
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
@ -34,12 +35,17 @@ func ValidateCodeSigningCertChain(certChain []*x509.Certificate, signingTime *ti
// For self-signed signing certificate (not a CA) // For self-signed signing certificate (not a CA)
if len(certChain) == 1 { if len(certChain) == 1 {
cert := certChain[0] cert := certChain[0]
if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { // check self-signed
return signedTimeError
}
if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { 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) 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 { if err := validateCodeSigningLeafCertificate(cert); err != nil {
return fmt.Errorf("invalid self-signed certificate. Error: %w", err) return fmt.Errorf("invalid self-signed certificate. Error: %w", err)
} }

View File

@ -19,6 +19,7 @@ import (
_ "embed" _ "embed"
"errors" "errors"
"os" "os"
"strings"
"testing" "testing"
"time" "time"
@ -200,6 +201,35 @@ 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}

View File

@ -20,7 +20,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/notaryproject/notation-core-go/signature" "github.com/notaryproject/notation-core-go/internal/algorithm"
) )
func isSelfSigned(cert *x509.Certificate) (bool, error) { func isSelfSigned(cert *x509.Certificate) (bool, error) {
@ -95,13 +95,10 @@ func validateLeafKeyUsage(cert *x509.Certificate) error {
} }
func validateSignatureAlgorithm(cert *x509.Certificate) error { func validateSignatureAlgorithm(cert *x509.Certificate) error {
keySpec, err := signature.ExtractKeySpec(cert) _, err := algorithm.ExtractKeySpec(cert)
if err != nil { if err != nil {
return fmt.Errorf("certificate with subject %q: %w", cert.Subject, err) return fmt.Errorf("certificate with subject %q: %w", cert.Subject, err)
} }
if keySpec.SignatureAlgorithm() == 0 {
return fmt.Errorf("certificate with subject %q: unsupported signature algorithm with key spec %+v", cert.Subject, keySpec)
}
return nil return nil
} }

View File

@ -14,6 +14,7 @@
package x509 package x509
import ( import (
"bytes"
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
@ -33,9 +34,14 @@ func ValidateTimestampingCertChain(certChain []*x509.Certificate) error {
// For self-signed signing certificate (not a CA) // For self-signed signing certificate (not a CA)
if len(certChain) == 1 { if len(certChain) == 1 {
cert := certChain[0] cert := certChain[0]
// check self-signed
if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { 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) 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 { if err := validateTimestampingLeafCertificate(cert); err != nil {
return fmt.Errorf("invalid self-signed certificate. Error: %w", err) return fmt.Errorf("invalid self-signed certificate. Error: %w", err)
} }

View File

@ -14,9 +14,18 @@
package x509 package x509
import ( import (
"crypto/rand"
"crypto/rsa"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
"encoding/asn1"
"math/big"
"strings"
"testing" "testing"
"time"
"github.com/notaryproject/notation-core-go/internal/oid"
"github.com/notaryproject/notation-core-go/testhelper"
) )
func TestValidTimestampingChain(t *testing.T) { func TestValidTimestampingChain(t *testing.T) {
@ -39,6 +48,42 @@ func TestValidTimestampingChain(t *testing.T) {
} }
} }
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) { func TestInvalidTimestampingChain(t *testing.T) {
timestamp_leaf, err := readSingleCertificate("testdata/timestamp_leaf.crt") timestamp_leaf, err := readSingleCertificate("testdata/timestamp_leaf.crt")
if err != nil { if err != nil {
@ -215,3 +260,47 @@ func TestEkuToString(t *testing.T) {
t.Fatalf("expected 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)
}