├── .github ├── .codecov.yml ├── ISSUE_TEMPLATE │ ├── bug-or-issue.yaml │ ├── config.yml │ └── feature-request.yaml └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── CODEOWNERS ├── LICENSE ├── README.md ├── SECURITY.md ├── algorithm.go ├── algorithm_test.go ├── bench_test.go ├── cbor.go ├── cbor_test.go ├── conformance_test.go ├── countersign.go ├── countersign_test.go ├── cwt.go ├── cwt_test.go ├── ecdsa.go ├── ecdsa_test.go ├── ed25519.go ├── ed25519_test.go ├── errors.go ├── example_test.go ├── fuzz_test.go ├── go.mod ├── go.sum ├── hash_envelope.go ├── hash_envelope_test.go ├── headers.go ├── headers_test.go ├── key.go ├── key_test.go ├── release-checklist.md ├── release-management.md ├── reports ├── NCC_Microsoft-go-cose-Report_2022-05-26_v1.0.pdf ├── README.md └── Trail-of-Bits_Microsoft-go-cose-Report_2022-07-26_v1.0.pdf ├── rsa.go ├── rsa_test.go ├── scripts └── licenses.sh ├── sign.go ├── sign1.go ├── sign1_test.go ├── sign_test.go ├── signer.go ├── signer_test.go ├── testdata ├── sign1-sign-0000.json ├── sign1-sign-0001.json ├── sign1-sign-0002.json ├── sign1-sign-0003.json ├── sign1-sign-0004.json ├── sign1-sign-0005.json ├── sign1-sign-0006.json ├── sign1-verify-0000.json ├── sign1-verify-0001.json ├── sign1-verify-0002.json ├── sign1-verify-0003.json ├── sign1-verify-0004.json ├── sign1-verify-0005.json ├── sign1-verify-0006.json ├── sign1-verify-negative-0000.json ├── sign1-verify-negative-0001.json ├── sign1-verify-negative-0002.json └── sign1-verify-negative-0003.json ├── verifier.go └── verifier_test.go /.github/.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: off 4 | project: 5 | default: 6 | target: 89% 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-or-issue.yaml: -------------------------------------------------------------------------------- 1 | # Copyright The Veraison Go-COSE Authors. 2 | # Licensed under the Mozilla Public License 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 | # https://www.mozilla.org/en-US/MPL/2.0/ 7 | 8 | # Permissions of this weak copyleft license are conditioned on making available 9 | # source code of licensed files and modifications of those files under the same license 10 | # (or in certain cases, one of the GNU licenses). Copyright and license notices must be preserved. 11 | # Contributors provide an express grant of patent rights. However, a larger work using 12 | # the licensed work may be distributed under different terms and without source code for 13 | # files added in the larger work. 14 | # 15 | # Unless required by applicable law or agreed to in writing, software 16 | # distributed under the License is distributed on an "AS IS" BASIS, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | # See the License for the specific language governing permissions and 19 | # limitations under the License. 20 | # 21 | # Original template copied from Notary Project https://github.com/notaryproject 22 | 23 | name: 🐛 Bug or Issue 24 | description: Something is not working as expected or not working at all! Report it here! 25 | labels: [bug, triage] 26 | body: 27 | - type: markdown 28 | attributes: 29 | value: | 30 | Thank you for taking the time to fill out this issue report. 🛑 Please check existing issues first before continuing: https://github.com/veraison/go-cose/issues 31 | - type: dropdown 32 | id: area 33 | validations: 34 | required: true 35 | attributes: 36 | label: "What is the areas you experience the issue in?" 37 | options: 38 | - Go-COSE Library 39 | - type: textarea 40 | id: verbatim 41 | validations: 42 | required: true 43 | attributes: 44 | label: "What is not working as expected?" 45 | description: "In your own words, describe what the issue is." 46 | - type: textarea 47 | id: expect 48 | validations: 49 | required: true 50 | attributes: 51 | label: "What did you expect to happen?" 52 | description: "A clear and concise description of what you expected to happen." 53 | - type: textarea 54 | id: reproduce 55 | validations: 56 | required: true 57 | attributes: 58 | label: "How can we reproduce it?" 59 | description: "Detailed steps to reproduce the behavior. Commands and their outputs are always helpful. If the bug is in a library, code snippets work as well." 60 | - type: textarea 61 | id: environment 62 | validations: 63 | required: true 64 | attributes: 65 | label: Describe your environment 66 | description: "Installation method (e.g. wget, curl, brew, apt-get, yum, chocolate, MSI) if applicable / OS version / Shell type (e.g. zsh, bash, cmd.exe, Bash on Windows) / Golang version if applicable" 67 | - type: textarea 68 | id: version 69 | validations: 70 | required: true 71 | attributes: 72 | label: What is the version of your Go-COSE Library? 73 | description: "For the libraries check the `go.mod` file." 74 | - type: markdown 75 | attributes: 76 | value: | 77 | If you want to contribute to this project, we will be happy to guide you through out contribution process especially when you already have a good proposal or understanding of how to fix this issue. Join us at https://veraison.zulipchat.com/. 78 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # Copyright The Veraison Go-COSE Authors. 2 | # Licensed under the Mozilla Public License 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 | # https://www.mozilla.org/en-US/MPL/2.0/ 7 | 8 | # Permissions of this weak copyleft license are conditioned on making available 9 | # source code of licensed files and modifications of those files under the same license 10 | # (or in certain cases, one of the GNU licenses). Copyright and license notices must be preserved. 11 | # Contributors provide an express grant of patent rights. However, a larger work using 12 | # the licensed work may be distributed under different terms and without source code for 13 | # files added in the larger work. 14 | # 15 | # Unless required by applicable law or agreed to in writing, software 16 | # distributed under the License is distributed on an "AS IS" BASIS, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | # See the License for the specific language governing permissions and 19 | # limitations under the License. 20 | # 21 | # Original template copied from Notary Project https://github.com/notaryproject 22 | 23 | blank_issues_enabled: true 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yaml: -------------------------------------------------------------------------------- 1 | # Copyright The Veraison Go-COSE Authors. 2 | # Licensed under the Mozilla Public License 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 | # https://www.mozilla.org/en-US/MPL/2.0/ 7 | 8 | # Permissions of this weak copyleft license are conditioned on making available 9 | # source code of licensed files and modifications of those files under the same license 10 | # (or in certain cases, one of the GNU licenses). Copyright and license notices must be preserved. 11 | # Contributors provide an express grant of patent rights. However, a larger work using 12 | # the licensed work may be distributed under different terms and without source code for 13 | # files added in the larger work. 14 | # 15 | # Unless required by applicable law or agreed to in writing, software 16 | # distributed under the License is distributed on an "AS IS" BASIS, 17 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18 | # See the License for the specific language governing permissions and 19 | # limitations under the License. 20 | # 21 | # Original template copied from Notary Project https://github.com/notaryproject 22 | 23 | name: 🚀 Feature Request 24 | description: Suggest an idea for this project. 25 | labels: [enhancement, triage] 26 | body: 27 | - type: markdown 28 | attributes: 29 | value: | 30 | Thank you for taking the time to suggest a useful feature for the project! 31 | - type: dropdown 32 | id: area 33 | validations: 34 | required: true 35 | attributes: 36 | label: "What is the areas you would like to add the new feature to?" 37 | options: 38 | - Go-COSE Library 39 | - type: textarea 40 | id: problem 41 | validations: 42 | required: true 43 | attributes: 44 | label: "Is your feature request related to a problem?" 45 | description: "A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]" 46 | - type: textarea 47 | id: solution 48 | validations: 49 | required: true 50 | attributes: 51 | label: "What solution do you propose?" 52 | description: "A clear and concise description of what you want to happen." 53 | - type: textarea 54 | id: alternatives 55 | validations: 56 | required: true 57 | attributes: 58 | label: "What alternatives have you considered?" 59 | description: "A clear and concise description of any alternative solutions or features you've considered." 60 | - type: textarea 61 | id: context 62 | validations: 63 | required: false 64 | attributes: 65 | label: "Any additional context?" 66 | description: "Add any other context or screenshots about the feature request here." 67 | - type: markdown 68 | attributes: 69 | value: | 70 | If you want to contribute to this project, we will be happy to guide you through out contribution process especially when you already have a good proposal or understanding of how to improve the functionality. Join us at https://veraison.zulipchat.com/. 71 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions - CI for Go to build & test. See ci-go-cover.yml and linters.yml for code coverage and linters. 2 | # Taken from: https://github.com/fxamacker/cbor/workflows/ci.yml (thanks!) 3 | name: ci 4 | on: [push, pull_request] 5 | jobs: 6 | 7 | # Test on various OS with default Go version. 8 | tests: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | go-version: [1.21, 1.22] 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Install Go 16 | uses: actions/setup-go@v3 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | check-latest: true 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | - name: Run tests 23 | run: go test -race -v -coverprofile=coverage.txt . 24 | - name: Upload coverage to codecov.io 25 | uses: codecov/codecov-action@v4 26 | env: 27 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | # test deps 17 | test/cose-rust/ 18 | test/cose-wg-examples/ 19 | 20 | # go deps 21 | vendor/ 22 | 23 | # OSX fs files 24 | .DS_Store 25 | /cose-fuzz.zip 26 | /workdir/ 27 | 28 | # Editor files 29 | .vscode/ 30 | .idea/ 31 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Do not delete linter settings. Linters like gocritic can be enabled on the command line. 2 | 3 | linters-settings: 4 | dupl: 5 | threshold: 100 6 | funlen: 7 | lines: 100 8 | statements: 50 9 | goconst: 10 | min-len: 2 11 | min-occurrences: 3 12 | gocritic: 13 | enabled-tags: 14 | - diagnostic 15 | - experimental 16 | - opinionated 17 | - performance 18 | - style 19 | disabled-checks: 20 | - dupImport # https://github.com/go-critic/go-critic/issues/845 21 | - ifElseChain 22 | - octalLiteral 23 | - paramTypeCombine 24 | - whyNoLint 25 | - wrapperFunc 26 | gofmt: 27 | simplify: false 28 | goimports: 29 | local-prefixes: github.com/fxamacker/cbor 30 | golint: 31 | min-confidence: 0 32 | govet: 33 | check-shadowing: true 34 | lll: 35 | line-length: 140 36 | maligned: 37 | suggest-new: true 38 | misspell: 39 | locale: US 40 | 41 | linters: 42 | disable-all: true 43 | enable: 44 | - deadcode 45 | - errcheck 46 | - goconst 47 | - gocyclo 48 | - gofmt 49 | - goimports 50 | - golint 51 | - gosec 52 | - govet 53 | - ineffassign 54 | - maligned 55 | - misspell 56 | - staticcheck 57 | - structcheck 58 | - typecheck 59 | - unconvert 60 | - unused 61 | - varcheck 62 | 63 | 64 | issues: 65 | # max-issues-per-linter default is 50. Set to 0 to disable limit. 66 | max-issues-per-linter: 0 67 | # max-same-issues default is 3. Set to 0 to disable limit. 68 | max-same-issues: 0 69 | # Excluding configuration per-path, per-linter, per-text and per-source 70 | exclude-rules: 71 | - path: _test\.go 72 | linters: 73 | - goconst 74 | - dupl 75 | - gomnd 76 | - lll 77 | - path: doc\.go 78 | linters: 79 | - goimports 80 | - gomnd 81 | - lll 82 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # To be kept in sync with: [community/OWNERS](https://github.com/veraison/community/blob/main/OWNERS) 2 | # and the GitHub Team: [go-cose-maintainers](https://github.com/orgs/veraison/teams/go-cose-maintainers) 3 | 4 | * @henkbirkholz @OR13 @qmuntal @roywill @setrofim @shizhMSFT @simonfrost-arm @SteveLasker @thomas-fossati @yogeshbdeshpande 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-cose 2 | 3 | [![go.dev](https://pkg.go.dev/badge/github.com/veraison/go-cose.svg)](https://pkg.go.dev/github.com/veraison/go-cose) 4 | [![tests](https://github.com/veraison/go-cose/workflows/ci/badge.svg)](https://github.com/veraison/go-cose/actions?query=workflow%3Aci) 5 | [![codecov](https://codecov.io/gh/veraison/go-cose/branch/main/graph/badge.svg?token=SL18TCTC03)](https://codecov.io/gh/veraison/go-cose) 6 | 7 | A golang library for the [COSE specification][cose-spec] 8 | 9 | ## Project Status 10 | 11 | The verasion/go-cose project is actively maintained. 12 | See [current releases](https://github.com/veraison/go-cose/releases). 13 | 14 | The project was *initially* forked from the upstream [mozilla-services/go-cose][mozilla-go-cose] project, however the Veraison and Mozilla maintainers have agreed to retire the mozilla-services/go-cose project and focus on [veraison/go-cose][veraison-go-cose] as the active project. 15 | 16 | We thank the [Mozilla maintainers and contributors][mozilla-contributors] for their great work that formed the base of the [veraison/go-cose][veraison-go-cose] project. 17 | 18 | ## Community 19 | 20 | The [veraison/go-cose](https://github.com/veraison/go-cose) project is an open source community effort. 21 | 22 | You can reach the go-cose community via:: 23 | 24 | - [Mailing List](veraison-project@confidentialcomputing.io) 25 | - Bi-weekly meetings: 08:00-09:00 Pacific 26 | - [Zoom meeting link](https://us02web.zoom.us/j/81054434992?pwd=YjNBU21seU5VcGdtVXY3VHVjS251Zz09) 27 | - [Calendar ics link](https://zoom.us/meeting/tZUtcu2srT8jE9YFubXn-lC9upuwUiiev52G/ics) 28 | - [Meeting Notes](https://veraison.zulipchat.com/#narrow/stream/317999-go-cose-meetings) 29 | - [Meeting Recordings](https://www.youtube.com/@go-cose-community3000) 30 | 31 | Participation in the go-cose community is governed by the Veraison [CODE_OF_CONDUCT.md](https://github.com/veraison/.github/blob/main/CODE_OF_CONDUCT.md) and [GOVERNANCE.md](https://github.com/veraison/community/blob/main/GOVERNANCE.md) 32 | 33 | ## Code of Conduct 34 | 35 | This project has adopted the [Contributor Covenant Code of Conduct](https://github.com/veraison/.github/blob/main/CODE_OF_CONDUCT.md). 36 | 37 | ## Installation 38 | 39 | go-cose is compatible with modern Go releases in module mode, with Go installed: 40 | 41 | ```bash 42 | go get github.com/veraison/go-cose 43 | ``` 44 | 45 | will resolve and add the package to the current development module, along with its dependencies. 46 | 47 | Alternatively the same can be achieved if you use import in a package: 48 | 49 | ```go 50 | import "github.com/veraison/go-cose" 51 | ``` 52 | 53 | and run `go get` without parameters. 54 | 55 | Finally, to use the top-of-trunk version of this repo, use the following command: 56 | 57 | ```bash 58 | go get github.com/veraison/go-cose@main 59 | ``` 60 | 61 | ## Usage 62 | 63 | ### Signing and Verification 64 | 65 | ```go 66 | import "github.com/veraison/go-cose" 67 | ``` 68 | 69 | Construct a new COSE_Sign1_Tagged message, then sign it using ECDSA w/ SHA-256 and finally marshal it. For example: 70 | 71 | ```go 72 | package main 73 | 74 | import ( 75 | "crypto/ecdsa" 76 | "crypto/elliptic" 77 | "crypto/rand" 78 | _ "crypto/sha256" 79 | 80 | "github.com/veraison/go-cose" 81 | ) 82 | 83 | func SignP256(data []byte) ([]byte, error) { 84 | // create a signer 85 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 86 | if err != nil { 87 | return nil, err 88 | } 89 | signer, err := cose.NewSigner(cose.AlgorithmES256, privateKey) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | // create message header 95 | headers := cose.Headers{ 96 | Protected: cose.ProtectedHeader{ 97 | cose.HeaderLabelAlgorithm: cose.AlgorithmES256, 98 | }, 99 | } 100 | 101 | // sign and marshal message 102 | return cose.Sign1(rand.Reader, signer, headers, data, nil) 103 | } 104 | ``` 105 | 106 | Verify a raw COSE_Sign1_Tagged message. For example: 107 | 108 | ```go 109 | package main 110 | 111 | import ( 112 | "crypto" 113 | _ "crypto/sha256" 114 | 115 | "github.com/veraison/go-cose" 116 | ) 117 | 118 | func VerifyP256(publicKey crypto.PublicKey, sig []byte) error { 119 | // create a verifier from a trusted private key 120 | verifier, err := cose.NewVerifier(cose.AlgorithmES256, publicKey) 121 | if err != nil { 122 | return err 123 | } 124 | 125 | // create a sign message from a raw COSE_Sign1 payload 126 | var msg cose.Sign1Message 127 | if err = msg.UnmarshalCBOR(sig); err != nil { 128 | return err 129 | } 130 | return msg.Verify(nil, verifier) 131 | } 132 | ``` 133 | 134 | See [example_test.go](./example_test.go) for more examples. 135 | 136 | #### Untagged Signing and Verification 137 | 138 | Untagged COSE_Sign1 messages can be signed and verified as above, using 139 | `cose.UntaggedSign1Message` instead of `cose.Sign1Message`. 140 | 141 | #### Signing and Verification of payload digest 142 | 143 | When `cose.NewSigner` is used with PS{256,384,512} or ES{256,384,512}, the returned signer 144 | can be casted to the `cose.DigestSigner` interface, whose `SignDigest` method signs an 145 | already digested message. 146 | 147 | When `cose.NewVerifier` is used with PS{256,384,512} or ES{256,384,512}, the returned verifier 148 | can be casted to the `cose.DigestVerifier` interface, whose `VerifyDigest` method verifies an 149 | already digested message. 150 | 151 | Please refer to [example_test.go](./example_test.go) for the API usage. 152 | 153 | ### About hashing 154 | 155 | `go-cose` does not import any hash package by its own to avoid linking unnecessary algorithms to the final binary. 156 | It is the the responsibility of the `go-cose` user to make the necessary hash functions available at runtime, i.e., 157 | by using a blank import: 158 | 159 | ```go 160 | import ( 161 | _ "crypto/sha256" 162 | _ "crypto/sha512" 163 | ) 164 | ``` 165 | 166 | These are the required packages for each built-in cose.Algorithm: 167 | 168 | - cose.AlgorithmPS256, cose.AlgorithmES256: `crypto/sha256` 169 | - cose.AlgorithmPS384, cose.AlgorithmPS512, cose.AlgorithmES384, cose.AlgorithmES512: `crypto/sha512` 170 | - cose.AlgorithmEdDSA: none 171 | 172 | ### Countersigning 173 | 174 | It is possible to countersign `cose.Sign1Message`, `cose.SignMessage`, `cose.Signature` and 175 | `cose.Countersignature` objects and add them as unprotected headers. In order to do so, first create 176 | a countersignature holder with `cose.NewCountersignature()` and call its `Sign` function passing 177 | the parent object which is going to be countersigned. Then assign the countersignature as an 178 | unprotected header `cose.HeaderLabelCounterSignatureV2` or, if preferred, maintain it as a 179 | detached countersignature. 180 | 181 | When verifying countersignatures, it is necessary to pass the parent object in the `Verify` function 182 | of the countersignature holder. 183 | 184 | See [example_test.go](./example_test.go) for examples. 185 | 186 | ## Features 187 | 188 | ### Signing and Verifying Objects 189 | 190 | go-cose supports two different signature structures: 191 | - [cose.Sign1Message](https://pkg.go.dev/github.com/veraison/go-cose#Sign1Message) implements [COSE_Sign1](https://datatracker.ietf.org/doc/html/rfc8152#section-4.2). 192 | - [cose.SignMessage](https://pkg.go.dev/github.com/veraison/go-cose#SignMessage) implements [COSE_Sign](https://datatracker.ietf.org/doc/html/rfc8152#section-4.1). 193 | > :warning: The COSE_Sign API is currently **EXPERIMENTAL** and may be changed or removed in a later release. In addition, the amount of functional and security testing it has received so far is significantly lower than the COSE_Sign1 API. 194 | 195 | ### Countersignatures 196 | 197 | go-cose supports [COSE_Countersignature](https://tools.ietf.org/html/rfc9338#section-3.1), check [cose.Countersignature](https://pkg.go.dev/github.com/veraison/go-cose#Countersignature). 198 | > :warning: The COSE_Countersignature API is currently **EXPERIMENTAL** and may be changed or removed in a later release. 199 | 200 | ### Built-in Algorithms 201 | 202 | go-cose has built-in supports the following algorithms: 203 | - PS{256,384,512}: RSASSA-PSS w/ SHA as defined in RFC 8230. 204 | - ES{256,384,512}: ECDSA w/ SHA as defined in RFC 8152. 205 | - Ed25519: PureEdDSA as defined in RFC 8152. 206 | 207 | ### Custom Algorithms 208 | 209 | It is possible to use custom algorithms with this library, for example: 210 | 211 | ```go 212 | package cose_test 213 | 214 | import ( 215 | "errors" 216 | "io" 217 | "testing" 218 | 219 | "github.com/cloudflare/circl/sign" 220 | "github.com/cloudflare/circl/sign/schemes" 221 | "github.com/veraison/go-cose" 222 | ) 223 | 224 | type customKeySigner struct { 225 | alg cose.Algorithm 226 | key sign.PrivateKey 227 | } 228 | 229 | func (ks *customKeySigner) Algorithm() cose.Algorithm { 230 | return ks.alg 231 | } 232 | 233 | func (ks *customKeySigner) Sign(rand io.Reader, content []byte) ([]byte, error) { 234 | suite := schemes.ByName("ML-DSA-44") 235 | return suite.Sign(ks.key, content, nil), nil 236 | } 237 | 238 | type customKeyVerifier struct { 239 | alg cose.Algorithm 240 | key sign.PublicKey 241 | } 242 | 243 | func (ks *customKeyVerifier) Algorithm() cose.Algorithm { 244 | return ks.alg 245 | } 246 | 247 | func (ks *customKeyVerifier) Verify(content []byte, signature []byte) error { 248 | suite := schemes.ByName("ML-DSA-44") 249 | valid := suite.Verify(ks.key, content, signature, nil) 250 | if !valid { 251 | return errors.New("Signature not from public key") 252 | } 253 | return nil 254 | } 255 | 256 | func TestCustomSigner(t *testing.T) { 257 | const ( 258 | COSE_ALG_ML_DSA_44 = -48 259 | ) 260 | suite := schemes.ByName("ML-DSA-44") 261 | var seed [32]byte // zero seed 262 | pub, priv := suite.DeriveKey(seed[:]) 263 | var ks cose.Signer = &customKeySigner{ 264 | alg: COSE_ALG_ML_DSA_44, 265 | key: priv, 266 | } 267 | var kv = customKeyVerifier{ 268 | alg: COSE_ALG_ML_DSA_44, 269 | key: pub, 270 | } 271 | 272 | headers := cose.Headers{ 273 | Protected: cose.ProtectedHeader{ 274 | cose.HeaderLabelAlgorithm: COSE_ALG_ML_DSA_44, 275 | cose.HeaderLabelKeyID: []byte("key-42"), 276 | }, 277 | } 278 | var payload = []byte("hello post quantum signatures") 279 | signature, _ := cose.Sign1(nil, ks, headers, payload, nil) 280 | var sign1 cose.Sign1Message 281 | _ = sign1.UnmarshalCBOR(signature) 282 | 283 | var verifier cose.Verifier = &kv 284 | verifyError := sign1.Verify(nil, verifier) 285 | 286 | if verifyError != nil { 287 | t.Fatalf("Verification failed") 288 | } else { 289 | // fmt.Println(cbor.Diagnose(signature)) 290 | // 18([ 291 | // <<{ 292 | // / alg / 1: -48, 293 | // / kid / 4: h'6B65792D3432'} 294 | // >>, 295 | // {}, 296 | // h'4974...722e', 297 | // h'cb5a...293b' 298 | // ]) 299 | } 300 | } 301 | ``` 302 | 303 | ### Integer Ranges 304 | 305 | CBOR supports integers in the range [-264, -1] ∪ [0, 264 - 1]. 306 | 307 | This does not map onto a single Go integer type. 308 | 309 | `go-cose` uses `int64` to encompass both positive and negative values to keep data sizes smaller and easy to use. 310 | 311 | The main effect is that integer label values in the [-264, -263 - 1] and the [263, 264 - 1] ranges, which are nominally valid 312 | per RFC 8152, are rejected by the go-cose library. 313 | 314 | ### Conformance Tests 315 | 316 | `go-cose` runs the [GlueCOSE](https://github.com/gluecose/test-vectors) test suite on every local `go test` execution. 317 | These are also executed on every CI job. 318 | 319 | ### Fuzz Tests 320 | 321 | `go-cose` implements several fuzz tests using [Go's native fuzzing](https://go.dev/doc/fuzz). 322 | 323 | Fuzzing requires Go 1.18 or higher, and can be executed as follows: 324 | 325 | ```bash 326 | go test -fuzz=FuzzSign1 327 | ``` 328 | 329 | ### Security Reviews 330 | 331 | `go-cose` undergoes periodic security review. The security review reports are located [here](./reports) 332 | 333 | [cose-spec]: https://datatracker.ietf.org/doc/rfc9052/ 334 | [mozilla-contributors]: https://github.com/mozilla-services/go-cose/graphs/contributors 335 | [mozilla-go-cose]: http://github.com/mozilla-services/go-cose 336 | [veraison-go-cose]: https://github.com/veraison/go-cose 337 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Process and Policy 2 | 3 | This document provides the details on the veraison/go-cose security policy and details the processes surrounding security handling. 4 | 5 | ## Supported Versions 6 | 7 | The current stable release of [go-cose][go-cose] is [v1.0.0][v1.0.0-release]. Please upgrade to [v1.0.0][v1.0.0-release] if you are using a pre-release version. 8 | 9 | | Version | Supported | 10 | | ------- | ------------------ | 11 | | [v1.0.0][v1.0.0-release] | Yes | 12 | 13 | ## Report A Vulnerability 14 | 15 | We’re extremely grateful for security researchers and users who report vulnerabilities 16 | to the [veraison/go-cose][go-cose] community. All reports are thoroughly investigated by a set of [veraison/go-cose maintainers][go-cose-maintainers]. 17 | 18 | To make a report please email the private security list at go-cose-security@googlegroups.com with details using the following template: 19 | 20 | ### Reporting Template 21 | 22 | ```console 23 | [TO:]: go-cose-security@googlegroups.com 24 | [SUBJECT]: go-cose Security Notification 25 | [BODY]: 26 | Release: v1.0.0 27 | 28 | Summary: 29 | A quick summary of the issue 30 | 31 | Impact: 32 | Details on how to reproduce the security issue. 33 | 34 | Contact: 35 | Information on who to contact for additional information 36 | ``` 37 | 38 | ### When To Send a Report 39 | 40 | You think you have found a vulnerability in the [veraison/go-cose][go-cose] project. 41 | 42 | ### Security Vulnerability Response 43 | 44 | Each report will be reviewed and receipt acknowledged in a timely manner. This will set off the security review process detailed below. 45 | 46 | Any vulnerability information shared with the security team stays within the [veraison/go-cose][go-cose] project and will not be shared with others unless it is necessary to fix the issue. Information is shared only on a need to know basis. 47 | 48 | We ask that vulnerability reporter(s) act in good faith by not disclosing the issue to others. And we strive to act in good faith by acting swiftly, and by justly crediting the vulnerability reporter(s) in writing (see [Public Disclosure](#public-disclosure)). 49 | 50 | As the security issue moves through triage, identification, and release the reporter of the security vulnerability will be notified. Additional questions about the vulnerability map may also be asked from the reporter. 51 | 52 | ### Public Disclosure 53 | 54 | A public disclosure of security vulnerabilities is released alongside release updates or details that fix the vulnerability. We try to fully disclose vulnerabilities once a mitigation strategy is available. Our goal is to perform a release and public disclosure quickly and in a timetable that works well for users. For example, a release may be ready on a Friday but for the sake of users may be delayed to a Monday. 55 | 56 | When needed, CVEs will be assigned to vulnerabilities. Due to the process and time it takes to obtain a CVE ID, disclosures will happen first. Once the disclosure is public the process will begin to obtain a CVE ID. Once the ID has been assigned the disclosure will be updated. 57 | 58 | If the vulnerability reporter would like their name and details shared as part of the disclosure process we are happy to. We will ask permission and for the way the reporter would like to be identified. We appreciate vulnerability reports and would like to credit reporters if they would like the credit. 59 | 60 | ## Security Team Membership 61 | 62 | The security team is made up of a subset of the Veraison project maintainers who are willing and able to respond to vulnerability reports. 63 | 64 | ### Responsibilities 65 | 66 | * Members MUST be active project maintainers on active (non-deprecated) Veraison projects as defined in the [governance](https://github.com/veraison/community/blob/main/GOVERNANCE.md) 67 | * Members SHOULD engage in each reported vulnerability, at a minimum to make sure it is being handled 68 | * Members MUST keep the vulnerability details private and only share on a need to know basis 69 | 70 | ### Membership 71 | 72 | New members are required to be active maintainers of Veraison projects who are willing to perform the responsibilities outlined above. The security team is a subset of the maintainers across Veraison sub-projects. Members can step down at any time and may join at any time. 73 | 74 | If at any time a security team member is found to be no longer an active maintainer on active Veraison sub-projects, this individual will be removed from the security team. 75 | 76 | ## Patch and Release Team 77 | 78 | When a vulnerability comes in and is acknowledged, a team - including maintainers of the Veraison project affected - will be assembled to patch the vulnerability, release an update, and publish the vulnerability disclosure. This may expand beyond the security team as needed but will stay within the pool of Veraison project maintainers. 79 | 80 | ## Disclosures 81 | 82 | Vulnerability disclosures are published to [security-advisories][security-advisories]. The disclosures will contain an overview, details about the vulnerability, a fix for the vulnerability that will typically be an update, and optionally a workaround if one is available. 83 | 84 | Disclosures will be published on the same day as a release fixing the vulnerability after the release is published. 85 | 86 | [go-cose]: https://github.com/veraison/go-cose 87 | [security-advisories]: https://github.com/veraison/go-cose/security/advisories 88 | [v1.0.0-release]: https://github.com/veraison/go-cose/releases/tag/v1.0.0 89 | [go-cose-maintainers]: https://github.com/veraison/community/blob/main/OWNERS 90 | -------------------------------------------------------------------------------- /algorithm.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "strconv" 6 | ) 7 | 8 | // Signature algorithms supported by this library. 9 | // 10 | // When using an algorithm which requires hashing, 11 | // make sure the associated hash function is linked to the binary. 12 | const ( 13 | // RSASSA-PSS w/ SHA-256 by RFC 8230. 14 | // Requires an available crypto.SHA256. 15 | AlgorithmPS256 Algorithm = -37 16 | 17 | // RSASSA-PSS w/ SHA-384 by RFC 8230. 18 | // Requires an available crypto.SHA384. 19 | AlgorithmPS384 Algorithm = -38 20 | 21 | // RSASSA-PSS w/ SHA-512 by RFC 8230. 22 | // Requires an available crypto.SHA512. 23 | AlgorithmPS512 Algorithm = -39 24 | 25 | // ECDSA w/ SHA-256 by RFC 8152. 26 | // Requires an available crypto.SHA256. 27 | AlgorithmES256 Algorithm = -7 28 | 29 | // ECDSA w/ SHA-384 by RFC 8152. 30 | // Requires an available crypto.SHA384. 31 | AlgorithmES384 Algorithm = -35 32 | 33 | // ECDSA w/ SHA-512 by RFC 8152. 34 | // Requires an available crypto.SHA512. 35 | AlgorithmES512 Algorithm = -36 36 | 37 | // PureEdDSA by RFC 8152. 38 | // 39 | // Deprecated: use [AlgorithmEdDSA] instead, which has 40 | // the same value but with a more accurate name. 41 | AlgorithmEd25519 Algorithm = -8 42 | 43 | // PureEdDSA by RFC 8152. 44 | AlgorithmEdDSA Algorithm = -8 45 | ) 46 | 47 | // Signature algorithms known, but not supported by this library. 48 | // 49 | // Signers and Verifiers requiring the algorithms below are not 50 | // directly supported by this library. They need to be provided 51 | // as an external [Signer] or [Verifier] implementation. 52 | // 53 | // An example use case where RS256 is allowed and used is in 54 | // WebAuthn: https://www.w3.org/TR/webauthn-2/#sctn-sample-registration. 55 | const ( 56 | // RSASSA-PKCS1-v1_5 using SHA-256 by RFC 8812. 57 | AlgorithmRS256 Algorithm = -257 58 | 59 | // RSASSA-PKCS1-v1_5 using SHA-384 by RFC 8812. 60 | AlgorithmRS384 Algorithm = -258 61 | 62 | // RSASSA-PKCS1-v1_5 using SHA-512 by RFC 8812. 63 | AlgorithmRS512 Algorithm = -259 64 | ) 65 | 66 | // Hash algorithms by RFC 9054. 67 | const ( 68 | // SHA-256 by RFC 9054. 69 | AlgorithmSHA256 Algorithm = -16 70 | 71 | // SHA-384 by RFC 9054. 72 | AlgorithmSHA384 Algorithm = -43 73 | 74 | // SHA-512 by RFC 9054. 75 | AlgorithmSHA512 Algorithm = -44 76 | ) 77 | 78 | // AlgorithmReserved represents a reserved algorithm value by RFC 9053. 79 | const AlgorithmReserved Algorithm = 0 80 | 81 | // Algorithm represents an IANA algorithm entry in the COSE Algorithms registry. 82 | // 83 | // # See Also 84 | // 85 | // COSE Algorithms: https://www.iana.org/assignments/cose/cose.xhtml#algorithms 86 | // 87 | // RFC 8152 section 16.4: https://datatracker.ietf.org/doc/html/rfc8152#section-16.4 88 | type Algorithm int64 89 | 90 | // String returns the name of the algorithm 91 | func (a Algorithm) String() string { 92 | switch a { 93 | case AlgorithmPS256: 94 | return "PS256" 95 | case AlgorithmPS384: 96 | return "PS384" 97 | case AlgorithmPS512: 98 | return "PS512" 99 | case AlgorithmRS256: 100 | return "RS256" 101 | case AlgorithmRS384: 102 | return "RS384" 103 | case AlgorithmRS512: 104 | return "RS512" 105 | case AlgorithmES256: 106 | return "ES256" 107 | case AlgorithmES384: 108 | return "ES384" 109 | case AlgorithmES512: 110 | return "ES512" 111 | case AlgorithmEdDSA: 112 | // As stated in RFC 8152 section 8.2, only the pure EdDSA version is 113 | // used for COSE. 114 | return "EdDSA" 115 | case AlgorithmReserved: 116 | return "Reserved" 117 | case AlgorithmSHA256: 118 | return "SHA-256" 119 | case AlgorithmSHA384: 120 | return "SHA-384" 121 | case AlgorithmSHA512: 122 | return "SHA-512" 123 | default: 124 | return "Algorithm(" + strconv.FormatInt(int64(a), 10) + ")" 125 | } 126 | } 127 | 128 | // hashFunc returns the hash associated with the algorithm supported by this 129 | // library. 130 | func (a Algorithm) hashFunc() crypto.Hash { 131 | switch a { 132 | case AlgorithmPS256, AlgorithmES256, AlgorithmSHA256: 133 | return crypto.SHA256 134 | case AlgorithmPS384, AlgorithmES384, AlgorithmSHA384: 135 | return crypto.SHA384 136 | case AlgorithmPS512, AlgorithmES512, AlgorithmSHA512: 137 | return crypto.SHA512 138 | default: 139 | return 0 140 | } 141 | } 142 | 143 | // computeHash computes the digest using the hash specified in the algorithm. 144 | func (a Algorithm) computeHash(data []byte) ([]byte, error) { 145 | return computeHash(a.hashFunc(), data) 146 | } 147 | 148 | // computeHash computes the digest using the given hash. 149 | func computeHash(h crypto.Hash, data []byte) ([]byte, error) { 150 | if !h.Available() { 151 | return nil, ErrUnavailableHashFunc 152 | } 153 | hh := h.New() 154 | if _, err := hh.Write(data); err != nil { 155 | return nil, err 156 | } 157 | return hh.Sum(nil), nil 158 | } 159 | -------------------------------------------------------------------------------- /algorithm_test.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "crypto/sha256" 6 | "hash" 7 | "io" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestAlgorithm_String(t *testing.T) { 13 | // run tests 14 | tests := []struct { 15 | alg Algorithm 16 | want string 17 | }{ 18 | {AlgorithmPS256, "PS256"}, 19 | {AlgorithmPS384, "PS384"}, 20 | {AlgorithmPS512, "PS512"}, 21 | {AlgorithmRS256, "RS256"}, 22 | {AlgorithmRS384, "RS384"}, 23 | {AlgorithmRS512, "RS512"}, 24 | {AlgorithmES256, "ES256"}, 25 | {AlgorithmES384, "ES384"}, 26 | {AlgorithmES512, "ES512"}, 27 | {AlgorithmEdDSA, "EdDSA"}, 28 | {AlgorithmReserved, "Reserved"}, 29 | {AlgorithmSHA256, "SHA-256"}, 30 | {AlgorithmSHA384, "SHA-384"}, 31 | {AlgorithmSHA512, "SHA-512"}, 32 | {7, "Algorithm(7)"}, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.want, func(t *testing.T) { 36 | if got := tt.alg.String(); got != tt.want { 37 | t.Errorf("Algorithm.String() = %v, want %v", got, tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | 43 | func TestAlgorithm_hashFunc(t *testing.T) { 44 | tests := []struct { 45 | alg Algorithm 46 | want crypto.Hash 47 | }{ 48 | {AlgorithmPS256, crypto.SHA256}, 49 | {AlgorithmPS384, crypto.SHA384}, 50 | {AlgorithmPS512, crypto.SHA512}, 51 | {AlgorithmRS256, 0}, // crypto.SHA256 but not supported as intended 52 | {AlgorithmRS384, 0}, // crypto.SHA384 but not supported as intended 53 | {AlgorithmRS512, 0}, // crypto.SHA512 but not supported as intended 54 | {AlgorithmES256, crypto.SHA256}, 55 | {AlgorithmES384, crypto.SHA384}, 56 | {AlgorithmES512, crypto.SHA512}, 57 | {AlgorithmEdDSA, 0}, 58 | {AlgorithmReserved, 0}, 59 | {AlgorithmSHA256, crypto.SHA256}, 60 | {AlgorithmSHA384, crypto.SHA384}, 61 | {AlgorithmSHA512, crypto.SHA512}, 62 | {7, 0}, 63 | } 64 | for _, tt := range tests { 65 | t.Run(tt.alg.String(), func(t *testing.T) { 66 | if got := tt.alg.hashFunc(); !reflect.DeepEqual(got, tt.want) { 67 | t.Errorf("Algorithm.hashFunc() = %v, want %v", got, tt.want) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func TestAlgorithm_computeHash(t *testing.T) { 74 | // run tests 75 | data := []byte("hello world") 76 | tests := []struct { 77 | name string 78 | alg Algorithm 79 | want []byte 80 | wantErr error 81 | }{ 82 | { 83 | name: "PS256", 84 | alg: AlgorithmPS256, 85 | want: []byte{ 86 | 0xb9, 0x4d, 0x27, 0xb9, 0x93, 0x4d, 0x3e, 0x08, 0xa5, 0x2e, 0x52, 0xd7, 0xda, 0x7d, 0xab, 0xfa, 87 | 0xc4, 0x84, 0xef, 0xe3, 0x7a, 0x53, 0x80, 0xee, 0x90, 0x88, 0xf7, 0xac, 0xe2, 0xef, 0xcd, 0xe9, 88 | }, 89 | }, 90 | { 91 | name: "PS384", 92 | alg: AlgorithmPS384, 93 | want: []byte{ 94 | 0xfd, 0xbd, 0x8e, 0x75, 0xa6, 0x7f, 0x29, 0xf7, 0x01, 0xa4, 0xe0, 0x40, 0x38, 0x5e, 0x2e, 0x23, 95 | 0x98, 0x63, 0x03, 0xea, 0x10, 0x23, 0x92, 0x11, 0xaf, 0x90, 0x7f, 0xcb, 0xb8, 0x35, 0x78, 0xb3, 96 | 0xe4, 0x17, 0xcb, 0x71, 0xce, 0x64, 0x6e, 0xfd, 0x08, 0x19, 0xdd, 0x8c, 0x08, 0x8d, 0xe1, 0xbd, 97 | }, 98 | }, 99 | { 100 | name: "PS512", 101 | alg: AlgorithmPS512, 102 | want: []byte{ 103 | 0x30, 0x9e, 0xcc, 0x48, 0x9c, 0x12, 0xd6, 0xeb, 0x4c, 0xc4, 0x0f, 0x50, 0xc9, 0x02, 0xf2, 0xb4, 104 | 0xd0, 0xed, 0x77, 0xee, 0x51, 0x1a, 0x7c, 0x7a, 0x9b, 0xcd, 0x3c, 0xa8, 0x6d, 0x4c, 0xd8, 0x6f, 105 | 0x98, 0x9d, 0xd3, 0x5b, 0xc5, 0xff, 0x49, 0x96, 0x70, 0xda, 0x34, 0x25, 0x5b, 0x45, 0xb0, 0xcf, 106 | 0xd8, 0x30, 0xe8, 0x1f, 0x60, 0x5d, 0xcf, 0x7d, 0xc5, 0x54, 0x2e, 0x93, 0xae, 0x9c, 0xd7, 0x6f, 107 | }, 108 | }, 109 | { 110 | name: "ES256", 111 | alg: AlgorithmES256, 112 | want: []byte{ 113 | 0xb9, 0x4d, 0x27, 0xb9, 0x93, 0x4d, 0x3e, 0x08, 0xa5, 0x2e, 0x52, 0xd7, 0xda, 0x7d, 0xab, 0xfa, 114 | 0xc4, 0x84, 0xef, 0xe3, 0x7a, 0x53, 0x80, 0xee, 0x90, 0x88, 0xf7, 0xac, 0xe2, 0xef, 0xcd, 0xe9, 115 | }, 116 | }, 117 | { 118 | name: "ES384", 119 | alg: AlgorithmES384, 120 | want: []byte{ 121 | 0xfd, 0xbd, 0x8e, 0x75, 0xa6, 0x7f, 0x29, 0xf7, 0x01, 0xa4, 0xe0, 0x40, 0x38, 0x5e, 0x2e, 0x23, 122 | 0x98, 0x63, 0x03, 0xea, 0x10, 0x23, 0x92, 0x11, 0xaf, 0x90, 0x7f, 0xcb, 0xb8, 0x35, 0x78, 0xb3, 123 | 0xe4, 0x17, 0xcb, 0x71, 0xce, 0x64, 0x6e, 0xfd, 0x08, 0x19, 0xdd, 0x8c, 0x08, 0x8d, 0xe1, 0xbd, 124 | }, 125 | }, 126 | { 127 | name: "ES512", 128 | alg: AlgorithmES512, 129 | want: []byte{ 130 | 0x30, 0x9e, 0xcc, 0x48, 0x9c, 0x12, 0xd6, 0xeb, 0x4c, 0xc4, 0x0f, 0x50, 0xc9, 0x02, 0xf2, 0xb4, 131 | 0xd0, 0xed, 0x77, 0xee, 0x51, 0x1a, 0x7c, 0x7a, 0x9b, 0xcd, 0x3c, 0xa8, 0x6d, 0x4c, 0xd8, 0x6f, 132 | 0x98, 0x9d, 0xd3, 0x5b, 0xc5, 0xff, 0x49, 0x96, 0x70, 0xda, 0x34, 0x25, 0x5b, 0x45, 0xb0, 0xcf, 133 | 0xd8, 0x30, 0xe8, 0x1f, 0x60, 0x5d, 0xcf, 0x7d, 0xc5, 0x54, 0x2e, 0x93, 0xae, 0x9c, 0xd7, 0x6f, 134 | }, 135 | }, 136 | { 137 | name: "Ed25519", 138 | alg: AlgorithmEdDSA, 139 | wantErr: ErrUnavailableHashFunc, 140 | }, 141 | { 142 | name: "unknown algorithm", 143 | alg: 0, 144 | wantErr: ErrUnavailableHashFunc, 145 | }, 146 | } 147 | for _, tt := range tests { 148 | t.Run(tt.name, func(t *testing.T) { 149 | got, err := tt.alg.computeHash(data) 150 | if err != tt.wantErr { 151 | t.Errorf("Algorithm.computeHash() error = %v, wantErr %v", err, tt.wantErr) 152 | return 153 | } 154 | if !reflect.DeepEqual(got, tt.want) { 155 | t.Errorf("Algorithm.computeHash() = %v, want %v", got, tt.want) 156 | } 157 | }) 158 | } 159 | } 160 | 161 | type badHash struct{} 162 | 163 | func badHashNew() hash.Hash { 164 | return &badHash{} 165 | } 166 | 167 | func (*badHash) Write(p []byte) (n int, err error) { 168 | return 0, io.EOF 169 | } 170 | 171 | func (*badHash) Sum(b []byte) []byte { 172 | return b 173 | } 174 | 175 | func (*badHash) Reset() {} 176 | 177 | func (*badHash) Size() int { 178 | return 0 179 | } 180 | func (*badHash) BlockSize() int { 181 | return 0 182 | } 183 | 184 | func Test_computeHash(t *testing.T) { 185 | crypto.RegisterHash(crypto.SHA256, badHashNew) 186 | defer crypto.RegisterHash(crypto.SHA256, sha256.New) 187 | 188 | _, err := computeHash(crypto.SHA256, nil) 189 | if err != io.EOF { 190 | t.Fatalf("computeHash() error = %v, wantErr %v", err, io.EOF) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package cose_test 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/veraison/go-cose" 8 | ) 9 | 10 | func newSign1Message() *cose.Sign1Message { 11 | return &cose.Sign1Message{ 12 | Headers: cose.Headers{ 13 | Protected: cose.ProtectedHeader{ 14 | cose.HeaderLabelAlgorithm: cose.AlgorithmES256, 15 | }, 16 | Unprotected: cose.UnprotectedHeader{ 17 | cose.HeaderLabelKeyID: []byte{0x01}, 18 | }, 19 | }, 20 | Payload: make([]byte, 100), 21 | Signature: make([]byte, 32), 22 | } 23 | } 24 | 25 | type noSigner struct{} 26 | 27 | func (noSigner) Algorithm() cose.Algorithm { 28 | return cose.AlgorithmES256 29 | } 30 | 31 | func (noSigner) Sign(_ io.Reader, digest []byte) ([]byte, error) { 32 | return digest, nil 33 | } 34 | 35 | func (noSigner) Verify(_, _ []byte) error { 36 | return nil 37 | } 38 | 39 | func BenchmarkSign1Message_MarshalCBOR(b *testing.B) { 40 | msg := newSign1Message() 41 | b.ReportAllocs() 42 | b.ResetTimer() 43 | for i := 0; i < b.N; i++ { 44 | _, err := msg.MarshalCBOR() 45 | if err != nil { 46 | b.Fatal(err) 47 | } 48 | } 49 | } 50 | 51 | func BenchmarkSign1Message_UnmarshalCBOR(b *testing.B) { 52 | data, err := newSign1Message().MarshalCBOR() 53 | if err != nil { 54 | b.Fatal(err) 55 | } 56 | b.ReportAllocs() 57 | b.ResetTimer() 58 | for i := 0; i < b.N; i++ { 59 | var m cose.Sign1Message 60 | err = m.UnmarshalCBOR(data) 61 | if err != nil { 62 | b.Fatal(err) 63 | } 64 | } 65 | } 66 | 67 | func BenchmarkSign1Message_Sign(b *testing.B) { 68 | msg := newSign1Message() 69 | b.ReportAllocs() 70 | b.ResetTimer() 71 | for i := 0; i < b.N; i++ { 72 | msg.Signature = nil 73 | err := msg.Sign(zeroSource{}, nil, noSigner{}) 74 | if err != nil { 75 | b.Fatal(err) 76 | } 77 | } 78 | } 79 | 80 | func BenchmarkSign1Message_Verify(b *testing.B) { 81 | msg := newSign1Message() 82 | b.ReportAllocs() 83 | b.ResetTimer() 84 | for i := 0; i < b.N; i++ { 85 | err := msg.Verify(nil, noSigner{}) 86 | if err != nil { 87 | b.Fatal(err) 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /cbor.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | 8 | "github.com/fxamacker/cbor/v2" 9 | ) 10 | 11 | // CBOR Tags for COSE signatures registered in the IANA "CBOR Tags" registry. 12 | // 13 | // Reference: https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml#tags 14 | const ( 15 | CBORTagSignMessage = 98 16 | CBORTagSign1Message = 18 17 | ) 18 | 19 | // Pre-configured modes for CBOR encoding and decoding. 20 | var ( 21 | encMode cbor.EncMode 22 | decMode cbor.DecMode 23 | decModeWithTagsForbidden cbor.DecMode 24 | ) 25 | 26 | func init() { 27 | var err error 28 | 29 | // init encode mode 30 | encOpts := cbor.EncOptions{ 31 | Sort: cbor.SortCoreDeterministic, // sort map keys 32 | IndefLength: cbor.IndefLengthForbidden, // no streaming 33 | } 34 | encMode, err = encOpts.EncMode() 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | // init decode mode 40 | decOpts := cbor.DecOptions{ 41 | DupMapKey: cbor.DupMapKeyEnforcedAPF, // duplicated key not allowed 42 | IndefLength: cbor.IndefLengthForbidden, // no streaming 43 | IntDec: cbor.IntDecConvertSigned, // decode CBOR uint/int to Go int64 44 | } 45 | decMode, err = decOpts.DecMode() 46 | if err != nil { 47 | panic(err) 48 | } 49 | decOpts.TagsMd = cbor.TagsForbidden 50 | decModeWithTagsForbidden, err = decOpts.DecMode() 51 | if err != nil { 52 | panic(err) 53 | } 54 | } 55 | 56 | // byteString represents a "bstr / nil" type. 57 | type byteString []byte 58 | 59 | // UnmarshalCBOR decodes data into a "bstr / nil" type. 60 | // It also ensures the data is of major type 2 since []byte can be alternatively 61 | // interpreted as an array of bytes. 62 | // 63 | // Note: `github.com/fxamacker/cbor/v2` considers the primitive value 64 | // `undefined` (major type 7, value 23) as nil, which is not recognized by COSE. 65 | // 66 | // Related Code: https://github.com/fxamacker/cbor/blob/v2.4.0/decode.go#L709 67 | // 68 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.3 69 | func (s *byteString) UnmarshalCBOR(data []byte) error { 70 | if s == nil { 71 | return errors.New("cbor: UnmarshalCBOR on nil byteString pointer") 72 | } 73 | if len(data) == 0 { 74 | return io.EOF // same error as returned by cbor.Unmarshal() 75 | } 76 | if bytes.Equal(data, []byte{0xf6}) { 77 | *s = nil 78 | return nil 79 | } 80 | if data[0]>>5 != 2 { // major type 2: bstr 81 | return errors.New("cbor: require bstr type") 82 | } 83 | return decModeWithTagsForbidden.Unmarshal(data, (*[]byte)(s)) 84 | } 85 | 86 | // deterministicBinaryString converts a bstr into the deterministic encoding. 87 | // 88 | // Reference: https://www.rfc-editor.org/rfc/rfc9052.html#section-9 89 | func deterministicBinaryString(data cbor.RawMessage) (cbor.RawMessage, error) { 90 | if len(data) == 0 { 91 | return nil, io.EOF 92 | } 93 | if data[0]>>5 != 2 { // major type 2: bstr 94 | return nil, errors.New("cbor: require bstr type") 95 | } 96 | 97 | // fast path: return immediately if bstr is already deterministic 98 | if err := decModeWithTagsForbidden.Wellformed(data); err != nil { 99 | return nil, err 100 | } 101 | ai := data[0] & 0x1f 102 | if ai < 24 { 103 | return data, nil 104 | } 105 | switch ai { 106 | case 24: 107 | if data[1] >= 24 { 108 | return data, nil 109 | } 110 | case 25: 111 | if data[1] != 0 { 112 | return data, nil 113 | } 114 | case 26: 115 | if data[1] != 0 || data[2] != 0 { 116 | return data, nil 117 | } 118 | case 27: 119 | if data[1] != 0 || data[2] != 0 || data[3] != 0 || data[4] != 0 { 120 | return data, nil 121 | } 122 | } 123 | 124 | // slow path: convert by re-encoding 125 | // error checking is not required since `data` has been validated 126 | var s []byte 127 | _ = decModeWithTagsForbidden.Unmarshal(data, &s) 128 | return encMode.Marshal(s) 129 | } 130 | -------------------------------------------------------------------------------- /cbor_test.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "bytes" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/fxamacker/cbor/v2" 9 | ) 10 | 11 | func Test_byteString_UnmarshalCBOR(t *testing.T) { 12 | // test nil pointer 13 | t.Run("nil byteString pointer", func(t *testing.T) { 14 | var s *byteString 15 | data := []byte{0x40} 16 | if err := s.UnmarshalCBOR(data); err == nil { 17 | t.Errorf("want error on nil *byteString") 18 | } 19 | }) 20 | 21 | // test others 22 | tests := []struct { 23 | name string 24 | data []byte 25 | want byteString 26 | wantErr string 27 | }{ 28 | { 29 | name: "valid string", 30 | data: []byte{0x43, 0x66, 0x6f, 0x6f}, 31 | want: []byte{0x66, 0x6f, 0x6f}, 32 | }, 33 | { 34 | name: "empty string", 35 | data: []byte{0x40}, 36 | want: []byte{}, 37 | }, 38 | { 39 | name: "nil string", 40 | data: []byte{0xf6}, 41 | want: nil, 42 | }, 43 | { 44 | name: "undefined string", 45 | data: []byte{0xf7}, 46 | wantErr: "cbor: require bstr type", 47 | }, 48 | { 49 | name: "nil CBOR data", 50 | data: nil, 51 | wantErr: "EOF", 52 | }, 53 | { 54 | name: "empty CBOR data", 55 | data: []byte{}, 56 | wantErr: "EOF", 57 | }, 58 | { 59 | name: "tagged string", 60 | data: []byte{0xc2, 0x40}, 61 | wantErr: "cbor: require bstr type", 62 | }, 63 | { 64 | name: "array of bytes", // issue #46 65 | data: []byte{0x82, 0x00, 0x1}, 66 | wantErr: "cbor: require bstr type", 67 | }, 68 | } 69 | for _, tt := range tests { 70 | t.Run(tt.name, func(t *testing.T) { 71 | var got byteString 72 | err := got.UnmarshalCBOR(tt.data) 73 | if err != nil && (err.Error() != tt.wantErr) { 74 | t.Errorf("byteString.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr) 75 | } else if err == nil && (tt.wantErr != "") { 76 | t.Errorf("byteString.UnmarshalCBOR() error = %v, wantErr %v", err, tt.wantErr) 77 | } 78 | if !bytes.Equal(got, tt.want) { 79 | t.Errorf("byteString.UnmarshalCBOR() = %v, want %v", got, tt.want) 80 | } 81 | }) 82 | } 83 | } 84 | 85 | func Test_deterministicBinaryString(t *testing.T) { 86 | gen := func(initial []byte, size int) []byte { 87 | data := make([]byte, size+len(initial)) 88 | copy(data, initial) 89 | return data 90 | } 91 | tests := []struct { 92 | name string 93 | data cbor.RawMessage 94 | want cbor.RawMessage 95 | wantErr bool 96 | }{ 97 | { 98 | name: "empty input", 99 | data: nil, 100 | wantErr: true, 101 | }, 102 | { 103 | name: "not bstr", 104 | data: []byte{0x00}, 105 | wantErr: true, 106 | }, 107 | { 108 | name: "short length", 109 | data: gen([]byte{0x57}, 23), 110 | want: gen([]byte{0x57}, 23), 111 | }, 112 | { 113 | name: "optimal uint8 length", 114 | data: gen([]byte{0x58, 0x18}, 24), 115 | want: gen([]byte{0x58, 0x18}, 24), 116 | }, 117 | { 118 | name: "non-optimal uint8 length", 119 | data: gen([]byte{0x58, 0x17}, 23), 120 | want: gen([]byte{0x57}, 23), 121 | }, 122 | { 123 | name: "optimal uint16 length", 124 | data: gen([]byte{0x59, 0x01, 0x00}, 256), 125 | want: gen([]byte{0x59, 0x01, 0x00}, 256), 126 | }, 127 | { 128 | name: "non-optimal uint16 length, target short", 129 | data: gen([]byte{0x59, 0x00, 0x17}, 23), 130 | want: gen([]byte{0x57}, 23), 131 | }, 132 | { 133 | name: "non-optimal uint16 length, target uint8", 134 | data: gen([]byte{0x59, 0x00, 0x18}, 24), 135 | want: gen([]byte{0x58, 0x18}, 24), 136 | }, 137 | { 138 | name: "optimal uint32 length", 139 | data: gen([]byte{0x5a, 0x00, 0x01, 0x00, 0x00}, 65536), 140 | want: gen([]byte{0x5a, 0x00, 0x01, 0x00, 0x00}, 65536), 141 | }, 142 | { 143 | name: "non-optimal uint32 length, target short", 144 | data: gen([]byte{0x5a, 0x00, 0x00, 0x00, 0x17}, 23), 145 | want: gen([]byte{0x57}, 23), 146 | }, 147 | { 148 | name: "non-optimal uint32 length, target uint8", 149 | data: gen([]byte{0x5a, 0x00, 0x00, 0x00, 0x18}, 24), 150 | want: gen([]byte{0x58, 0x18}, 24), 151 | }, 152 | { 153 | name: "non-optimal uint32 length, target uint16", 154 | data: gen([]byte{0x5a, 0x00, 0x00, 0x01, 0x00}, 256), 155 | want: gen([]byte{0x59, 0x01, 0x00}, 256), 156 | }, 157 | { 158 | name: "non-optimal uint64 length, target short", 159 | data: gen([]byte{0x5b, 160 | 0x00, 0x00, 0x00, 0x00, 161 | 0x00, 0x00, 0x00, 0x17, 162 | }, 23), 163 | want: gen([]byte{0x57}, 23), 164 | }, 165 | { 166 | name: "non-optimal uint64 length, target uint8", 167 | data: gen([]byte{0x5b, 168 | 0x00, 0x00, 0x00, 0x00, 169 | 0x00, 0x00, 0x00, 0x18, 170 | }, 24), 171 | want: gen([]byte{0x58, 0x18}, 24), 172 | }, 173 | { 174 | name: "non-optimal uint64 length, target uint16", 175 | data: gen([]byte{0x5b, 176 | 0x00, 0x00, 0x00, 0x00, 177 | 0x00, 0x00, 0x01, 0x00, 178 | }, 256), 179 | want: gen([]byte{0x59, 0x01, 0x00}, 256), 180 | }, 181 | { 182 | name: "non-optimal uint64 length, target uint32", 183 | data: gen([]byte{0x5b, 184 | 0x00, 0x00, 0x00, 0x00, 185 | 0x00, 0x01, 0x00, 0x00, 186 | }, 65536), 187 | want: gen([]byte{0x5a, 0x00, 0x01, 0x00, 0x00}, 65536), 188 | }, 189 | { 190 | name: "early EOF", 191 | data: gen([]byte{0x5b, 192 | 0x01, 0x00, 0x00, 0x00, 193 | 0x00, 0x00, 0x00, 0x00, 194 | }, 42), 195 | wantErr: true, 196 | }, 197 | } 198 | for _, tt := range tests { 199 | t.Run(tt.name, func(t *testing.T) { 200 | got, err := deterministicBinaryString(tt.data) 201 | if (err != nil) != tt.wantErr { 202 | t.Errorf("deterministicBinaryString() error = %v, wantErr %v", err, tt.wantErr) 203 | return 204 | } 205 | if !reflect.DeepEqual(got, tt.want) { 206 | t.Errorf("deterministicBinaryString() = %v, want %v", got, tt.want) 207 | } 208 | }) 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /conformance_test.go: -------------------------------------------------------------------------------- 1 | package cose_test 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/ecdsa" 7 | "crypto/elliptic" 8 | "crypto/rsa" 9 | _ "crypto/sha256" 10 | _ "crypto/sha512" 11 | "encoding/base64" 12 | "encoding/hex" 13 | "encoding/json" 14 | "errors" 15 | "math/big" 16 | "os" 17 | "path/filepath" 18 | "strings" 19 | "testing" 20 | 21 | "github.com/fxamacker/cbor/v2" 22 | "github.com/veraison/go-cose" 23 | ) 24 | 25 | type TestCase struct { 26 | UUID string `json:"uuid"` 27 | Title string `json:"title"` 28 | Description string `json:"description"` 29 | Key Key `json:"key"` 30 | Alg string `json:"alg"` 31 | Sign1 *Sign1 `json:"sign1::sign"` 32 | Verify1 *Verify1 `json:"sign1::verify"` 33 | } 34 | 35 | type Key map[string]string 36 | 37 | type Sign1 struct { 38 | Payload string `json:"payload"` 39 | ProtectedHeaders *CBOR `json:"protectedHeaders"` 40 | UnprotectedHeaders *CBOR `json:"unprotectedHeaders"` 41 | External string `json:"external"` 42 | Detached bool `json:"detached"` 43 | TBS CBOR `json:"tbsHex"` 44 | Output CBOR `json:"expectedOutput"` 45 | OutputLength int `json:"fixedOutputLength"` 46 | } 47 | 48 | type Verify1 struct { 49 | TaggedCOSESign1 CBOR `json:"taggedCOSESign1"` 50 | External string `json:"external"` 51 | Verify bool `json:"shouldVerify"` 52 | } 53 | 54 | type CBOR struct { 55 | CBORHex string `json:"cborHex"` 56 | CBORDiag string `json:"cborDiag"` 57 | } 58 | 59 | // Conformance samples are taken from 60 | // https://github.com/gluecose/test-vectors. 61 | var testCases = []struct { 62 | name string 63 | deterministic bool 64 | err string 65 | skip bool 66 | }{ 67 | {name: "sign1-sign-0000"}, 68 | {name: "sign1-sign-0001"}, 69 | {name: "sign1-sign-0002"}, 70 | {name: "sign1-sign-0003"}, 71 | {name: "sign1-sign-0004", deterministic: true}, 72 | {name: "sign1-sign-0005", deterministic: true}, 73 | {name: "sign1-sign-0006", deterministic: true}, 74 | {name: "sign1-verify-0000"}, 75 | {name: "sign1-verify-0001"}, 76 | {name: "sign1-verify-0002"}, 77 | {name: "sign1-verify-0003"}, 78 | {name: "sign1-verify-0004"}, 79 | {name: "sign1-verify-0005"}, 80 | {name: "sign1-verify-0006"}, 81 | {name: "sign1-verify-negative-0000", err: "cbor: invalid protected header: cbor: require bstr type"}, 82 | {name: "sign1-verify-negative-0001", err: "cbor: invalid protected header: cbor: protected header: require map type"}, 83 | {name: "sign1-verify-negative-0002", err: "cbor: invalid protected header: cbor: found duplicate map key \"1\" at map element index 1"}, 84 | {name: "sign1-verify-negative-0003", err: "cbor: invalid unprotected header: cbor: found duplicate map key \"4\" at map element index 1"}, 85 | } 86 | 87 | func TestConformance(t *testing.T) { 88 | for _, tt := range testCases { 89 | t.Run(tt.name, func(t *testing.T) { 90 | if tt.skip { 91 | t.SkipNow() 92 | } 93 | data, err := os.ReadFile(filepath.Join("testdata", tt.name+".json")) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | var tc TestCase 98 | err = json.Unmarshal(data, &tc) 99 | if err != nil { 100 | t.Fatal(err) 101 | } 102 | if tc.Sign1 != nil { 103 | testSign1(t, &tc, tt.deterministic) 104 | } else if tc.Verify1 != nil { 105 | testVerify1(t, &tc, tt.err) 106 | } else { 107 | t.Fatal("test case not supported") 108 | } 109 | }) 110 | } 111 | } 112 | 113 | func testVerify1(t *testing.T, tc *TestCase, wantErr string) { 114 | var err error 115 | defer func() { 116 | if tc.Verify1.Verify && err != nil { 117 | t.Fatal(err) 118 | } else if !tc.Verify1.Verify { 119 | if err == nil { 120 | t.Fatal("Verify1 should have failed") 121 | } 122 | if wantErr != "" { 123 | if got := err.Error(); !strings.Contains(got, wantErr) { 124 | t.Fatalf("error mismatch; want %q, got %q", wantErr, got) 125 | } 126 | } 127 | } 128 | }() 129 | var verifier cose.Verifier 130 | _, verifier, err = getSigner(tc, false) 131 | if err != nil { 132 | return 133 | } 134 | var sigMsg cose.Sign1Message 135 | err = sigMsg.UnmarshalCBOR(mustHexToBytes(tc.Verify1.TaggedCOSESign1.CBORHex)) 136 | if err != nil { 137 | return 138 | } 139 | var external []byte 140 | if tc.Verify1.External != "" { 141 | external = mustHexToBytes(tc.Verify1.External) 142 | } 143 | err = sigMsg.Verify(external, verifier) 144 | if tc.Verify1.Verify && err != nil { 145 | t.Fatal(err) 146 | } else if !tc.Verify1.Verify && err == nil { 147 | t.Fatal("Verify1 should have failed") 148 | } 149 | } 150 | 151 | func testSign1(t *testing.T, tc *TestCase, deterministic bool) { 152 | signer, verifier, err := getSigner(tc, true) 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | sig := tc.Sign1 157 | sigMsg := cose.NewSign1Message() 158 | sigMsg.Payload = mustHexToBytes(sig.Payload) 159 | sigMsg.Headers, err = decodeHeaders(mustHexToBytes(sig.ProtectedHeaders.CBORHex), mustHexToBytes(sig.UnprotectedHeaders.CBORHex)) 160 | if err != nil { 161 | t.Fatal(err) 162 | } 163 | var external []byte 164 | if sig.External != "" { 165 | external = mustHexToBytes(sig.External) 166 | } 167 | err = sigMsg.Sign(new(zeroSource), external, signer) 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | err = sigMsg.Verify(external, verifier) 172 | if err != nil { 173 | t.Fatal(err) 174 | } 175 | got, err := sigMsg.MarshalCBOR() 176 | if err != nil { 177 | t.Fatal(err) 178 | } 179 | want := mustHexToBytes(sig.Output.CBORHex) 180 | if !deterministic { 181 | got = got[:sig.OutputLength] 182 | want = want[:sig.OutputLength] 183 | } 184 | if !bytes.Equal(want, got) { 185 | t.Fatalf("unexpected output:\nwant: %x\n got: %x", want, got) 186 | } 187 | } 188 | 189 | func getSigner(tc *TestCase, private bool) (cose.Signer, cose.Verifier, error) { 190 | pkey, err := getKey(tc.Key, private) 191 | if err != nil { 192 | return nil, nil, err 193 | } 194 | alg := mustNameToAlg(tc.Alg) 195 | signer, err := cose.NewSigner(alg, pkey) 196 | if err != nil { 197 | return nil, nil, err 198 | } 199 | verifier, err := cose.NewVerifier(alg, pkey.Public()) 200 | if err != nil { 201 | return nil, nil, err 202 | } 203 | return signer, verifier, nil 204 | } 205 | 206 | func getKey(key Key, private bool) (crypto.Signer, error) { 207 | switch key["kty"] { 208 | case "RSA": 209 | pkey := &rsa.PrivateKey{ 210 | PublicKey: rsa.PublicKey{ 211 | N: mustBase64ToBigInt(key["n"]), 212 | E: mustBase64ToInt(key["e"]), 213 | }, 214 | } 215 | if private { 216 | pkey.D = mustBase64ToBigInt(key["d"]) 217 | pkey.Primes = []*big.Int{mustBase64ToBigInt(key["p"]), mustBase64ToBigInt(key["q"])} 218 | pkey.Precomputed = rsa.PrecomputedValues{ 219 | Dp: mustBase64ToBigInt(key["dp"]), 220 | Dq: mustBase64ToBigInt(key["dq"]), 221 | Qinv: mustBase64ToBigInt(key["qi"]), 222 | CRTValues: make([]rsa.CRTValue, 0), 223 | } 224 | } 225 | return pkey, nil 226 | case "EC": 227 | var c elliptic.Curve 228 | switch key["crv"] { 229 | case "P-224": 230 | c = elliptic.P224() 231 | case "P-256": 232 | c = elliptic.P256() 233 | case "P-384": 234 | c = elliptic.P384() 235 | case "P-521": 236 | c = elliptic.P521() 237 | default: 238 | return nil, errors.New("unsupported EC curve: " + key["crv"]) 239 | } 240 | pkey := &ecdsa.PrivateKey{ 241 | PublicKey: ecdsa.PublicKey{ 242 | X: mustBase64ToBigInt(key["x"]), 243 | Y: mustBase64ToBigInt(key["y"]), 244 | Curve: c, 245 | }, 246 | } 247 | if private { 248 | pkey.D = mustBase64ToBigInt(key["d"]) 249 | } 250 | return pkey, nil 251 | } 252 | return nil, errors.New("unsupported key type: " + key["kty"]) 253 | } 254 | 255 | // zeroSource is an io.Reader that returns an unlimited number of zero bytes. 256 | type zeroSource struct{} 257 | 258 | func (zeroSource) Read(b []byte) (n int, err error) { 259 | for i := range b { 260 | b[i] = 0 261 | } 262 | 263 | return len(b), nil 264 | } 265 | 266 | var encMode, _ = cbor.CanonicalEncOptions().EncMode() 267 | 268 | func decodeHeaders(protected, unprotected []byte) (hdr cose.Headers, err error) { 269 | // test-vectors encodes the protected header as a map instead of a map wrapped in a bstr. 270 | // UnmarshalFromRaw expects the former, so wrap the map here before passing it to UnmarshalFromRaw. 271 | hdr.RawProtected, err = encMode.Marshal(protected) 272 | if err != nil { 273 | return 274 | } 275 | hdr.RawUnprotected = unprotected 276 | err = hdr.UnmarshalFromRaw() 277 | return hdr, err 278 | } 279 | 280 | func mustBase64ToInt(s string) int { 281 | return int(mustBase64ToBigInt(s).Int64()) 282 | } 283 | 284 | func mustHexToBytes(s string) []byte { 285 | b, err := hex.DecodeString(s) 286 | if err != nil { 287 | panic(err) 288 | } 289 | return b 290 | } 291 | 292 | func mustBase64ToBigInt(s string) *big.Int { 293 | val, err := base64.RawURLEncoding.DecodeString(s) 294 | if err != nil { 295 | panic(err) 296 | } 297 | return new(big.Int).SetBytes(val) 298 | } 299 | 300 | // mustNameToAlg returns the algorithm associated to name. 301 | // The content of name is not defined in any RFC, 302 | // but it's what the test cases use to identify algorithms. 303 | func mustNameToAlg(name string) cose.Algorithm { 304 | switch name { 305 | case "PS256": 306 | return cose.AlgorithmPS256 307 | case "PS384": 308 | return cose.AlgorithmPS384 309 | case "PS512": 310 | return cose.AlgorithmPS512 311 | case "RS256": 312 | return cose.AlgorithmRS256 313 | case "RS384": 314 | return cose.AlgorithmRS384 315 | case "RS512": 316 | return cose.AlgorithmRS512 317 | case "ES256": 318 | return cose.AlgorithmES256 319 | case "ES384": 320 | return cose.AlgorithmES384 321 | case "ES512": 322 | return cose.AlgorithmES512 323 | } 324 | panic("algorithm name not found: " + name) 325 | } 326 | -------------------------------------------------------------------------------- /countersign.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/fxamacker/cbor/v2" 9 | ) 10 | 11 | // Countersignature represents a decoded COSE_Countersignature. 12 | // 13 | // Reference: https://tools.ietf.org/html/rfc9338#section-3.1 14 | // 15 | // # Experimental 16 | // 17 | // Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or 18 | // removed in a later release. 19 | type Countersignature Signature 20 | 21 | // NewCountersignature returns a Countersignature with header initialized. 22 | // 23 | // # Experimental 24 | // 25 | // Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or 26 | // removed in a later release. 27 | func NewCountersignature() *Countersignature { 28 | return (*Countersignature)(NewSignature()) 29 | } 30 | 31 | // MarshalCBOR encodes Countersignature into a COSE_Countersignature object. 32 | // 33 | // # Experimental 34 | // 35 | // Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or 36 | // removed in a later release. 37 | func (s *Countersignature) MarshalCBOR() ([]byte, error) { 38 | if s == nil { 39 | return nil, errors.New("cbor: MarshalCBOR on nil Countersignature pointer") 40 | } 41 | // COSE_Countersignature share the exact same format as COSE_Signature 42 | return (*Signature)(s).MarshalCBOR() 43 | } 44 | 45 | // UnmarshalCBOR decodes a COSE_Countersignature object into Countersignature. 46 | // 47 | // # Experimental 48 | // 49 | // Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or 50 | // removed in a later release. 51 | func (s *Countersignature) UnmarshalCBOR(data []byte) error { 52 | if s == nil { 53 | return errors.New("cbor: UnmarshalCBOR on nil Countersignature pointer") 54 | } 55 | // COSE_Countersignature share the exact same format as COSE_Signature 56 | return (*Signature)(s).UnmarshalCBOR(data) 57 | } 58 | 59 | // Sign signs a Countersignature using the provided Signer. 60 | // Signing a COSE_Countersignature requires the parent message to be completely 61 | // fulfilled. 62 | // 63 | // Reference: https://datatracker.ietf.org/doc/html/rfc9338#section-3.3 64 | // 65 | // # Experimental 66 | // 67 | // Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or 68 | // removed in a later release. 69 | func (s *Countersignature) Sign(rand io.Reader, signer Signer, parent any, external []byte) error { 70 | if s == nil { 71 | return errors.New("signing nil Countersignature") 72 | } 73 | if len(s.Signature) > 0 { 74 | return errors.New("Countersignature already has signature bytes") 75 | } 76 | 77 | // check algorithm if present. 78 | // `alg` header MUST present if there is no externally supplied data. 79 | alg := signer.Algorithm() 80 | if err := s.Headers.ensureSigningAlgorithm(alg, external); err != nil { 81 | return err 82 | } 83 | 84 | // sign the message 85 | toBeSigned, err := s.toBeSigned(parent, external) 86 | if err != nil { 87 | return err 88 | } 89 | sig, err := signer.Sign(rand, toBeSigned) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | s.Signature = sig 95 | return nil 96 | } 97 | 98 | // Verify verifies the countersignature, returning nil on success or a suitable 99 | // error if verification fails. 100 | // Verifying a COSE_Countersignature requires the parent message. 101 | // 102 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 103 | // 104 | // # Experimental 105 | // 106 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 107 | // later release. 108 | func (s *Countersignature) Verify(verifier Verifier, parent any, external []byte) error { 109 | if s == nil { 110 | return errors.New("verifying nil Countersignature") 111 | } 112 | if len(s.Signature) == 0 { 113 | return ErrEmptySignature 114 | } 115 | 116 | // check algorithm if present. 117 | // `alg` header MUST present if there is no externally supplied data. 118 | alg := verifier.Algorithm() 119 | err := s.Headers.ensureVerificationAlgorithm(alg, external) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | // verify the message 125 | toBeSigned, err := s.toBeSigned(parent, external) 126 | if err != nil { 127 | return err 128 | } 129 | return verifier.Verify(toBeSigned, s.Signature) 130 | } 131 | 132 | // toBeSigned returns ToBeSigned from COSE_Countersignature object. 133 | // 134 | // Reference: https://datatracker.ietf.org/doc/html/rfc9338#section-3.3 135 | func (s *Countersignature) toBeSigned(target any, external []byte) ([]byte, error) { 136 | var signProtected cbor.RawMessage 137 | signProtected, err := s.Headers.MarshalProtected() 138 | if err != nil { 139 | return nil, err 140 | } 141 | return countersignToBeSigned(false, target, signProtected, external) 142 | } 143 | 144 | // countersignToBeSigned constructs Countersign_structure, computes and returns 145 | // ToBeSigned. 146 | // 147 | // Reference: https://datatracker.ietf.org/doc/html/rfc9338#section-3.3 148 | func countersignToBeSigned(abbreviated bool, target any, signProtected cbor.RawMessage, external []byte) ([]byte, error) { 149 | // create a Countersign_structure and populate it with the appropriate 150 | // fields. 151 | // 152 | // Countersign_structure = [ 153 | // context : "CounterSignature" / "CounterSignature0" / 154 | // "CounterSignatureV2" / "CounterSignature0V2" /, 155 | // body_protected : empty_or_serialized_map, 156 | // ? sign_protected : empty_or_serialized_map, 157 | // external_aad : bstr, 158 | // payload : bstr, 159 | // ? other_fields : [+ bstr ] 160 | // ] 161 | 162 | var err error 163 | var bodyProtected cbor.RawMessage 164 | var otherFields []cbor.RawMessage 165 | var payload []byte 166 | 167 | switch t := target.(type) { 168 | case *SignMessage: 169 | return countersignToBeSigned(abbreviated, *t, signProtected, external) 170 | case SignMessage: 171 | if len(t.Signatures) == 0 { 172 | return nil, errors.New("SignMessage has no signatures yet") 173 | } 174 | bodyProtected, err = t.Headers.MarshalProtected() 175 | if err != nil { 176 | return nil, err 177 | } 178 | if t.Payload == nil { 179 | return nil, ErrMissingPayload 180 | } 181 | payload = t.Payload 182 | case *Sign1Message: 183 | return countersignToBeSigned(abbreviated, *t, signProtected, external) 184 | case Sign1Message: 185 | if len(t.Signature) == 0 { 186 | return nil, errors.New("Sign1Message was not signed yet") 187 | } 188 | bodyProtected, err = t.Headers.MarshalProtected() 189 | if err != nil { 190 | return nil, err 191 | } 192 | if t.Payload == nil { 193 | return nil, ErrMissingPayload 194 | } 195 | payload = t.Payload 196 | signature, err := encMode.Marshal(t.Signature) 197 | if err != nil { 198 | return nil, err 199 | } 200 | signature, err = deterministicBinaryString(signature) 201 | if err != nil { 202 | return nil, err 203 | } 204 | otherFields = []cbor.RawMessage{signature} 205 | case *Signature: 206 | return countersignToBeSigned(abbreviated, *t, signProtected, external) 207 | case Signature: 208 | bodyProtected, err = t.Headers.MarshalProtected() 209 | if err != nil { 210 | return nil, err 211 | } 212 | if len(t.Signature) == 0 { 213 | return nil, errors.New("Signature was not signed yet") 214 | } 215 | payload = t.Signature 216 | case *Countersignature: 217 | return countersignToBeSigned(abbreviated, *t, signProtected, external) 218 | case Countersignature: 219 | bodyProtected, err = t.Headers.MarshalProtected() 220 | if err != nil { 221 | return nil, err 222 | } 223 | if len(t.Signature) == 0 { 224 | return nil, errors.New("Countersignature was not signed yet") 225 | } 226 | payload = t.Signature 227 | default: 228 | return nil, fmt.Errorf("unsupported target %T", target) 229 | } 230 | 231 | var context string 232 | if len(otherFields) == 0 { 233 | if abbreviated { 234 | context = "CounterSignature0" 235 | } else { 236 | context = "CounterSignature" 237 | } 238 | } else { 239 | if abbreviated { 240 | context = "CounterSignature0V2" 241 | } else { 242 | context = "CounterSignatureV2" 243 | } 244 | } 245 | 246 | bodyProtected, err = deterministicBinaryString(bodyProtected) 247 | if err != nil { 248 | return nil, err 249 | } 250 | signProtected, err = deterministicBinaryString(signProtected) 251 | if err != nil { 252 | return nil, err 253 | } 254 | if external == nil { 255 | external = []byte{} 256 | } 257 | countersigStructure := []any{ 258 | context, // context 259 | bodyProtected, // body_protected 260 | signProtected, // sign_protected 261 | external, // external_aad 262 | payload, // payload 263 | } 264 | if len(otherFields) > 0 { 265 | countersigStructure = append(countersigStructure, otherFields) 266 | } 267 | 268 | // create the value ToBeSigned by encoding the Countersign_structure to a 269 | // byte string. 270 | return encMode.Marshal(countersigStructure) 271 | } 272 | 273 | // Countersign0 performs an abbreviated signature over a parent message using 274 | // the provided Signer. 275 | // 276 | // The parent message must be completely fulfilled prior signing. 277 | // 278 | // Reference: https://datatracker.ietf.org/doc/html/rfc9338#section-3.2 279 | // 280 | // # Experimental 281 | // 282 | // Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or 283 | // removed in a later release. 284 | func Countersign0(rand io.Reader, signer Signer, parent any, external []byte) ([]byte, error) { 285 | toBeSigned, err := countersignToBeSigned(true, parent, []byte{0x40}, external) 286 | if err != nil { 287 | return nil, err 288 | } 289 | return signer.Sign(rand, toBeSigned) 290 | } 291 | 292 | // VerifyCountersign0 verifies an abbreviated signature over a parent message 293 | // using the provided Verifier. 294 | // 295 | // Reference: https://datatracker.ietf.org/doc/html/rfc9338#section-3.2 296 | // 297 | // # Experimental 298 | // 299 | // Notice: The COSE Countersignature API is EXPERIMENTAL and may be changed or 300 | // removed in a later release. 301 | func VerifyCountersign0(verifier Verifier, parent any, external, signature []byte) error { 302 | toBeSigned, err := countersignToBeSigned(true, parent, []byte{0x40}, external) 303 | if err != nil { 304 | return err 305 | } 306 | return verifier.Verify(toBeSigned, signature) 307 | } 308 | -------------------------------------------------------------------------------- /cwt.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | // https://www.iana.org/assignments/cwt/cwt.xhtml#claims-registry 4 | const ( 5 | CWTClaimIssuer int64 = 1 6 | CWTClaimSubject int64 = 2 7 | CWTClaimAudience int64 = 3 8 | CWTClaimExpirationTime int64 = 4 9 | CWTClaimNotBefore int64 = 5 10 | CWTClaimIssuedAt int64 = 6 11 | CWTClaimCWTID int64 = 7 12 | CWTClaimConfirmation int64 = 8 13 | CWTClaimScope int64 = 9 14 | 15 | // TODO: the rest upon request 16 | ) 17 | 18 | // CWTClaims contains parameters that are to be cryptographically 19 | // protected. 20 | type CWTClaims map[any]any 21 | -------------------------------------------------------------------------------- /cwt_test.go: -------------------------------------------------------------------------------- 1 | package cose_test 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "fmt" 8 | 9 | "github.com/veraison/go-cose" 10 | ) 11 | 12 | // This example demonstrates signing and verifying COSE_Sign1 signatures. 13 | func ExampleCWTClaims() { 14 | // create message to be signed 15 | msgToSign := cose.NewSign1Message() 16 | msgToSign.Payload = []byte("hello world") 17 | msgToSign.Headers.Protected.SetAlgorithm(cose.AlgorithmES512) 18 | 19 | msgToSign.Headers.Protected.SetType("application/cwt") 20 | claims := cose.CWTClaims{ 21 | cose.CWTClaimIssuer: "issuer.example", 22 | cose.CWTClaimSubject: "subject.example", 23 | } 24 | msgToSign.Headers.Protected.SetCWTClaims(claims) 25 | 26 | msgToSign.Headers.Unprotected[cose.HeaderLabelKeyID] = []byte("1") 27 | 28 | // create a signer 29 | privateKey, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) 30 | if err != nil { 31 | panic(err) 32 | } 33 | signer, err := cose.NewSigner(cose.AlgorithmES512, privateKey) 34 | if err != nil { 35 | panic(err) 36 | } 37 | 38 | // sign message 39 | err = msgToSign.Sign(rand.Reader, nil, signer) 40 | if err != nil { 41 | panic(err) 42 | } 43 | sig, err := msgToSign.MarshalCBOR() 44 | // uncomment to review EDN 45 | // coseSign1Diagnostic, err := cbor.Diagnose(sig) 46 | // fmt.Println(coseSign1Diagnostic) 47 | if err != nil { 48 | panic(err) 49 | } 50 | fmt.Println("message signed") 51 | 52 | // create a verifier from a trusted public key 53 | publicKey := privateKey.Public() 54 | verifier, err := cose.NewVerifier(cose.AlgorithmES512, publicKey) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | // verify message 60 | var msgToVerify cose.Sign1Message 61 | err = msgToVerify.UnmarshalCBOR(sig) 62 | if err != nil { 63 | panic(err) 64 | } 65 | err = msgToVerify.Verify(nil, verifier) 66 | if err != nil { 67 | panic(err) 68 | } 69 | fmt.Println("message verified") 70 | 71 | // tamper the message and verification should fail 72 | msgToVerify.Payload = []byte("foobar") 73 | err = msgToVerify.Verify(nil, verifier) 74 | if err != cose.ErrVerification { 75 | panic(err) 76 | } 77 | fmt.Println("verification error as expected") 78 | // Output: 79 | // message signed 80 | // message verified 81 | // verification error as expected 82 | } 83 | -------------------------------------------------------------------------------- /ecdsa.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "encoding/asn1" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "math/big" 12 | ) 13 | 14 | // I2OSP - Integer-to-Octet-String primitive converts a nonnegative integer to 15 | // an octet string of a specified length `len(buf)`, and stores it in `buf`. 16 | // I2OSP is used for encoding ECDSA signature (r, s) into byte strings. 17 | // 18 | // Reference: https://datatracker.ietf.org/doc/html/rfc8017#section-4.1 19 | func I2OSP(x *big.Int, buf []byte) error { 20 | if x.Sign() < 0 { 21 | return errors.New("I2OSP: negative integer") 22 | } 23 | if x.BitLen() > len(buf)*8 { 24 | return errors.New("I2OSP: integer too large") 25 | } 26 | x.FillBytes(buf) 27 | return nil 28 | } 29 | 30 | // OS2IP - Octet-String-to-Integer primitive converts an octet string to a 31 | // nonnegative integer. 32 | // OS2IP is used for decoding ECDSA signature (r, s) from byte strings. 33 | // 34 | // Reference: https://datatracker.ietf.org/doc/html/rfc8017#section-4.2 35 | func OS2IP(x []byte) *big.Int { 36 | return new(big.Int).SetBytes(x) 37 | } 38 | 39 | // ecdsaKeySigner is a ECDSA-based signer with golang built-in keys. 40 | type ecdsaKeySigner struct { 41 | alg Algorithm 42 | key *ecdsa.PrivateKey 43 | } 44 | 45 | // Algorithm returns the signing algorithm associated with the private key. 46 | func (es *ecdsaKeySigner) Algorithm() Algorithm { 47 | return es.alg 48 | } 49 | 50 | // Sign signs message content with the private key using entropy from rand. 51 | // The resulting signature should follow RFC 8152 section 8.1, 52 | // although it does not follow the recommendation of being deterministic. 53 | // 54 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 55 | func (es *ecdsaKeySigner) Sign(rand io.Reader, content []byte) ([]byte, error) { 56 | digest, err := es.alg.computeHash(content) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return es.SignDigest(rand, digest) 61 | } 62 | 63 | // SignDigest signs message digest with the private key, possibly using 64 | // entropy from rand. 65 | // The resulting signature should follow RFC 8152 section 8. 66 | func (es *ecdsaKeySigner) SignDigest(rand io.Reader, digest []byte) ([]byte, error) { 67 | r, s, err := ecdsa.Sign(rand, es.key, digest) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return encodeECDSASignature(es.key.Curve, r, s) 72 | } 73 | 74 | // ecdsaKeySigner is a ECDSA based signer with a generic crypto.Signer. 75 | type ecdsaCryptoSigner struct { 76 | alg Algorithm 77 | key *ecdsa.PublicKey 78 | signer crypto.Signer 79 | } 80 | 81 | // Algorithm returns the signing algorithm associated with the private key. 82 | func (es *ecdsaCryptoSigner) Algorithm() Algorithm { 83 | return es.alg 84 | } 85 | 86 | // Sign signs message content with the private key, possibly using entropy from 87 | // rand. 88 | // The resulting signature should follow RFC 8152 section 8.1. 89 | // 90 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 91 | func (es *ecdsaCryptoSigner) Sign(rand io.Reader, content []byte) ([]byte, error) { 92 | digest, err := es.alg.computeHash(content) 93 | if err != nil { 94 | return nil, err 95 | } 96 | return es.SignDigest(rand, digest) 97 | } 98 | 99 | // SignDigest signs message digest with the private key, possibly using 100 | // entropy from rand. 101 | // The resulting signature should follow RFC 8152 section 8. 102 | func (es *ecdsaCryptoSigner) SignDigest(rand io.Reader, digest []byte) ([]byte, error) { 103 | sigASN1, err := es.signer.Sign(rand, digest, nil) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | // decode ASN.1 decoded signature 109 | var sig struct { 110 | R, S *big.Int 111 | } 112 | if _, err := asn1.Unmarshal(sigASN1, &sig); err != nil { 113 | return nil, err 114 | } 115 | 116 | // encode signature in the COSE form 117 | return encodeECDSASignature(es.key.Curve, sig.R, sig.S) 118 | } 119 | 120 | // encodeECDSASignature encodes (r, s) into a signature binary string using the 121 | // method specified by RFC 8152 section 8.1. 122 | // 123 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 124 | func encodeECDSASignature(curve elliptic.Curve, r, s *big.Int) ([]byte, error) { 125 | n := (curve.Params().N.BitLen() + 7) / 8 126 | sig := make([]byte, n*2) 127 | if err := I2OSP(r, sig[:n]); err != nil { 128 | return nil, err 129 | } 130 | if err := I2OSP(s, sig[n:]); err != nil { 131 | return nil, err 132 | } 133 | return sig, nil 134 | } 135 | 136 | // decodeECDSASignature decodes (r, s) from a signature binary string using the 137 | // method specified by RFC 8152 section 8.1. 138 | // 139 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 140 | func decodeECDSASignature(curve elliptic.Curve, sig []byte) (r, s *big.Int, err error) { 141 | n := (curve.Params().N.BitLen() + 7) / 8 142 | if len(sig) != n*2 { 143 | return nil, nil, fmt.Errorf("invalid signature length: %d", len(sig)) 144 | } 145 | return OS2IP(sig[:n]), OS2IP(sig[n:]), nil 146 | } 147 | 148 | // ecdsaVerifier is a ECDSA based verifier with golang built-in keys. 149 | type ecdsaVerifier struct { 150 | alg Algorithm 151 | key *ecdsa.PublicKey 152 | } 153 | 154 | // Algorithm returns the signing algorithm associated with the public key. 155 | func (ev *ecdsaVerifier) Algorithm() Algorithm { 156 | return ev.alg 157 | } 158 | 159 | // Verify verifies message content with the public key, returning nil for 160 | // success. 161 | // Otherwise, it returns [ErrVerification]. 162 | // 163 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 164 | func (ev *ecdsaVerifier) Verify(content []byte, signature []byte) error { 165 | // compute digest 166 | digest, err := ev.alg.computeHash(content) 167 | if err != nil { 168 | return err 169 | } 170 | return ev.VerifyDigest(digest, signature) 171 | } 172 | 173 | // VerifyDigest verifies message digest with the public key, returning nil 174 | // for success. 175 | // Otherwise, it returns [ErrVerification]. 176 | // 177 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 178 | func (ev *ecdsaVerifier) VerifyDigest(digest []byte, signature []byte) error { 179 | // verify signature 180 | r, s, err := decodeECDSASignature(ev.key.Curve, signature) 181 | if err != nil { 182 | return ErrVerification 183 | } 184 | if verified := ecdsa.Verify(ev.key, digest, r, s); !verified { 185 | return ErrVerification 186 | } 187 | return nil 188 | } 189 | -------------------------------------------------------------------------------- /ecdsa_test.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/sha256" 9 | "encoding/asn1" 10 | "errors" 11 | "io" 12 | "math/big" 13 | "reflect" 14 | "testing" 15 | ) 16 | 17 | func TestI2OSP(t *testing.T) { 18 | tests := []struct { 19 | name string 20 | x *big.Int 21 | buf []byte 22 | want []byte 23 | wantErr string 24 | }{ 25 | { 26 | name: "negative int", 27 | x: big.NewInt(-1), 28 | buf: make([]byte, 2), 29 | wantErr: "I2OSP: negative integer", 30 | }, 31 | { 32 | name: "integer too large #1", 33 | x: big.NewInt(1), 34 | buf: make([]byte, 0), 35 | wantErr: "I2OSP: integer too large", 36 | }, 37 | { 38 | name: "integer too large #2", 39 | x: big.NewInt(256), 40 | buf: make([]byte, 0), 41 | wantErr: "I2OSP: integer too large", 42 | }, 43 | { 44 | name: "integer too large #3", 45 | x: big.NewInt(1 << 24), 46 | buf: make([]byte, 3), 47 | wantErr: "I2OSP: integer too large", 48 | }, 49 | { 50 | name: "zero length string", 51 | x: big.NewInt(0), 52 | buf: make([]byte, 0), 53 | want: []byte{}, 54 | }, 55 | { 56 | name: "zero length string with nil buffer", 57 | x: big.NewInt(0), 58 | buf: nil, 59 | want: nil, 60 | }, 61 | { 62 | name: "I2OSP(0, 2)", 63 | x: big.NewInt(0), 64 | buf: make([]byte, 2), 65 | want: []byte{0x00, 0x00}, 66 | }, 67 | { 68 | name: "I2OSP(1, 2)", 69 | x: big.NewInt(1), 70 | buf: make([]byte, 2), 71 | want: []byte{0x00, 0x01}, 72 | }, 73 | { 74 | name: "I2OSP(255, 2)", 75 | x: big.NewInt(255), 76 | buf: make([]byte, 2), 77 | want: []byte{0x00, 0xff}, 78 | }, 79 | { 80 | name: "I2OSP(256, 2)", 81 | x: big.NewInt(256), 82 | buf: make([]byte, 2), 83 | want: []byte{0x01, 0x00}, 84 | }, 85 | { 86 | name: "I2OSP(65535, 2)", 87 | x: big.NewInt(65535), 88 | buf: make([]byte, 2), 89 | want: []byte{0xff, 0xff}, 90 | }, 91 | { 92 | name: "I2OSP(1234, 5)", 93 | x: big.NewInt(1234), 94 | buf: make([]byte, 5), 95 | want: []byte{0x00, 0x00, 0x00, 0x04, 0xd2}, 96 | }, 97 | } 98 | for _, tt := range tests { 99 | t.Run(tt.name, func(t *testing.T) { 100 | err := I2OSP(tt.x, tt.buf) 101 | if err != nil && (err.Error() != tt.wantErr) { 102 | t.Errorf("I2OSP() error = %v, wantErr %v", err, tt.wantErr) 103 | return 104 | } else if err == nil && (tt.wantErr != "") { 105 | t.Errorf("I2OSP() error = %v, wantErr %v", err, tt.wantErr) 106 | return 107 | } 108 | 109 | if got := tt.buf; (tt.wantErr == "") && !reflect.DeepEqual(got, tt.want) { 110 | t.Errorf("I2OSP() = %v, want %v", got, tt.want) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func TestOS2IP(t *testing.T) { 117 | tests := []struct { 118 | name string 119 | x []byte 120 | want *big.Int 121 | }{ 122 | { 123 | name: "zero length string", 124 | x: []byte{}, 125 | want: big.NewInt(0), 126 | }, 127 | { 128 | name: "OS2IP(I2OSP(0, 2))", 129 | x: []byte{0x00, 0x00}, 130 | want: big.NewInt(0), 131 | }, 132 | { 133 | name: "OS2IP(I2OSP(1, 2))", 134 | x: []byte{0x00, 0x01}, 135 | want: big.NewInt(1), 136 | }, 137 | { 138 | name: "OS2IP(I2OSP(255, 2))", 139 | x: []byte{0x00, 0xff}, 140 | want: big.NewInt(255), 141 | }, 142 | { 143 | name: "OS2IP(I2OSP(256, 2))", 144 | x: []byte{0x01, 0x00}, 145 | want: big.NewInt(256), 146 | }, 147 | { 148 | name: "OS2IP(I2OSP(65535, 2))", 149 | x: []byte{0xff, 0xff}, 150 | want: big.NewInt(65535), 151 | }, 152 | { 153 | name: "OS2IP(I2OSP(1234, 5))", 154 | x: []byte{0x00, 0x00, 0x00, 0x04, 0xd2}, 155 | want: big.NewInt(1234), 156 | }, 157 | } 158 | for _, tt := range tests { 159 | t.Run(tt.name, func(t *testing.T) { 160 | if got := OS2IP(tt.x); tt.want.Cmp(got) != 0 { 161 | t.Errorf("OS2IP() = %v, want %v", got, tt.want) 162 | } 163 | }) 164 | } 165 | } 166 | 167 | func generateTestECDSAKey(t *testing.T) *ecdsa.PrivateKey { 168 | key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 169 | if err != nil { 170 | t.Fatalf("ecdsa.GenerateKey() error = %v", err) 171 | } 172 | return key 173 | } 174 | 175 | func Test_ecdsaKeySigner(t *testing.T) { 176 | key := generateTestECDSAKey(t) 177 | testSignVerify(t, AlgorithmES256, key, false) 178 | } 179 | 180 | func Test_ecdsaCryptoSigner(t *testing.T) { 181 | wrappedKey := struct { 182 | crypto.Signer 183 | }{ 184 | Signer: generateTestECDSAKey(t), 185 | } 186 | testSignVerify(t, AlgorithmES256, wrappedKey, true) 187 | } 188 | 189 | func testSignVerify(t *testing.T, alg Algorithm, key crypto.Signer, isCryptoSigner bool) { 190 | // set up signer 191 | signer, err := NewSigner(alg, key) 192 | if err != nil { 193 | t.Fatalf("NewSigner() error = %v", err) 194 | } 195 | if isCryptoSigner { 196 | if _, ok := signer.(*ecdsaCryptoSigner); !ok { 197 | t.Fatalf("NewSigner() type = %v, want *ecdsaCryptoSigner", reflect.TypeOf(signer)) 198 | } 199 | } else { 200 | if _, ok := signer.(*ecdsaKeySigner); !ok { 201 | t.Fatalf("NewSigner() type = %v, want *ecdsaKeySigner", reflect.TypeOf(signer)) 202 | } 203 | } 204 | if got := signer.Algorithm(); got != alg { 205 | t.Fatalf("Algorithm() = %v, want %v", got, alg) 206 | } 207 | 208 | // sign / verify round trip 209 | // see also conformance_test.go for strict tests. 210 | content := []byte("hello world, مرحبا بالعالم") 211 | sig, err := signer.Sign(rand.Reader, content) 212 | if err != nil { 213 | t.Fatalf("Sign() error = %v", err) 214 | } 215 | 216 | verifier, err := NewVerifier(alg, key.Public()) 217 | if err != nil { 218 | t.Fatalf("NewVerifier() error = %v", err) 219 | } 220 | if err := verifier.Verify(content, sig); err != nil { 221 | t.Fatalf("Verifier.Verify() error = %v", err) 222 | } 223 | 224 | // digested sign/verify round trip 225 | dsigner, ok := signer.(DigestSigner) 226 | if !ok { 227 | t.Fatalf("signer is not a DigestSigner") 228 | } 229 | digest := sha256.Sum256(content) 230 | dsig, err := dsigner.SignDigest(rand.Reader, digest[:]) 231 | if err != nil { 232 | t.Fatalf("SignDigest() error = %v", err) 233 | } 234 | dverifier, ok := verifier.(DigestVerifier) 235 | if !ok { 236 | t.Fatalf("verifier is not a DigestVerifier") 237 | } 238 | if err := dverifier.VerifyDigest(digest[:], dsig); err != nil { 239 | t.Fatalf("VerifyDigest() error = %v", err) 240 | } 241 | } 242 | 243 | type ecdsaBadCryptoSigner struct { 244 | crypto.Signer 245 | signature []byte 246 | err error 247 | } 248 | 249 | func (s *ecdsaBadCryptoSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) (signature []byte, err error) { 250 | return s.signature, s.err 251 | } 252 | 253 | func Test_ecdsaBadCryptoSigner_SignFailure(t *testing.T) { 254 | badSigner := &ecdsaBadCryptoSigner{ 255 | Signer: generateTestECDSAKey(t), 256 | err: errors.New("sign failure"), 257 | } 258 | testSignFailure(t, AlgorithmES256, badSigner) 259 | } 260 | 261 | func Test_ecdsaBadCryptoSigner_BadSignature(t *testing.T) { 262 | key := generateTestECDSAKey(t) 263 | 264 | // nil signature 265 | badSigner := &ecdsaBadCryptoSigner{ 266 | Signer: key, 267 | signature: nil, 268 | } 269 | testSignFailure(t, AlgorithmES256, badSigner) 270 | 271 | // malformed signature: bad r 272 | sig, err := asn1.Marshal(struct { 273 | R, S *big.Int 274 | }{ 275 | R: big.NewInt(-1), 276 | S: big.NewInt(1), 277 | }) 278 | if err != nil { 279 | t.Fatalf("asn1.Marshal() error = %v", err) 280 | } 281 | badSigner = &ecdsaBadCryptoSigner{ 282 | Signer: key, 283 | signature: sig, 284 | } 285 | testSignFailure(t, AlgorithmES256, badSigner) 286 | 287 | // malformed signature: bad s 288 | sig, err = asn1.Marshal(struct { 289 | R, S *big.Int 290 | }{ 291 | R: big.NewInt(1), 292 | S: big.NewInt(-1), 293 | }) 294 | if err != nil { 295 | t.Fatalf("asn1.Marshal() error = %v", err) 296 | } 297 | badSigner = &ecdsaBadCryptoSigner{ 298 | Signer: key, 299 | signature: sig, 300 | } 301 | testSignFailure(t, AlgorithmES256, badSigner) 302 | } 303 | 304 | func Test_ecdsaKeySigner_SignHashFailure(t *testing.T) { 305 | key := generateTestECDSAKey(t) 306 | crypto.RegisterHash(crypto.SHA256, badHashNew) 307 | defer crypto.RegisterHash(crypto.SHA256, sha256.New) 308 | testSignFailure(t, AlgorithmES256, key) 309 | } 310 | 311 | func Test_ecdsaCryptoSigner_SignHashFailure(t *testing.T) { 312 | wrappedKey := struct { 313 | crypto.Signer 314 | }{ 315 | Signer: generateTestECDSAKey(t), 316 | } 317 | crypto.RegisterHash(crypto.SHA256, badHashNew) 318 | defer crypto.RegisterHash(crypto.SHA256, sha256.New) 319 | testSignFailure(t, AlgorithmES256, wrappedKey) 320 | } 321 | 322 | func testSignFailure(t *testing.T, alg Algorithm, key crypto.Signer) { 323 | signer, err := NewSigner(alg, key) 324 | if err != nil { 325 | t.Fatalf("NewSigner() error = %v", err) 326 | } 327 | 328 | content := []byte("hello world") 329 | if _, err = signer.Sign(rand.Reader, content); err == nil { 330 | t.Fatalf("Sign() error = nil, wantErr true") 331 | } 332 | } 333 | 334 | func Test_ecdsaVerifier_Verify_Success(t *testing.T) { 335 | // generate key 336 | alg := AlgorithmES256 337 | key := generateTestECDSAKey(t) 338 | 339 | // generate a valid signature 340 | content, sig := signTestData(t, alg, key) 341 | 342 | // set up verifier 343 | verifier, err := NewVerifier(alg, key.Public()) 344 | if err != nil { 345 | t.Fatalf("NewVerifier() error = %v", err) 346 | } 347 | if _, ok := verifier.(*ecdsaVerifier); !ok { 348 | t.Fatalf("NewVerifier() type = %v, want *ecdsaVerifier", reflect.TypeOf(verifier)) 349 | } 350 | if got := verifier.Algorithm(); got != alg { 351 | t.Fatalf("Algorithm() = %v, want %v", got, alg) 352 | } 353 | 354 | // verify round trip 355 | if err := verifier.Verify(content, sig); err != nil { 356 | t.Fatalf("ecdsaVerifier.Verify() error = %v", err) 357 | } 358 | } 359 | 360 | func Test_ecdsaVerifier_Verify_AlgorithmMismatch(t *testing.T) { 361 | // generate key 362 | alg := AlgorithmES256 363 | key := generateTestECDSAKey(t) 364 | 365 | // generate a valid signature 366 | content, sig := signTestData(t, alg, key) 367 | 368 | // set up verifier with a different algorithm 369 | verifier := &ecdsaVerifier{ 370 | alg: AlgorithmES512, 371 | key: &key.PublicKey, 372 | } 373 | 374 | // verification should fail on algorithm mismatch 375 | if err := verifier.Verify(content, sig); err != ErrVerification { 376 | t.Fatalf("ecdsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification) 377 | } 378 | } 379 | 380 | func Test_ecdsaVerifier_Verify_KeyMismatch(t *testing.T) { 381 | // generate key 382 | alg := AlgorithmES256 383 | key := generateTestECDSAKey(t) 384 | 385 | // generate a valid signature 386 | content, sig := signTestData(t, alg, key) 387 | 388 | // set up verifier with a different key / new key 389 | key = generateTestECDSAKey(t) 390 | verifier := &ecdsaVerifier{ 391 | alg: alg, 392 | key: &key.PublicKey, 393 | } 394 | 395 | // verification should fail on key mismatch 396 | if err := verifier.Verify(content, sig); err != ErrVerification { 397 | t.Fatalf("ecdsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification) 398 | } 399 | } 400 | 401 | func Test_ecdsaVerifier_Verify_InvalidSignature(t *testing.T) { 402 | // generate key 403 | alg := AlgorithmES256 404 | key := generateTestECDSAKey(t) 405 | 406 | // generate a valid signature with a tampered one 407 | content, sig := signTestData(t, alg, key) 408 | tamperedSig := make([]byte, len(sig)) 409 | copy(tamperedSig, sig) 410 | tamperedSig[0]++ 411 | 412 | // set up verifier with a different algorithm 413 | verifier := &ecdsaVerifier{ 414 | alg: alg, 415 | key: &key.PublicKey, 416 | } 417 | 418 | // verification should fail on invalid signature 419 | tests := []struct { 420 | name string 421 | signature []byte 422 | }{ 423 | { 424 | name: "nil signature", 425 | signature: nil, 426 | }, 427 | { 428 | name: "empty signature", 429 | signature: []byte{}, 430 | }, 431 | { 432 | name: "incomplete signature", 433 | signature: sig[:len(sig)-2], 434 | }, 435 | { 436 | name: "tampered signature", 437 | signature: tamperedSig, 438 | }, 439 | { 440 | name: "too many signature bytes", 441 | signature: append(sig, 0), 442 | }, 443 | } 444 | for _, tt := range tests { 445 | t.Run(tt.name, func(t *testing.T) { 446 | if err := verifier.Verify(content, tt.signature); err != ErrVerification { 447 | t.Errorf("ecdsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification) 448 | } 449 | }) 450 | } 451 | } 452 | 453 | func Test_ecdsaVerifier_Verify_HashFailure(t *testing.T) { 454 | // generate key 455 | alg := AlgorithmES256 456 | key := generateTestECDSAKey(t) 457 | 458 | // generate a valid signature 459 | content, sig := signTestData(t, alg, key) 460 | 461 | // set up verifier 462 | verifier, err := NewVerifier(alg, key.Public()) 463 | if err != nil { 464 | t.Fatalf("NewVerifier() error = %v", err) 465 | } 466 | 467 | // verify with bad hash implementation 468 | crypto.RegisterHash(crypto.SHA256, badHashNew) 469 | defer crypto.RegisterHash(crypto.SHA256, sha256.New) 470 | if err := verifier.Verify(content, sig); err == nil { 471 | t.Fatalf("ecdsaVerifier.Verify() error = nil, wantErr true") 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /ed25519.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ed25519" 6 | "io" 7 | ) 8 | 9 | // ed25519Signer is a Pure EdDSA based signer with a generic crypto.Signer. 10 | type ed25519Signer struct { 11 | key crypto.Signer 12 | } 13 | 14 | // Algorithm returns the signing algorithm associated with the private key. 15 | func (es *ed25519Signer) Algorithm() Algorithm { 16 | return AlgorithmEdDSA 17 | } 18 | 19 | // Sign signs message content with the private key, possibly using entropy from 20 | // rand. 21 | // The resulting signature should follow RFC 8152 section 8.2. 22 | // 23 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.2 24 | func (es *ed25519Signer) Sign(rand io.Reader, content []byte) ([]byte, error) { 25 | // crypto.Hash(0) must be passed as an option. 26 | // Reference: https://pkg.go.dev/crypto/ed25519#PrivateKey.Sign 27 | return es.key.Sign(rand, content, crypto.Hash(0)) 28 | } 29 | 30 | // ed25519Verifier is a Pure EdDSA based verifier with golang built-in keys. 31 | type ed25519Verifier struct { 32 | key ed25519.PublicKey 33 | } 34 | 35 | // Algorithm returns the signing algorithm associated with the public key. 36 | func (ev *ed25519Verifier) Algorithm() Algorithm { 37 | return AlgorithmEdDSA 38 | } 39 | 40 | // Verify verifies message content with the public key, returning nil for 41 | // success. 42 | // Otherwise, it returns [ErrVerification]. 43 | // 44 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.2 45 | func (ev *ed25519Verifier) Verify(content []byte, signature []byte) error { 46 | if verified := ed25519.Verify(ev.key, content, signature); !verified { 47 | return ErrVerification 48 | } 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /ed25519_test.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "crypto/rand" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func generateTestEd25519Key(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) { 11 | vk, sk, err := ed25519.GenerateKey(rand.Reader) 12 | if err != nil { 13 | t.Fatalf("ed25519.GenerateKey() error = %v", err) 14 | } 15 | return vk, sk 16 | } 17 | 18 | func Test_ed25519Signer(t *testing.T) { 19 | // generate key 20 | alg := AlgorithmEdDSA 21 | _, key := generateTestEd25519Key(t) 22 | 23 | // set up signer 24 | signer, err := NewSigner(alg, key) 25 | if err != nil { 26 | t.Fatalf("NewSigner() error = %v", err) 27 | } 28 | if _, ok := signer.(*ed25519Signer); !ok { 29 | t.Fatalf("NewSigner() type = %v, want *ed25519Signer", reflect.TypeOf(signer)) 30 | } 31 | if got := signer.Algorithm(); got != alg { 32 | t.Fatalf("Algorithm() = %v, want %v", got, alg) 33 | } 34 | 35 | // sign / verify round trip 36 | // see also conformance_test.go for strict tests. 37 | content := []byte("hello world") 38 | sig, err := signer.Sign(rand.Reader, content) 39 | if err != nil { 40 | t.Fatalf("Sign() error = %v", err) 41 | } 42 | 43 | verifier, err := NewVerifier(alg, key.Public()) 44 | if err != nil { 45 | t.Fatalf("NewVerifier() error = %v", err) 46 | } 47 | if err := verifier.Verify(content, sig); err != nil { 48 | t.Fatalf("Verifier.Verify() error = %v", err) 49 | } 50 | 51 | _, ok := signer.(DigestSigner) 52 | if ok { 53 | t.Fatalf("signer shouldn't be a DigestSigner") 54 | } 55 | _, ok = verifier.(DigestVerifier) 56 | if ok { 57 | t.Fatalf("verifier shouldn't be a DigestVerifier") 58 | } 59 | } 60 | 61 | func Test_ed25519Verifier_Verify_Success(t *testing.T) { 62 | // generate key 63 | alg := AlgorithmEdDSA 64 | _, key := generateTestEd25519Key(t) 65 | 66 | // generate a valid signature 67 | content, sig := signTestData(t, alg, key) 68 | 69 | // set up verifier 70 | verifier, err := NewVerifier(alg, key.Public()) 71 | if err != nil { 72 | t.Fatalf("NewVerifier() error = %v", err) 73 | } 74 | if _, ok := verifier.(*ed25519Verifier); !ok { 75 | t.Fatalf("NewVerifier() type = %v, want *ed25519Verifier", reflect.TypeOf(verifier)) 76 | } 77 | if got := verifier.Algorithm(); got != alg { 78 | t.Fatalf("Algorithm() = %v, want %v", got, alg) 79 | } 80 | 81 | // verify round trip 82 | if err := verifier.Verify(content, sig); err != nil { 83 | t.Fatalf("ed25519Verifier.Verify() error = %v", err) 84 | } 85 | } 86 | 87 | func Test_ed25519Verifier_Verify_KeyMismatch(t *testing.T) { 88 | // generate key 89 | alg := AlgorithmEdDSA 90 | _, key := generateTestEd25519Key(t) 91 | 92 | // generate a valid signature 93 | content, sig := signTestData(t, alg, key) 94 | 95 | // set up verifier with a different key / new key 96 | vk, _ := generateTestEd25519Key(t) 97 | verifier := &ed25519Verifier{ 98 | key: vk, 99 | } 100 | 101 | // verification should fail on key mismatch 102 | if err := verifier.Verify(content, sig); err != ErrVerification { 103 | t.Fatalf("ed25519Verifier.Verify() error = %v, wantErr %v", err, ErrVerification) 104 | } 105 | } 106 | 107 | func Test_ed25519Verifier_Verify_InvalidSignature(t *testing.T) { 108 | // generate key 109 | alg := AlgorithmEdDSA 110 | vk, sk := generateTestEd25519Key(t) 111 | 112 | // generate a valid signature with a tampered one 113 | content, sig := signTestData(t, alg, sk) 114 | tamperedSig := make([]byte, len(sig)) 115 | copy(tamperedSig, sig) 116 | tamperedSig[0]++ 117 | 118 | // set up verifier with a different algorithm 119 | verifier := &ed25519Verifier{ 120 | key: vk, 121 | } 122 | 123 | // verification should fail on invalid signature 124 | tests := []struct { 125 | name string 126 | signature []byte 127 | }{ 128 | { 129 | name: "nil signature", 130 | signature: nil, 131 | }, 132 | { 133 | name: "empty signature", 134 | signature: []byte{}, 135 | }, 136 | { 137 | name: "incomplete signature", 138 | signature: sig[:len(sig)-2], 139 | }, 140 | { 141 | name: "tampered signature", 142 | signature: tamperedSig, 143 | }, 144 | { 145 | name: "too many signature bytes", 146 | signature: append(sig, 0), 147 | }, 148 | } 149 | for _, tt := range tests { 150 | t.Run(tt.name, func(t *testing.T) { 151 | if err := verifier.Verify(content, tt.signature); err != ErrVerification { 152 | t.Errorf("ed25519Verifier.Verify() error = %v, wantErr %v", err, ErrVerification) 153 | } 154 | }) 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import "errors" 4 | 5 | // Common errors 6 | var ( 7 | ErrAlgorithmMismatch = errors.New("algorithm mismatch") 8 | ErrAlgorithmNotFound = errors.New("algorithm not found") 9 | ErrAlgorithmNotSupported = errors.New("algorithm not supported") 10 | ErrEmptySignature = errors.New("empty signature") 11 | ErrInvalidAlgorithm = errors.New("invalid algorithm") 12 | ErrMissingPayload = errors.New("missing payload") 13 | ErrNoSignatures = errors.New("no signatures attached") 14 | ErrUnavailableHashFunc = errors.New("hash function is not available") 15 | ErrVerification = errors.New("verification error") 16 | ErrInvalidKey = errors.New("invalid key") 17 | ErrInvalidPubKey = errors.New("invalid public key") 18 | ErrInvalidPrivKey = errors.New("invalid private key") 19 | ErrNotPrivKey = errors.New("not a private key") 20 | ErrOpNotSupported = errors.New("key_op not supported by key") 21 | ErrEC2NoPub = errors.New("cannot create PrivateKey from EC2 key: missing x or y") 22 | ErrOKPNoPub = errors.New("cannot create PrivateKey from OKP key: missing x") 23 | ) 24 | -------------------------------------------------------------------------------- /fuzz_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.18 2 | // +build go1.18 3 | 4 | package cose_test 5 | 6 | import ( 7 | "bytes" 8 | "crypto" 9 | "crypto/ecdsa" 10 | "crypto/ed25519" 11 | "crypto/elliptic" 12 | "crypto/rand" 13 | "crypto/rsa" 14 | "encoding/json" 15 | "os" 16 | "path/filepath" 17 | "strings" 18 | "testing" 19 | 20 | "github.com/fxamacker/cbor/v2" 21 | "github.com/veraison/go-cose" 22 | ) 23 | 24 | var supportedAlgorithms = [...]cose.Algorithm{ 25 | cose.AlgorithmPS256, cose.AlgorithmPS384, cose.AlgorithmPS512, 26 | cose.AlgorithmES256, cose.AlgorithmES384, cose.AlgorithmES512, 27 | cose.AlgorithmEdDSA, 28 | } 29 | 30 | func FuzzSign1Message_UnmarshalCBOR(f *testing.F) { 31 | testdata, err := os.ReadDir("testdata") 32 | if err != nil { 33 | f.Fatalf("failed to read testdata directory: %s", err) 34 | } 35 | for _, de := range testdata { 36 | if de.IsDir() || !strings.HasPrefix(de.Name(), "sign1-") || !strings.HasSuffix(de.Name(), ".json") { 37 | continue 38 | } 39 | b, err := os.ReadFile(filepath.Join("testdata", de.Name())) 40 | if err != nil { 41 | f.Fatalf("failed to read testdata: %s", err) 42 | } 43 | type testCase struct { 44 | Sign1 *Sign1 `json:"sign1::sign"` 45 | Verify1 *Verify1 `json:"sign1::verify"` 46 | } 47 | var tc testCase 48 | err = json.Unmarshal(b, &tc) 49 | if err != nil { 50 | f.Fatal(err) 51 | } 52 | if tc.Sign1 != nil { 53 | f.Add(mustHexToBytes(tc.Sign1.Output.CBORHex)) 54 | } else if tc.Verify1 != nil { 55 | f.Add(mustHexToBytes(tc.Verify1.TaggedCOSESign1.CBORHex)) 56 | } 57 | } 58 | enc, _ := cbor.CanonicalEncOptions().EncMode() 59 | dec, _ := cbor.DecOptions{IntDec: cbor.IntDecConvertSigned}.DecMode() 60 | isCanonical := func(b []byte) bool { 61 | var tmp any 62 | err := dec.Unmarshal(b, &tmp) 63 | if err != nil { 64 | return false 65 | } 66 | b1, err := enc.Marshal(tmp) 67 | if err != nil { 68 | return false 69 | } 70 | return bytes.Equal(b, b1) 71 | } 72 | f.Fuzz(func(t *testing.T, b []byte) { 73 | var msg cose.Sign1Message 74 | if err := msg.UnmarshalCBOR(b); err != nil { 75 | return 76 | } 77 | got, err := msg.MarshalCBOR() 78 | if err != nil { 79 | t.Fatalf("failed to marshal valid message: %s", err) 80 | } 81 | if !isCanonical(b) { 82 | return 83 | } 84 | if len(b) > len(got) { 85 | b = b[:len(got)] 86 | } 87 | if !bytes.Equal(b, got) { 88 | t.Fatalf("roundtripped message has changed, got: %v, want: %v", got, b) 89 | } 90 | }) 91 | } 92 | 93 | func FuzzSign1(f *testing.F) { 94 | testdata, err := os.ReadDir("testdata") 95 | if err != nil { 96 | f.Fatalf("failed to read testdata directory: %s", err) 97 | } 98 | for _, de := range testdata { 99 | if de.IsDir() || !strings.HasPrefix(de.Name(), "sign1-sign") || !strings.HasSuffix(de.Name(), ".json") { 100 | continue 101 | } 102 | b, err := os.ReadFile(filepath.Join("testdata", de.Name())) 103 | if err != nil { 104 | f.Fatalf("failed to read testdata: %s", err) 105 | } 106 | type testCase struct { 107 | Sign1 *Sign1 `json:"sign1::sign"` 108 | } 109 | var tc testCase 110 | err = json.Unmarshal(b, &tc) 111 | if err != nil { 112 | f.Fatal(err) 113 | } 114 | if tc.Sign1 != nil { 115 | hdr, _ := encMode.Marshal(mustHexToBytes(tc.Sign1.ProtectedHeaders.CBORHex)) 116 | f.Add(hdr, mustHexToBytes(tc.Sign1.Payload), mustHexToBytes(tc.Sign1.External)) 117 | } 118 | } 119 | // Generating new keys consumes a lot of memory, 120 | // to the point that the host can decide to kill the fuzzing execution 121 | // when the memory is low. 122 | // We can avoid this by always reusing the same signer and verifier for a given algorithm. 123 | signverif := make(map[cose.Algorithm]signVerifier, len(supportedAlgorithms)) 124 | for _, alg := range supportedAlgorithms { 125 | signverif[alg], err = newSignerWithEphemeralKey(alg) 126 | if err != nil { 127 | f.Fatal(err) 128 | } 129 | } 130 | 131 | f.Fuzz(func(t *testing.T, hdr_data, payload, external []byte) { 132 | hdr := make(cose.ProtectedHeader) 133 | err := hdr.UnmarshalCBOR(hdr_data) 134 | if err != nil { 135 | return 136 | } 137 | alg, err := hdr.Algorithm() 138 | if err != nil { 139 | return 140 | } 141 | sv, ok := signverif[alg] 142 | if !ok { 143 | return 144 | } 145 | msg := cose.Sign1Message{ 146 | Headers: cose.Headers{Protected: hdr}, 147 | Payload: payload, 148 | } 149 | err = msg.Sign(rand.Reader, external, sv.signer) 150 | if err != nil { 151 | t.Fatal(err) 152 | } 153 | err = msg.Verify(external, sv.verifier) 154 | if err != nil { 155 | t.Fatal(err) 156 | } 157 | err = msg.Verify(append(external, []byte{0}...), sv.verifier) 158 | if err == nil { 159 | t.Fatal("verification error expected") 160 | } 161 | }) 162 | } 163 | 164 | type signVerifier struct { 165 | signer cose.Signer 166 | verifier cose.Verifier 167 | } 168 | 169 | func newSignerWithEphemeralKey(alg cose.Algorithm) (sv signVerifier, err error) { 170 | var key crypto.Signer 171 | switch alg { 172 | case cose.AlgorithmPS256: 173 | key, err = rsa.GenerateKey(rand.Reader, 2048) 174 | case cose.AlgorithmPS384: 175 | key, err = rsa.GenerateKey(rand.Reader, 3072) 176 | case cose.AlgorithmPS512: 177 | key, err = rsa.GenerateKey(rand.Reader, 4096) 178 | case cose.AlgorithmES256: 179 | key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 180 | case cose.AlgorithmES384: 181 | key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) 182 | case cose.AlgorithmES512: 183 | key, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) 184 | case cose.AlgorithmEdDSA: 185 | _, key, err = ed25519.GenerateKey(rand.Reader) 186 | default: 187 | err = cose.ErrAlgorithmNotSupported 188 | } 189 | if err != nil { 190 | return 191 | } 192 | sv.signer, err = cose.NewSigner(alg, key) 193 | if err != nil { 194 | return 195 | } 196 | sv.verifier, err = cose.NewVerifier(alg, key.Public()) 197 | return 198 | } 199 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/veraison/go-cose 2 | 3 | go 1.21 4 | 5 | require github.com/fxamacker/cbor/v2 v2.5.0 6 | 7 | require github.com/x448/float16 v0.8.4 // indirect 8 | 9 | retract ( 10 | v1.2.1 // contains retractions only 11 | v1.2.0 // published in error 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= 2 | github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= 3 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 4 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 5 | -------------------------------------------------------------------------------- /hash_envelope.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "maps" 8 | ) 9 | 10 | // HashEnvelopePayload indicates the payload of a Hash_Envelope object. 11 | // It is used by the [SignHashEnvelope] function. 12 | // 13 | // # Experimental 14 | // 15 | // Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or 16 | // removed in a later release. 17 | type HashEnvelopePayload struct { 18 | // HashAlgorithm is the hash algorithm used to produce the hash value. 19 | HashAlgorithm Algorithm 20 | 21 | // HashValue is the hash value of the payload. 22 | HashValue []byte 23 | 24 | // PreimageContentType is the content type of the data that has been hashed. 25 | // The value is either an unsigned integer (RFC 7252 Section 12.3) or a 26 | // string (RFC 9110 Section 8.3). 27 | // This field is optional. 28 | // 29 | // References: 30 | // - https://www.iana.org/assignments/core-parameters/core-parameters.xhtml 31 | // - https://www.iana.org/assignments/media-types/media-types.xhtml 32 | PreimageContentType any // uint / string 33 | 34 | // Location is the location of the hash value in the payload. 35 | // This field is optional. 36 | Location string 37 | } 38 | 39 | // SignHashEnvelope signs a [Sign1Message] using the provided [Signer] and 40 | // produces a Hash_Envelope object. 41 | // 42 | // Hash_Envelope_Protected_Header = { 43 | // ? &(alg: 1) => int, 44 | // &(payload_hash_alg: 258) => int 45 | // &(payload_preimage_content_type: 259) => uint / tstr 46 | // ? &(payload_location: 260) => tstr 47 | // * int / tstr => any 48 | // } 49 | // 50 | // Hash_Envelope_Unprotected_Header = { 51 | // * int / tstr => any 52 | // } 53 | // 54 | // Hash_Envelope_as_COSE_Sign1 = [ 55 | // protected : bstr .cbor Hash_Envelope_Protected_Header, 56 | // unprotected : Hash_Envelope_Unprotected_Header, 57 | // payload: bstr / nil, 58 | // signature : bstr 59 | // ] 60 | // 61 | // Hash_Envelope = #6.18(Hash_Envelope_as_COSE_Sign1) 62 | // 63 | // Reference: https://www.ietf.org/archive/id/draft-ietf-cose-hash-envelope-05.html 64 | // 65 | // # Experimental 66 | // 67 | // Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or 68 | // removed in a later release. 69 | func SignHashEnvelope(rand io.Reader, signer Signer, headers Headers, payload HashEnvelopePayload) ([]byte, error) { 70 | if err := validateHash(payload.HashAlgorithm, payload.HashValue); err != nil { 71 | return nil, err 72 | } 73 | 74 | headers.Protected = setHashEnvelopeProtectedHeader(headers.Protected, &payload) 75 | headers.RawProtected = nil 76 | if err := validateHashEnvelopeHeaders(&headers); err != nil { 77 | return nil, err 78 | } 79 | 80 | return Sign1(rand, signer, headers, payload.HashValue, nil) 81 | } 82 | 83 | // VerifyHashEnvelope verifies a Hash_Envelope object using the provided 84 | // [Verifier]. 85 | // It returns the decoded [Sign1Message] if the verification is successful. 86 | // 87 | // # Experimental 88 | // 89 | // Notice: The COSE Hash Envelope API is EXPERIMENTAL and may be changed or 90 | // removed in a later release. 91 | func VerifyHashEnvelope(verifier Verifier, envelope []byte) (*Sign1Message, error) { 92 | // parse and validate the Hash_Envelope object 93 | var message Sign1Message 94 | if err := message.UnmarshalCBOR(envelope); err != nil { 95 | return nil, err 96 | } 97 | if err := validateHashEnvelopeHeaders(&message.Headers); err != nil { 98 | return nil, err 99 | } 100 | 101 | // verify the Hash_Envelope object 102 | if err := message.Verify(nil, verifier); err != nil { 103 | return nil, err 104 | } 105 | 106 | // cast to type Algorithm 107 | hashAlgorithm, err := message.Headers.Protected.PayloadHashAlgorithm() 108 | if err != nil { 109 | return nil, err 110 | } 111 | message.Headers.Protected[HeaderLabelPayloadHashAlgorithm] = hashAlgorithm 112 | 113 | // validate the hash value 114 | if err := validateHash(hashAlgorithm, message.Payload); err != nil { 115 | return nil, err 116 | } 117 | 118 | return &message, nil 119 | } 120 | 121 | // validateHash checks the validity of the known hash. 122 | func validateHash(alg Algorithm, value []byte) error { 123 | hash := alg.hashFunc() 124 | if hash == 0 { 125 | return nil // no check on unsupported hash algorithms 126 | } 127 | if size := hash.Size(); size != len(value) { 128 | return fmt.Errorf("%v: size mismatch: expected %d, got %d", alg, size, len(value)) 129 | } 130 | return nil 131 | } 132 | 133 | // setHashEnvelopeProtectedHeader sets the protected header for a Hash_Envelope 134 | // object. 135 | func setHashEnvelopeProtectedHeader(base ProtectedHeader, payload *HashEnvelopePayload) ProtectedHeader { 136 | header := maps.Clone(base) 137 | if header == nil { 138 | header = make(ProtectedHeader) 139 | } 140 | header[HeaderLabelPayloadHashAlgorithm] = payload.HashAlgorithm 141 | if payload.PreimageContentType != nil { 142 | header[HeaderLabelPayloadPreimageContentType] = payload.PreimageContentType 143 | } 144 | if payload.Location != "" { 145 | header[HeaderLabelPayloadLocation] = payload.Location 146 | } 147 | return header 148 | } 149 | 150 | // validateHashEnvelopeHeaders validates the headers of a Hash_Envelope object. 151 | // See https://www.ietf.org/archive/id/draft-ietf-cose-hash-envelope-05.html 152 | // section 4 for more details. 153 | func validateHashEnvelopeHeaders(headers *Headers) error { 154 | var foundPayloadHashAlgorithm bool 155 | for label, value := range headers.Protected { 156 | // Validate that all header labels are integers or strings. 157 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4 158 | label, ok := normalizeLabel(label) 159 | if !ok { 160 | return errors.New("header label: require int / tstr type") 161 | } 162 | 163 | switch label { 164 | case HeaderLabelContentType: 165 | return errors.New("protected header parameter: content type: not allowed") 166 | case HeaderLabelPayloadHashAlgorithm: 167 | _, isAlg := value.(Algorithm) 168 | if !isAlg && !canInt(value) { 169 | return errors.New("protected header parameter: payload hash alg: require int type") 170 | } 171 | foundPayloadHashAlgorithm = true 172 | case HeaderLabelPayloadPreimageContentType: 173 | if !canUint(value) && !canTstr(value) { 174 | return errors.New("protected header parameter: payload preimage content type: require uint / tstr type") 175 | } 176 | case HeaderLabelPayloadLocation: 177 | if !canTstr(value) { 178 | return errors.New("protected header parameter: payload location: require tstr type") 179 | } 180 | } 181 | } 182 | if !foundPayloadHashAlgorithm { 183 | return errors.New("protected header parameter: payload hash alg: required") 184 | } 185 | 186 | for label := range headers.Unprotected { 187 | // Validate that all header labels are integers or strings. 188 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-1.4 189 | label, ok := normalizeLabel(label) 190 | if !ok { 191 | return errors.New("header label: require int / tstr type") 192 | } 193 | 194 | switch label { 195 | case HeaderLabelContentType: 196 | return errors.New("unprotected header parameter: content type: not allowed") 197 | case HeaderLabelPayloadHashAlgorithm: 198 | return errors.New("unprotected header parameter: payload hash alg: not allowed") 199 | case HeaderLabelPayloadPreimageContentType: 200 | return errors.New("unprotected header parameter: payload preimage content type: not allowed") 201 | case HeaderLabelPayloadLocation: 202 | return errors.New("unprotected header parameter: payload location: not allowed") 203 | } 204 | } 205 | 206 | return nil 207 | } 208 | -------------------------------------------------------------------------------- /hash_envelope_test.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "maps" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestSignHashEnvelope(t *testing.T) { 13 | // generate key and set up signer / verifier 14 | alg := AlgorithmES256 15 | key := generateTestECDSAKey(t) 16 | signer, err := NewSigner(alg, key) 17 | if err != nil { 18 | t.Fatalf("NewSigner() error = %v", err) 19 | } 20 | verifier, err := NewVerifier(alg, key.Public()) 21 | if err != nil { 22 | t.Fatalf("NewVerifier() error = %v", err) 23 | } 24 | payload := []byte("hello world") 25 | payloadAlg := AlgorithmSHA256 26 | payloadSHA256 := sha256.Sum256(payload) 27 | payloadHash := payloadSHA256[:] 28 | 29 | tests := []struct { 30 | name string 31 | headers Headers 32 | payload HashEnvelopePayload 33 | wantHeaders Headers 34 | wantErr string 35 | }{ 36 | { 37 | name: "minimal signing", 38 | payload: HashEnvelopePayload{ 39 | HashAlgorithm: payloadAlg, 40 | HashValue: payloadHash, 41 | }, 42 | wantHeaders: Headers{ 43 | Protected: ProtectedHeader{ 44 | HeaderLabelAlgorithm: alg, 45 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 46 | }, 47 | }, 48 | }, 49 | { 50 | name: "with preimage content type (int)", 51 | payload: HashEnvelopePayload{ 52 | HashAlgorithm: payloadAlg, 53 | HashValue: payloadHash, 54 | PreimageContentType: 0, 55 | }, 56 | wantHeaders: Headers{ 57 | Protected: ProtectedHeader{ 58 | HeaderLabelAlgorithm: alg, 59 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 60 | HeaderLabelPayloadPreimageContentType: int64(0), 61 | }, 62 | }, 63 | }, 64 | { 65 | name: "with preimage content type (tstr)", 66 | payload: HashEnvelopePayload{ 67 | HashAlgorithm: payloadAlg, 68 | HashValue: payloadHash, 69 | PreimageContentType: "text/plain", 70 | }, 71 | wantHeaders: Headers{ 72 | Protected: ProtectedHeader{ 73 | HeaderLabelAlgorithm: alg, 74 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 75 | HeaderLabelPayloadPreimageContentType: "text/plain", 76 | }, 77 | }, 78 | }, 79 | { 80 | name: "with payload location", 81 | payload: HashEnvelopePayload{ 82 | HashAlgorithm: payloadAlg, 83 | HashValue: payloadHash, 84 | Location: "urn:example:location", 85 | }, 86 | wantHeaders: Headers{ 87 | Protected: ProtectedHeader{ 88 | HeaderLabelAlgorithm: alg, 89 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 90 | HeaderLabelPayloadLocation: "urn:example:location", 91 | }, 92 | }, 93 | }, 94 | { 95 | name: "full signing with base headers", 96 | headers: Headers{ 97 | Protected: ProtectedHeader{ 98 | HeaderLabelAlgorithm: alg, 99 | }, 100 | Unprotected: UnprotectedHeader{ 101 | HeaderLabelKeyID: []byte("42"), 102 | }, 103 | }, 104 | payload: HashEnvelopePayload{ 105 | HashAlgorithm: payloadAlg, 106 | HashValue: payloadHash, 107 | PreimageContentType: "text/plain", 108 | Location: "urn:example:location", 109 | }, 110 | wantHeaders: Headers{ 111 | Protected: ProtectedHeader{ 112 | HeaderLabelAlgorithm: alg, 113 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 114 | HeaderLabelPayloadPreimageContentType: "text/plain", 115 | HeaderLabelPayloadLocation: "urn:example:location", 116 | }, 117 | Unprotected: UnprotectedHeader{ 118 | HeaderLabelKeyID: []byte("42"), 119 | }, 120 | }, 121 | }, 122 | { 123 | name: "unsupported hash algorithm", 124 | payload: HashEnvelopePayload{ 125 | HashAlgorithm: Algorithm(-15), // SHA-256/64 126 | HashValue: payloadHash, 127 | }, 128 | wantHeaders: Headers{ 129 | Protected: ProtectedHeader{ 130 | HeaderLabelAlgorithm: alg, 131 | HeaderLabelPayloadHashAlgorithm: Algorithm(-15), // SHA-256/64 132 | }, 133 | }, 134 | }, 135 | { 136 | name: "bad hash value", 137 | payload: HashEnvelopePayload{ 138 | HashAlgorithm: payloadAlg, 139 | }, 140 | wantErr: "SHA-256: size mismatch: expected 32, got 0", 141 | }, 142 | { 143 | name: "invalid preimage content type", 144 | payload: HashEnvelopePayload{ 145 | HashAlgorithm: payloadAlg, 146 | HashValue: payloadHash, 147 | PreimageContentType: -1, 148 | }, 149 | wantErr: "protected header parameter: payload preimage content type: require uint / tstr type", 150 | }, 151 | } 152 | for _, tt := range tests { 153 | t.Run(tt.name, func(t *testing.T) { 154 | got, err := SignHashEnvelope(rand.Reader, signer, tt.headers, tt.payload) 155 | if err != nil { 156 | if tt.wantErr == "" || err.Error() != tt.wantErr { 157 | t.Fatalf("SignHashEnvelope() error = %v, wantErr %s", err, tt.wantErr) 158 | } 159 | return 160 | } 161 | if tt.wantErr != "" { 162 | t.Fatalf("SignHashEnvelope() error = %v, wantErr %s", err, tt.wantErr) 163 | } 164 | msg, err := VerifyHashEnvelope(verifier, got) 165 | if err != nil { 166 | t.Fatalf("VerifyHashEnvelope() error = %v", err) 167 | } 168 | if !maps.EqualFunc(msg.Headers.Protected, tt.wantHeaders.Protected, reflect.DeepEqual) { 169 | t.Errorf("SignHashEnvelope() Protected Header = %v, want %v", msg.Headers.Protected, tt.wantHeaders.Protected) 170 | } 171 | if !maps.EqualFunc(msg.Headers.Unprotected, tt.wantHeaders.Unprotected, reflect.DeepEqual) { 172 | t.Errorf("SignHashEnvelope() Unprotected Header = %v, want %v", msg.Headers.Unprotected, tt.wantHeaders.Unprotected) 173 | } 174 | if !bytes.Equal(msg.Payload, tt.payload.HashValue) { 175 | t.Errorf("SignHashEnvelope() Payload = %v, want %v", msg.Payload, tt.payload.HashValue) 176 | } 177 | }) 178 | } 179 | } 180 | 181 | func TestVerifyHashEnvelope(t *testing.T) { 182 | // generate key and set up signer / verifier 183 | alg := AlgorithmES256 184 | key := generateTestECDSAKey(t) 185 | signer, err := NewSigner(alg, key) 186 | if err != nil { 187 | t.Fatalf("NewSigner() error = %v", err) 188 | } 189 | verifier, err := NewVerifier(alg, key.Public()) 190 | if err != nil { 191 | t.Fatalf("NewVerifier() error = %v", err) 192 | } 193 | payload := []byte("hello world") 194 | payloadAlg := AlgorithmSHA256 195 | payloadSHA256 := sha256.Sum256(payload) 196 | payloadHash := payloadSHA256[:] 197 | 198 | tests := []struct { 199 | name string 200 | envelope []byte 201 | message *Sign1Message 202 | wantErr string 203 | }{ 204 | { 205 | name: "valid envelope", 206 | message: &Sign1Message{ 207 | Headers: Headers{ 208 | Protected: ProtectedHeader{ 209 | HeaderLabelAlgorithm: alg, 210 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 211 | }, 212 | }, 213 | Payload: payloadHash, 214 | }, 215 | }, 216 | { 217 | name: "nil envelope", 218 | wantErr: "cbor: invalid COSE_Sign1_Tagged object", 219 | }, 220 | { 221 | name: "empty envelope", 222 | envelope: []byte{}, 223 | wantErr: "cbor: invalid COSE_Sign1_Tagged object", 224 | }, 225 | { 226 | name: "not a Hash_Envelope object", 227 | message: &Sign1Message{ 228 | Headers: Headers{ 229 | Protected: ProtectedHeader{ 230 | HeaderLabelAlgorithm: alg, 231 | }, 232 | }, 233 | Payload: payloadHash, 234 | }, 235 | wantErr: "protected header parameter: payload hash alg: required", 236 | }, 237 | 238 | { 239 | name: "payload hash algorithm in the unprotected header", 240 | message: &Sign1Message{ 241 | Headers: Headers{ 242 | Protected: ProtectedHeader{ 243 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 244 | HeaderLabelAlgorithm: alg, 245 | }, 246 | Unprotected: UnprotectedHeader{ 247 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 248 | }, 249 | }, 250 | Payload: payloadHash, 251 | }, 252 | wantErr: "unprotected header parameter: payload hash alg: not allowed", 253 | }, 254 | { 255 | name: "invalid payload hash algorithm", 256 | message: &Sign1Message{ 257 | Headers: Headers{ 258 | Protected: ProtectedHeader{ 259 | HeaderLabelAlgorithm: alg, 260 | HeaderLabelPayloadHashAlgorithm: "SHA-256", 261 | }, 262 | }, 263 | Payload: payloadHash, 264 | }, 265 | wantErr: "protected header parameter: payload hash alg: require int type", 266 | }, 267 | { 268 | name: "invalid preimage content type in the protected header", 269 | message: &Sign1Message{ 270 | Headers: Headers{ 271 | Protected: ProtectedHeader{ 272 | HeaderLabelAlgorithm: alg, 273 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 274 | HeaderLabelPayloadPreimageContentType: -1, 275 | }, 276 | }, 277 | Payload: payloadHash, 278 | }, 279 | wantErr: "protected header parameter: payload preimage content type: require uint / tstr type", 280 | }, 281 | { 282 | name: "preimage content type present in the unprotected header", 283 | message: &Sign1Message{ 284 | Headers: Headers{ 285 | Protected: ProtectedHeader{ 286 | HeaderLabelAlgorithm: alg, 287 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 288 | }, 289 | Unprotected: UnprotectedHeader{ 290 | HeaderLabelPayloadPreimageContentType: "text/plain", 291 | }, 292 | }, 293 | Payload: payloadHash, 294 | }, 295 | wantErr: "unprotected header parameter: payload preimage content type: not allowed", 296 | }, 297 | { 298 | name: "payload location present in the unprotected header", 299 | message: &Sign1Message{ 300 | Headers: Headers{ 301 | Protected: ProtectedHeader{ 302 | HeaderLabelAlgorithm: alg, 303 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 304 | }, 305 | Unprotected: UnprotectedHeader{ 306 | HeaderLabelPayloadLocation: "urn:example:location", 307 | }, 308 | }, 309 | Payload: payloadHash, 310 | }, 311 | wantErr: "unprotected header parameter: payload location: not allowed", 312 | }, 313 | { 314 | name: "invalid payload location in the protected header", 315 | message: &Sign1Message{ 316 | Headers: Headers{ 317 | Protected: ProtectedHeader{ 318 | HeaderLabelAlgorithm: alg, 319 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 320 | HeaderLabelPayloadLocation: 0, 321 | }, 322 | }, 323 | Payload: payloadHash, 324 | }, 325 | wantErr: "protected header parameter: payload location: require tstr type", 326 | }, 327 | { 328 | name: "content type present in the protected header", 329 | message: &Sign1Message{ 330 | Headers: Headers{ 331 | Protected: ProtectedHeader{ 332 | HeaderLabelAlgorithm: alg, 333 | HeaderLabelContentType: "text/plain", 334 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 335 | }, 336 | }, 337 | Payload: payloadHash, 338 | }, 339 | wantErr: "protected header parameter: content type: not allowed", 340 | }, 341 | { 342 | name: "content type present in the unprotected header", 343 | message: &Sign1Message{ 344 | Headers: Headers{ 345 | Protected: ProtectedHeader{ 346 | HeaderLabelAlgorithm: alg, 347 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 348 | }, 349 | Unprotected: UnprotectedHeader{ 350 | HeaderLabelContentType: "text/plain", 351 | }, 352 | }, 353 | Payload: payloadHash, 354 | }, 355 | wantErr: "unprotected header parameter: content type: not allowed", 356 | }, 357 | { 358 | name: "bad signature", 359 | message: &Sign1Message{ 360 | Headers: Headers{ 361 | Protected: ProtectedHeader{ 362 | HeaderLabelAlgorithm: alg, 363 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 364 | }, 365 | Unprotected: UnprotectedHeader{ 366 | HeaderLabelKeyID: []byte("42"), 367 | }, 368 | }, 369 | Payload: payloadHash, 370 | Signature: []byte("bad signature"), 371 | }, 372 | wantErr: "verification error", 373 | }, 374 | { 375 | name: "bad hash value", 376 | message: &Sign1Message{ 377 | Headers: Headers{ 378 | Protected: ProtectedHeader{ 379 | HeaderLabelAlgorithm: alg, 380 | HeaderLabelPayloadHashAlgorithm: payloadAlg, 381 | }, 382 | Unprotected: UnprotectedHeader{ 383 | HeaderLabelKeyID: []byte("42"), 384 | }, 385 | }, 386 | Payload: []byte("bad hash value"), 387 | }, 388 | wantErr: "SHA-256: size mismatch: expected 32, got 14", 389 | }, 390 | } 391 | for _, tt := range tests { 392 | t.Run(tt.name, func(t *testing.T) { 393 | envelope := tt.envelope 394 | if tt.message != nil { 395 | message := tt.message 396 | if message.Signature == nil { 397 | if err := message.Sign(rand.Reader, nil, signer); err != nil { 398 | t.Fatalf("Sign1Message.Sign() error = %v", err) 399 | } 400 | } 401 | var err error 402 | envelope, err = message.MarshalCBOR() 403 | if err != nil { 404 | t.Fatalf("Sign1Message.MarshalCBOR() error = %v", err) 405 | } 406 | } 407 | msg, err := VerifyHashEnvelope(verifier, envelope) 408 | if err != nil { 409 | if tt.wantErr == "" || err.Error() != tt.wantErr { 410 | t.Fatalf("VerifyHashEnvelope() error = %v, wantErr %s", err, tt.wantErr) 411 | } 412 | return 413 | } 414 | if tt.wantErr != "" { 415 | t.Fatalf("VerifyHashEnvelope() error = %v, wantErr %s", err, tt.wantErr) 416 | } 417 | if msg == nil { 418 | t.Fatalf("VerifyHashEnvelope() message = nil, want not nil") 419 | } 420 | }) 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /release-checklist.md: -------------------------------------------------------------------------------- 1 | # Release Checklist 2 | 3 | ## Overview 4 | 5 | This document describes the checklist to publish a release via GitHub workflow. 6 | 7 | See [Release Management](./release-management.md) for the overall process of versioning and support. 8 | 9 | > [!NOTE] 10 | The maintainers may periodically update this checklist based on feedback. 11 | 12 | ## Release Steps 13 | 14 | Releasing is a two-step process for voting on a specific release, and cutting the release. 15 | 16 | ### Vote On a Specific Commit 17 | 18 | 1. Determine a [SemVer2](https://semver.org/)-valid version prefixed with the letter `v` for release. For example, `v1.0.0-alpha.1`, `v1.0.0`. 19 | 1. Make sure the dependencies in `go.mod` file are expected by the release. 20 | 1. After updating `go.mod` file, run `go mod tidy` to ensure the `go.sum` file is also updated with any potential changes. 21 | 1. Determine the commit to be tagged and released. 22 | 1. Create an issue for voting with title similar to **"vote**: `tag v1.0.0-alpha.1` with the proposed commit. (see [Issue #204](https://github.com/veraison/go-cose/issues/204) 23 | 1. Request a 👍or 👎from each maintainer, requesting details if they voted against, and opening a corresponding issue. 24 | 1. Wait a max of 2 weeks for the vote pass, or sooner if a majority of maintainers approve. 25 | 26 | ### Cut a Release 27 | 28 | 1. Select [[Draft a new release](https://github.com/veraison/go-cose/releases/new)] 29 | 1. **[Choose a tag]**: Create a new tag based on Alpha, RC or a final release: (`v1.0.0-alpha.1`, `v1.0.0-rc.1`, `v1.0.0`). 30 | 1. **[Target]**: Select the voted commit in "Recent Commits". 31 | 1. **[Previous Tag]**: Select the previous corresponding release. 32 | 1. For alpha and rc releases, select the previous release within that release band (rc.1, select the latest alpha release). 33 | 1. For final releases, select the previous full release: ([v1.3.0](https://github.com/veraison/go-cose/releases/tag/v1.3.0), selects [v1.1.0](https://github.com/veraison/go-cose/releases/tag/v1.1.0)) 34 | 1. **[Generate Release Notes]**: Edit for spelling and clarity. 35 | 1. **[Release title]** Set to the same value as the tag and voted issue. 36 | 1. **[Set as a pre-release]**: Set if the release is an alpha or rc. 37 | Do not set for final release. 38 | 1. **[Set as the latest release]**: To bring focus to the latest, always check this box. 39 | 1. Announce the release in the community. 40 | -------------------------------------------------------------------------------- /release-management.md: -------------------------------------------------------------------------------- 1 | # go-cose Release Management 2 | 3 | ## Overview 4 | 5 | This document describes [go-cose][go-cose] project release management, which includes release criteria, versioning, supported releases, and supported upgrades. 6 | 7 | The go-cose project maintainers strive to provide a stable go-lang implementation for interacting with [COSE][ietf-cose] constructs. 8 | Stable implies appropriate and measured changes to the library assuring consumers have the necessary functionality to interact with COSE objects. 9 | If you or your project require added functionality, or bug fixes, please open an issue or create a pull request. 10 | The project welcomes all contributions from adding functionality, implementing testing, security reviews to the release management. 11 | 12 | To cut a release, see [Release Checklist](./release-checklist.md). 13 | 14 | > [!NOTE] 15 | The maintainers may periodically update this policy based on feedback. 16 | 17 | ## Release Versioning 18 | 19 | Consumers of the go-cose module may reference `main` directly, or reference released tags. 20 | 21 | All go-cose [releases][releases] follow a go-lang flavored derivation (`v*`) of the [semver][sem-ver] format, with optional pre-release labels. 22 | 23 | Logical Progression of a release: `v1.0.0-alpha.1` --> `v1.0.0-alpha.2` --> `v1.0.0-rc.1` --> `v1.0.0` 24 | 25 | A new major or minor release will not have an automated release posted until the branch reaches alpha quality. 26 | 27 | - All versions use a preface of `v` 28 | - Given a version `vX.Y.Z`, 29 | - `X` is the [Major](#major-releases) version 30 | - `Y` is the [Minor](#minor-releases) version 31 | - `Z` is the [Patch](#patch-releases) version 32 | - _Optional_ `-alpha.n` | `-rc.n` [pre-release](#pre-release) version 33 | - Each incremental alpha or rc build will bump the suffix (`n`) number. 34 | - It's not expected to have more than 9 alphas or rcs. 35 | The suffix will be a single digit. 36 | - If > 9 builds do occur, the format will simply use two digit indicators (`v1.0.0-alpha.10`) 37 | 38 | ## Branch Management 39 | 40 | To meet the projects stability goals, go-cose does not typically operate with multiple feature branches. 41 | All active development happens in `main`, with releases made on a specific commit. 42 | 43 | ### Major Releases 44 | 45 | As a best practice, consumers should opt-into new capabilities through major releases. 46 | The go-cose project will not add new functionality to patches or minor releases as this could create a new surface area that may be exploited. 47 | Consumers should make explicit opt-in decisions to upgrade, or possibly downgrade if necessary due to unexpected breaking changes. 48 | 49 | The go-cose project will issue major releases when: 50 | 51 | - Functionality has changed 52 | - Breaking changes are required 53 | 54 | Each major release will go through one or more `-alpha.n` and `-rc.n` pre-release phases. 55 | 56 | ### Minor Releases 57 | 58 | The go-cose project will issue minor releases when incremental improvements, or bug fixes are added to existing functionality. 59 | Minor releases will increment the minor field within the [semver][sem-ver] format. 60 | 61 | Each minor release will go through one or more `-alpha.n` and `-rc.n` pre-release phases. 62 | 63 | ### Patch Releases 64 | 65 | Patch Releases include bug and security fixes. 66 | Patches will branch from the released branch being patched. 67 | Fixes completed in main may be ported to a patch release if the maintainers believe the effort is justified by requests from the go-cose community. 68 | If a bug fix requires new incremental, non-breaking change functionality, a new minor release may be issued. 69 | 70 | Principals of a patch release: 71 | 72 | - Should be a "safe bet" to upgrade to. 73 | - No breaking changes. 74 | - No feature or surface area changes. 75 | - A "bug fix" that may be a breaking change may require a major release. 76 | - If a patched release is necessary, deviating from main, a new branch will be created, based on the commit of the patched release. 77 | - Applicable fixes, including security fixes, may be cherry-picked from main into the patched `release-X.Y.Z` branch. 78 | 79 | Each patch release will go through one or more `-alpha.n` and `-rc.n` pre-release phases. 80 | 81 | ### Pre-Release 82 | 83 | As builds of `main` become stable, and a pending release is planned, a pre-release build will be made. 84 | Pre-releases go through one or more `-alpha.n` releases, followed by one or more incremental `-rc.n` releases. 85 | 86 | - **alpha.n:** `X.Y.Z-alpha.n` 87 | - alpha release, cut from the branch where development occurs. 88 | To minimize branch management, no additional branches are maintained for each incremental release. 89 | - Considered an unstable release which should only be used for early development purposes. 90 | - Released incrementally until no additional issues and prs are made against the release. 91 | - Once no triaged issues or pull requests (prs) are scoped to the release, a release candidate (`rc`) is cut. 92 | - To minimize confusion, and the risk of an alpha being widely deployed, alpha branches and released binaries may be removed at the discretion, and a [two-thirds supermajority][super-majority] vote, of the maintainers. 93 | Maintainers will create an Issue, and vote upon it for transparency to the decision to remove a release and/or branch. 94 | - Not [supported](#supported-releases) 95 | - **rc.n:** `X.Y.Z-rc.n` 96 | - Released as needed before a final version is released 97 | - Bugfixes on new features only as reported through usage 98 | - An `rc` is not expected to revert to an alpha release. 99 | - Once no triaged issues or PRs are scoped to the release, an final version is cut. 100 | - A release candidate will typically have at least two weeks of bake time, providing the community time to provide feedback. 101 | - Release candidates are cut from the branch where the work is done. 102 | - To minimize confusion, and the risk of an rc being widely deployed, rc branches and released binaries may be removed at the discretion, and a [two-thirds supermajority][super-majority] vote, of the maintainers. 103 | Maintainers will create an Issue, and vote upon it for transparency to the decision to remove a release and/or branch. 104 | - Not [supported](#supported-releases) 105 | 106 | ## Supported Releases 107 | 108 | The go-cose maintainers expect to "support" n (current) and `n-1` major.minor releases. 109 | "Support" means we expect users to be running that version in production. 110 | For example, when `v1.3.0` comes out, `v1.1.x` will no longer be supported for patches, and the maintainers encourage users to upgrade to a supported version as soon as possible. 111 | Support will be provided best effort by the maintainers via GitHub issues and pull requests from the community. 112 | 113 | The go-cose maintainers expect users to stay up-to-date with the versions of go-cose release they use in production, but understand that it may take time to upgrade. 114 | We expect users to be running approximately the latest patch release of a given minor release and encourage users to upgrade as soon as possible. 115 | 116 | While pre-releases may be deleted at the discretion of the maintainers, all Major, Minor and Patch releases should be maintained. 117 | Only in extreme circumstances, as agreed upon by a [two-thirds supermajority][super-majority] of the maintainers, shall a release be removed. 118 | 119 | Applicable fixes, including security fixes, may be cherry-picked into the release branch, depending on severity and feasibility. 120 | Patch releases are cut from that branch as needed. 121 | 122 | ## Security Reviews 123 | 124 | The go-cose library is an sdk around underlying crypto libraries, tailored to COSE scenarios. 125 | The go-cose library does not implement cryptographic functionality, reducing the potential risk. 126 | To assure go-cose had the proper baseline, two [security reviews](./reports) were conducted prior to the [v1.0.0](https://github.com/veraison/go-cose/releases/tag/v1.0.0) release 127 | 128 | For each release, new security reviews are evaluated by the maintainers as required or optional. 129 | The go-cose project welcomes additional security reviews. 130 | See [SECURITY.md](./SECURITY.md) for more information. 131 | 132 | ## Glossary of Terms 133 | 134 | - **X.Y.Z** refers to the version (based on git tag) of go-cose that is released. 135 | This is the version of the go-cose binary. 136 | - **Breaking changes** refer to schema changes, flag changes, and behavior changes of go-cose that may require existing content to be upgraded and may also introduce changes that could break backward compatibility. 137 | - **Milestone** GitHub milestones are used by maintainers to manage each release. 138 | PRs and Issues for each release should be created as part of a corresponding milestone. 139 | - **Patch releases** refer to applicable fixes, including security fixes, may be backported to support releases, depending on severity and feasibility. 140 | 141 | ## Attribution 142 | 143 | This document builds on the ideas and implementations of release processes from the [notation](https://github.com/notaryproject/notation) project. 144 | 145 | [go-cose]: https://github.com/veraison/go-cose 146 | [ietf-cose]: https://datatracker.ietf.org/group/cose/about/ 147 | [sem-ver]: https://semver.org 148 | [releases]: https://github.com/veraison/go-cose/releases 149 | [super-majority]: https://en.wikipedia.org/wiki/Supermajority#Two-thirds_vote 150 | -------------------------------------------------------------------------------- /reports/NCC_Microsoft-go-cose-Report_2022-05-26_v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veraison/go-cose/a633822d54e270749baecce653c5ba4f07ccbb46/reports/NCC_Microsoft-go-cose-Report_2022-05-26_v1.0.pdf -------------------------------------------------------------------------------- /reports/README.md: -------------------------------------------------------------------------------- 1 | # Security Reports 2 | 3 | This folder contains all the security review reports for the go-cose library. 4 | 5 | ## List of Security Reports 6 | 7 | | Review Date | Name of Security Review | Report Location | 8 | |:------------|:--------------------------------------| -------------------------------| 9 | | May 16, 2022 | NCC Group go-cose Security Assessment | [NCC Report](./NCC_Microsoft-go-cose-Report_2022-05-26_v1.0.pdf) | 10 | | July 26, 2022 | Trail of Bits go-cose Security Assessment | [Trail of Bits Report](./Trail-of-Bits_Microsoft-go-cose-Report_2022-07-26_v1.0.pdf) | 11 | -------------------------------------------------------------------------------- /reports/Trail-of-Bits_Microsoft-go-cose-Report_2022-07-26_v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/veraison/go-cose/a633822d54e270749baecce653c5ba4f07ccbb46/reports/Trail-of-Bits_Microsoft-go-cose-Report_2022-07-26_v1.0.pdf -------------------------------------------------------------------------------- /rsa.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rsa" 6 | "io" 7 | ) 8 | 9 | // rsaSigner is a RSASSA-PSS based signer with a generic [crypto.Signer]. 10 | // 11 | // Reference: https://www.rfc-editor.org/rfc/rfc8230.html#section-2 12 | type rsaSigner struct { 13 | alg Algorithm 14 | key crypto.Signer 15 | } 16 | 17 | // Algorithm returns the signing algorithm associated with the private key. 18 | func (rs *rsaSigner) Algorithm() Algorithm { 19 | return rs.alg 20 | } 21 | 22 | // Sign signs message content with the private key, using entropy from rand. 23 | // The resulting signature should follow RFC 8152 section 8. 24 | // 25 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8 26 | func (rs *rsaSigner) Sign(rand io.Reader, content []byte) ([]byte, error) { 27 | digest, err := rs.alg.computeHash(content) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return rs.SignDigest(rand, digest) 32 | } 33 | 34 | // SignDigest signs message digest with the private key, possibly using 35 | // entropy from rand. 36 | // The resulting signature should follow RFC 8152 section 8. 37 | func (rs *rsaSigner) SignDigest(rand io.Reader, digest []byte) ([]byte, error) { 38 | return rs.key.Sign(rand, digest, &rsa.PSSOptions{ 39 | SaltLength: rsa.PSSSaltLengthEqualsHash, // defined in RFC 8230 sec 2 40 | Hash: rs.alg.hashFunc(), 41 | }) 42 | } 43 | 44 | // rsaVerifier is a RSASSA-PSS based verifier with golang built-in keys. 45 | // 46 | // Reference: https://www.rfc-editor.org/rfc/rfc8230.html#section-2 47 | type rsaVerifier struct { 48 | alg Algorithm 49 | key *rsa.PublicKey 50 | } 51 | 52 | // Algorithm returns the signing algorithm associated with the public key. 53 | func (rv *rsaVerifier) Algorithm() Algorithm { 54 | return rv.alg 55 | } 56 | 57 | // Verify verifies message content with the public key, returning nil for 58 | // success. 59 | // Otherwise, it returns [ErrVerification]. 60 | // 61 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8 62 | func (rv *rsaVerifier) Verify(content []byte, signature []byte) error { 63 | digest, err := rv.alg.computeHash(content) 64 | if err != nil { 65 | return err 66 | } 67 | return rv.VerifyDigest(digest, signature) 68 | } 69 | 70 | // VerifyDigest verifies message digest with the public key, returning nil 71 | // for success. 72 | // Otherwise, it returns [ErrVerification]. 73 | // 74 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8.1 75 | func (rv *rsaVerifier) VerifyDigest(digest []byte, signature []byte) error { 76 | if err := rsa.VerifyPSS(rv.key, rv.alg.hashFunc(), digest, signature, &rsa.PSSOptions{ 77 | SaltLength: rsa.PSSSaltLengthEqualsHash, // defined in RFC 8230 sec 2 78 | }); err != nil { 79 | return ErrVerification 80 | } 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /rsa_test.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha256" 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func generateTestRSAKey(t *testing.T) *rsa.PrivateKey { 13 | key, err := rsa.GenerateKey(rand.Reader, 2048) 14 | if err != nil { 15 | t.Fatalf("rsa.GenerateKey() error = %v", err) 16 | } 17 | return key 18 | } 19 | 20 | func Test_rsaSigner(t *testing.T) { 21 | // generate key 22 | alg := AlgorithmPS256 23 | key := generateTestRSAKey(t) 24 | 25 | // set up signer 26 | signer, err := NewSigner(alg, key) 27 | if err != nil { 28 | t.Fatalf("NewSigner() error = %v", err) 29 | } 30 | if _, ok := signer.(*rsaSigner); !ok { 31 | t.Fatalf("NewSigner() type = %v, want *rsaSigner", reflect.TypeOf(signer)) 32 | } 33 | if got := signer.Algorithm(); got != alg { 34 | t.Fatalf("Algorithm() = %v, want %v", got, alg) 35 | } 36 | 37 | // sign / verify round trip 38 | // see also conformance_test.go for strict tests. 39 | content := []byte("hello world, مرحبا بالعالم") 40 | sig, err := signer.Sign(rand.Reader, content) 41 | if err != nil { 42 | t.Fatalf("Sign() error = %v", err) 43 | } 44 | 45 | verifier, err := NewVerifier(alg, key.Public()) 46 | if err != nil { 47 | t.Fatalf("NewVerifier() error = %v", err) 48 | } 49 | if err := verifier.Verify(content, sig); err != nil { 50 | t.Fatalf("Verifier.Verify() error = %v", err) 51 | } 52 | 53 | // digested sign/verify round trip 54 | dsigner, ok := signer.(DigestSigner) 55 | if !ok { 56 | t.Fatalf("signer is not a DigestSigner") 57 | } 58 | digest := sha256.Sum256(content) 59 | dsig, err := dsigner.SignDigest(rand.Reader, digest[:]) 60 | if err != nil { 61 | t.Fatalf("SignDigest() error = %v", err) 62 | } 63 | dverifier, ok := verifier.(DigestVerifier) 64 | if !ok { 65 | t.Fatalf("verifier is not a DigestVerifier") 66 | } 67 | if err := dverifier.VerifyDigest(digest[:], dsig); err != nil { 68 | t.Fatalf("VerifyDigest() error = %v", err) 69 | } 70 | } 71 | 72 | func Test_rsaSigner_SignHashFailure(t *testing.T) { 73 | // generate key 74 | alg := AlgorithmPS256 75 | key := generateTestRSAKey(t) 76 | 77 | // set up signer 78 | signer, err := NewSigner(alg, key) 79 | if err != nil { 80 | t.Fatalf("NewSigner() error = %v", err) 81 | } 82 | 83 | // sign with bad hash implementation 84 | crypto.RegisterHash(crypto.SHA256, badHashNew) 85 | defer crypto.RegisterHash(crypto.SHA256, sha256.New) 86 | content := []byte("hello world") 87 | if _, err = signer.Sign(rand.Reader, content); err == nil { 88 | t.Fatalf("Sign() error = nil, wantErr true") 89 | } 90 | } 91 | 92 | func Test_rsaVerifier_Verify_Success(t *testing.T) { 93 | // generate key 94 | alg := AlgorithmPS256 95 | key := generateTestRSAKey(t) 96 | 97 | // generate a valid signature 98 | content, sig := signTestData(t, alg, key) 99 | 100 | // set up verifier 101 | verifier, err := NewVerifier(alg, key.Public()) 102 | if err != nil { 103 | t.Fatalf("NewVerifier() error = %v", err) 104 | } 105 | if _, ok := verifier.(*rsaVerifier); !ok { 106 | t.Fatalf("NewVerifier() type = %v, want *rsaVerifier", reflect.TypeOf(verifier)) 107 | } 108 | if got := verifier.Algorithm(); got != alg { 109 | t.Fatalf("Algorithm() = %v, want %v", got, alg) 110 | } 111 | 112 | // verify round trip 113 | if err := verifier.Verify(content, sig); err != nil { 114 | t.Fatalf("rsaVerifier.Verify() error = %v", err) 115 | } 116 | } 117 | 118 | func Test_rsaVerifier_Verify_AlgorithmMismatch(t *testing.T) { 119 | // generate key 120 | alg := AlgorithmPS256 121 | key := generateTestRSAKey(t) 122 | 123 | // generate a valid signature 124 | content, sig := signTestData(t, alg, key) 125 | 126 | // set up verifier with a different algorithm 127 | verifier := &rsaVerifier{ 128 | alg: AlgorithmPS512, 129 | key: &key.PublicKey, 130 | } 131 | 132 | // verification should fail on algorithm mismatch 133 | if err := verifier.Verify(content, sig); err != ErrVerification { 134 | t.Fatalf("rsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification) 135 | } 136 | } 137 | 138 | func Test_rsaVerifier_Verify_KeyMismatch(t *testing.T) { 139 | // generate key 140 | alg := AlgorithmPS256 141 | key := generateTestRSAKey(t) 142 | 143 | // generate a valid signature 144 | content, sig := signTestData(t, alg, key) 145 | 146 | // set up verifier with a different key / new key 147 | key = generateTestRSAKey(t) 148 | verifier := &rsaVerifier{ 149 | alg: alg, 150 | key: &key.PublicKey, 151 | } 152 | 153 | // verification should fail on key mismatch 154 | if err := verifier.Verify(content, sig); err != ErrVerification { 155 | t.Fatalf("rsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification) 156 | } 157 | } 158 | 159 | func Test_rsaVerifier_Verify_InvalidSignature(t *testing.T) { 160 | // generate key 161 | alg := AlgorithmPS256 162 | key := generateTestRSAKey(t) 163 | 164 | // generate a valid signature with a tampered one 165 | content, sig := signTestData(t, alg, key) 166 | tamperedSig := make([]byte, len(sig)) 167 | copy(tamperedSig, sig) 168 | tamperedSig[0]++ 169 | 170 | // set up verifier with a different algorithm 171 | verifier := &rsaVerifier{ 172 | alg: alg, 173 | key: &key.PublicKey, 174 | } 175 | 176 | // verification should fail on invalid signature 177 | tests := []struct { 178 | name string 179 | signature []byte 180 | }{ 181 | { 182 | name: "nil signature", 183 | signature: nil, 184 | }, 185 | { 186 | name: "empty signature", 187 | signature: []byte{}, 188 | }, 189 | { 190 | name: "incomplete signature", 191 | signature: sig[:len(sig)-2], 192 | }, 193 | { 194 | name: "tampered signature", 195 | signature: tamperedSig, 196 | }, 197 | { 198 | name: "too many signature bytes", 199 | signature: append(sig, 0), 200 | }, 201 | } 202 | for _, tt := range tests { 203 | t.Run(tt.name, func(t *testing.T) { 204 | if err := verifier.Verify(content, tt.signature); err != ErrVerification { 205 | t.Errorf("rsaVerifier.Verify() error = %v, wantErr %v", err, ErrVerification) 206 | } 207 | }) 208 | } 209 | } 210 | 211 | func Test_rsaVerifier_Verify_HashFailure(t *testing.T) { 212 | // generate key 213 | alg := AlgorithmPS256 214 | key := generateTestRSAKey(t) 215 | 216 | // generate a valid signature 217 | content, sig := signTestData(t, alg, key) 218 | 219 | // set up verifier 220 | verifier, err := NewVerifier(alg, key.Public()) 221 | if err != nil { 222 | t.Fatalf("NewVerifier() error = %v", err) 223 | } 224 | 225 | // verify with bad hash implementation 226 | crypto.RegisterHash(crypto.SHA256, badHashNew) 227 | defer crypto.RegisterHash(crypto.SHA256, sha256.New) 228 | if err := verifier.Verify(content, sig); err == nil { 229 | t.Fatalf("rsaVerifier.Verify() error = nil, wantErr true") 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /scripts/licenses.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | type go-licenses &> /dev/null || go get github.com/google/go-licenses 6 | 7 | MODULES+=("github.com/veraison/go-cose") 8 | 9 | for module in ${MODULES[@]} 10 | do 11 | echo ">> retrieving licenses [ ${module} ]" 12 | go-licenses csv ${module} 13 | done 14 | -------------------------------------------------------------------------------- /sign.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/fxamacker/cbor/v2" 10 | ) 11 | 12 | // signature represents a COSE_Signature CBOR object: 13 | // 14 | // COSE_Signature = [ 15 | // Headers, 16 | // signature : bstr 17 | // ] 18 | // 19 | // Reference: https://tools.ietf.org/html/rfc8152#section-4.1 20 | type signature struct { 21 | _ struct{} `cbor:",toarray"` 22 | Protected cbor.RawMessage 23 | Unprotected cbor.RawMessage 24 | Signature byteString 25 | } 26 | 27 | // signaturePrefix represents the fixed prefix of COSE_Signature. 28 | var signaturePrefix = []byte{ 29 | 0x83, // Array of length 3 30 | } 31 | 32 | // Signature represents a decoded COSE_Signature. 33 | // 34 | // Reference: https://tools.ietf.org/html/rfc8152#section-4.1 35 | // 36 | // # Experimental 37 | // 38 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 39 | // later release. 40 | type Signature struct { 41 | Headers Headers 42 | Signature []byte 43 | } 44 | 45 | // NewSignature returns a Signature with header initialized. 46 | // 47 | // # Experimental 48 | // 49 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 50 | // later release. 51 | func NewSignature() *Signature { 52 | return &Signature{ 53 | Headers: Headers{ 54 | Protected: ProtectedHeader{}, 55 | Unprotected: UnprotectedHeader{}, 56 | }, 57 | } 58 | } 59 | 60 | // MarshalCBOR encodes Signature into a COSE_Signature object. 61 | // 62 | // # Experimental 63 | // 64 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 65 | // later release. 66 | func (s *Signature) MarshalCBOR() ([]byte, error) { 67 | if s == nil { 68 | return nil, errors.New("cbor: MarshalCBOR on nil Signature pointer") 69 | } 70 | if len(s.Signature) == 0 { 71 | return nil, ErrEmptySignature 72 | } 73 | protected, unprotected, err := s.Headers.marshal() 74 | if err != nil { 75 | return nil, err 76 | } 77 | sig := signature{ 78 | Protected: protected, 79 | Unprotected: unprotected, 80 | Signature: s.Signature, 81 | } 82 | return encMode.Marshal(sig) 83 | } 84 | 85 | // UnmarshalCBOR decodes a COSE_Signature object into Signature. 86 | // 87 | // # Experimental 88 | // 89 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 90 | // later release. 91 | func (s *Signature) UnmarshalCBOR(data []byte) error { 92 | if s == nil { 93 | return errors.New("cbor: UnmarshalCBOR on nil Signature pointer") 94 | } 95 | 96 | // fast signature check 97 | if !bytes.HasPrefix(data, signaturePrefix) { 98 | return errors.New("cbor: invalid Signature object") 99 | } 100 | 101 | // decode to signature and parse 102 | var raw signature 103 | if err := decModeWithTagsForbidden.Unmarshal(data, &raw); err != nil { 104 | return err 105 | } 106 | if len(raw.Signature) == 0 { 107 | return ErrEmptySignature 108 | } 109 | sig := Signature{ 110 | Headers: Headers{ 111 | RawProtected: raw.Protected, 112 | RawUnprotected: raw.Unprotected, 113 | }, 114 | Signature: raw.Signature, 115 | } 116 | if err := sig.Headers.UnmarshalFromRaw(); err != nil { 117 | return err 118 | } 119 | 120 | *s = sig 121 | return nil 122 | } 123 | 124 | // Sign signs a Signature using the provided Signer. 125 | // Signing a COSE_Signature requires the encoded protected header and the 126 | // payload of its parent message. 127 | // 128 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 129 | // 130 | // # Experimental 131 | // 132 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 133 | // later release. 134 | func (s *Signature) Sign(rand io.Reader, signer Signer, protected cbor.RawMessage, payload, external []byte) error { 135 | if s == nil { 136 | return errors.New("signing nil Signature") 137 | } 138 | if payload == nil { 139 | return ErrMissingPayload 140 | } 141 | if len(s.Signature) > 0 { 142 | return errors.New("Signature already has signature bytes") 143 | } 144 | if len(protected) == 0 || protected[0]>>5 != 2 { // protected is a bstr 145 | return errors.New("invalid body protected headers") 146 | } 147 | 148 | // check algorithm if present. 149 | // `alg` header MUST present if there is no externally supplied data. 150 | alg := signer.Algorithm() 151 | if err := s.Headers.ensureSigningAlgorithm(alg, external); err != nil { 152 | return err 153 | } 154 | 155 | // sign the message 156 | toBeSigned, err := s.toBeSigned(protected, payload, external) 157 | if err != nil { 158 | return err 159 | } 160 | sig, err := signer.Sign(rand, toBeSigned) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | s.Signature = sig 166 | return nil 167 | } 168 | 169 | // Verify verifies the signature, returning nil on success or a suitable error 170 | // if verification fails. 171 | // Verifying a COSE_Signature requires the encoded protected header and the 172 | // payload of its parent message. 173 | // 174 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 175 | // 176 | // # Experimental 177 | // 178 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 179 | // later release. 180 | func (s *Signature) Verify(verifier Verifier, protected cbor.RawMessage, payload, external []byte) error { 181 | if s == nil { 182 | return errors.New("verifying nil Signature") 183 | } 184 | if payload == nil { 185 | return ErrMissingPayload 186 | } 187 | if len(s.Signature) == 0 { 188 | return ErrEmptySignature 189 | } 190 | if len(protected) == 0 || protected[0]>>5 != 2 { // protected is a bstr 191 | return errors.New("invalid body protected headers") 192 | } 193 | 194 | // check algorithm if present. 195 | // `alg` header MUST present if there is no externally supplied data. 196 | alg := verifier.Algorithm() 197 | err := s.Headers.ensureVerificationAlgorithm(alg, external) 198 | if err != nil { 199 | return err 200 | } 201 | 202 | // verify the message 203 | toBeSigned, err := s.toBeSigned(protected, payload, external) 204 | if err != nil { 205 | return err 206 | } 207 | return verifier.Verify(toBeSigned, s.Signature) 208 | } 209 | 210 | // toBeSigned constructs Sig_structure, computes and returns ToBeSigned. 211 | // 212 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 213 | func (s *Signature) toBeSigned(bodyProtected cbor.RawMessage, payload, external []byte) ([]byte, error) { 214 | // create a Sig_structure and populate it with the appropriate fields. 215 | // 216 | // Sig_structure = [ 217 | // context : "Signature", 218 | // body_protected : empty_or_serialized_map, 219 | // sign_protected : empty_or_serialized_map, 220 | // external_aad : bstr, 221 | // payload : bstr 222 | // ] 223 | bodyProtected, err := deterministicBinaryString(bodyProtected) 224 | if err != nil { 225 | return nil, err 226 | } 227 | var signProtected cbor.RawMessage 228 | signProtected, err = s.Headers.MarshalProtected() 229 | if err != nil { 230 | return nil, err 231 | } 232 | signProtected, err = deterministicBinaryString(signProtected) 233 | if err != nil { 234 | return nil, err 235 | } 236 | if external == nil { 237 | external = []byte{} 238 | } 239 | sigStructure := []any{ 240 | "Signature", // context 241 | bodyProtected, // body_protected 242 | signProtected, // sign_protected 243 | external, // external_aad 244 | payload, // payload 245 | } 246 | 247 | // create the value ToBeSigned by encoding the Sig_structure to a byte 248 | // string. 249 | return encMode.Marshal(sigStructure) 250 | } 251 | 252 | // signMessage represents a COSE_Sign CBOR object: 253 | // 254 | // COSE_Sign = [ 255 | // Headers, 256 | // payload : bstr / nil, 257 | // signatures : [+ COSE_Signature] 258 | // ] 259 | // 260 | // Reference: https://tools.ietf.org/html/rfc8152#section-4.1 261 | type signMessage struct { 262 | _ struct{} `cbor:",toarray"` 263 | Protected cbor.RawMessage 264 | Unprotected cbor.RawMessage 265 | Payload byteString 266 | Signatures []cbor.RawMessage 267 | } 268 | 269 | // signMessagePrefix represents the fixed prefix of COSE_Sign_Tagged. 270 | var signMessagePrefix = []byte{ 271 | 0xd8, 0x62, // #6.98 272 | 0x84, // Array of length 4 273 | } 274 | 275 | // SignMessage represents a decoded COSE_Sign message. 276 | // 277 | // Reference: https://tools.ietf.org/html/rfc8152#section-4.1 278 | // 279 | // # Experimental 280 | // 281 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 282 | // later release. 283 | type SignMessage struct { 284 | Headers Headers 285 | Payload []byte 286 | Signatures []*Signature 287 | } 288 | 289 | // NewSignMessage returns a SignMessage with header initialized. 290 | // 291 | // # Experimental 292 | // 293 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 294 | // later release. 295 | func NewSignMessage() *SignMessage { 296 | return &SignMessage{ 297 | Headers: Headers{ 298 | Protected: ProtectedHeader{}, 299 | Unprotected: UnprotectedHeader{}, 300 | }, 301 | } 302 | } 303 | 304 | // MarshalCBOR encodes SignMessage into a COSE_Sign_Tagged object. 305 | // 306 | // # Experimental 307 | // 308 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 309 | // later release. 310 | func (m *SignMessage) MarshalCBOR() ([]byte, error) { 311 | if m == nil { 312 | return nil, errors.New("cbor: MarshalCBOR on nil SignMessage pointer") 313 | } 314 | if len(m.Signatures) == 0 { 315 | return nil, ErrNoSignatures 316 | } 317 | protected, unprotected, err := m.Headers.marshal() 318 | if err != nil { 319 | return nil, err 320 | } 321 | signatures := make([]cbor.RawMessage, 0, len(m.Signatures)) 322 | for _, sig := range m.Signatures { 323 | sigCBOR, err := sig.MarshalCBOR() 324 | if err != nil { 325 | return nil, err 326 | } 327 | signatures = append(signatures, sigCBOR) 328 | } 329 | content := signMessage{ 330 | Protected: protected, 331 | Unprotected: unprotected, 332 | Payload: m.Payload, 333 | Signatures: signatures, 334 | } 335 | return encMode.Marshal(cbor.Tag{ 336 | Number: CBORTagSignMessage, 337 | Content: content, 338 | }) 339 | } 340 | 341 | // UnmarshalCBOR decodes a COSE_Sign_Tagged object into SignMessage. 342 | // 343 | // # Experimental 344 | // 345 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 346 | // later release. 347 | func (m *SignMessage) UnmarshalCBOR(data []byte) error { 348 | if m == nil { 349 | return errors.New("cbor: UnmarshalCBOR on nil SignMessage pointer") 350 | } 351 | 352 | // fast message check 353 | if !bytes.HasPrefix(data, signMessagePrefix) { 354 | return errors.New("cbor: invalid COSE_Sign_Tagged object") 355 | } 356 | 357 | // decode to signMessage and parse 358 | var raw signMessage 359 | if err := decModeWithTagsForbidden.Unmarshal(data[2:], &raw); err != nil { 360 | return err 361 | } 362 | if len(raw.Signatures) == 0 { 363 | return ErrNoSignatures 364 | } 365 | signatures := make([]*Signature, 0, len(raw.Signatures)) 366 | for _, sigCBOR := range raw.Signatures { 367 | sig := &Signature{} 368 | if err := sig.UnmarshalCBOR(sigCBOR); err != nil { 369 | return err 370 | } 371 | signatures = append(signatures, sig) 372 | } 373 | msg := SignMessage{ 374 | Headers: Headers{ 375 | RawProtected: raw.Protected, 376 | RawUnprotected: raw.Unprotected, 377 | }, 378 | Payload: raw.Payload, 379 | Signatures: signatures, 380 | } 381 | if err := msg.Headers.UnmarshalFromRaw(); err != nil { 382 | return err 383 | } 384 | 385 | *m = msg 386 | return nil 387 | } 388 | 389 | // Sign signs a SignMessage using the provided signers corresponding to the 390 | // signatures. 391 | // 392 | // See [Signature.Sign] for advanced signing scenarios. 393 | // 394 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 395 | // 396 | // # Experimental 397 | // 398 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 399 | // later release. 400 | func (m *SignMessage) Sign(rand io.Reader, external []byte, signers ...Signer) error { 401 | if m == nil { 402 | return errors.New("signing nil SignMessage") 403 | } 404 | if m.Payload == nil { 405 | return ErrMissingPayload 406 | } 407 | switch len(m.Signatures) { 408 | case 0: 409 | return ErrNoSignatures 410 | case len(signers): 411 | // no ops 412 | default: 413 | return fmt.Errorf("%d signers for %d signatures", len(signers), len(m.Signatures)) 414 | } 415 | 416 | // populate common parameters 417 | var protected cbor.RawMessage 418 | protected, err := m.Headers.MarshalProtected() 419 | if err != nil { 420 | return err 421 | } 422 | 423 | // sign message accordingly 424 | for i, signature := range m.Signatures { 425 | if err := signature.Sign(rand, signers[i], protected, m.Payload, external); err != nil { 426 | return err 427 | } 428 | } 429 | 430 | return nil 431 | } 432 | 433 | // Verify verifies the signatures on the SignMessage against the corresponding 434 | // verifier, returning nil on success or a suitable error if verification fails. 435 | // 436 | // See [Signature.Verify] for advanced verification scenarios like threshold 437 | // policies. 438 | // 439 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 440 | // 441 | // # Experimental 442 | // 443 | // Notice: The COSE Sign API is EXPERIMENTAL and may be changed or removed in a 444 | // later release. 445 | func (m *SignMessage) Verify(external []byte, verifiers ...Verifier) error { 446 | if m == nil { 447 | return errors.New("verifying nil SignMessage") 448 | } 449 | if m.Payload == nil { 450 | return ErrMissingPayload 451 | } 452 | switch len(m.Signatures) { 453 | case 0: 454 | return ErrNoSignatures 455 | case len(verifiers): 456 | // no ops 457 | default: 458 | return fmt.Errorf("%d verifiers for %d signatures", len(verifiers), len(m.Signatures)) 459 | } 460 | 461 | // populate common parameters 462 | var protected cbor.RawMessage 463 | protected, err := m.Headers.MarshalProtected() 464 | if err != nil { 465 | return err 466 | } 467 | 468 | // verify message accordingly 469 | for i, signature := range m.Signatures { 470 | if err := signature.Verify(verifiers[i], protected, m.Payload, external); err != nil { 471 | return err 472 | } 473 | } 474 | return nil 475 | } 476 | -------------------------------------------------------------------------------- /sign1.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | 8 | "github.com/fxamacker/cbor/v2" 9 | ) 10 | 11 | // sign1Message represents a COSE_Sign1 CBOR object: 12 | // 13 | // COSE_Sign1 = [ 14 | // Headers, 15 | // payload : bstr / nil, 16 | // signature : bstr 17 | // ] 18 | // 19 | // Reference: https://tools.ietf.org/html/rfc8152#section-4.2 20 | type sign1Message struct { 21 | _ struct{} `cbor:",toarray"` 22 | Protected cbor.RawMessage 23 | Unprotected cbor.RawMessage 24 | Payload byteString 25 | Signature byteString 26 | } 27 | 28 | // sign1MessagePrefix represents the fixed prefix of COSE_Sign1_Tagged. 29 | var sign1MessagePrefix = []byte{ 30 | 0xd2, // #6.18 31 | 0x84, // Array of length 4 32 | } 33 | 34 | // Sign1Message represents a decoded COSE_Sign1 message. 35 | // 36 | // Reference: https://tools.ietf.org/html/rfc8152#section-4.2 37 | type Sign1Message struct { 38 | Headers Headers 39 | Payload []byte 40 | Signature []byte 41 | } 42 | 43 | // NewSign1Message returns a Sign1Message with header initialized. 44 | func NewSign1Message() *Sign1Message { 45 | return &Sign1Message{ 46 | Headers: Headers{ 47 | Protected: ProtectedHeader{}, 48 | Unprotected: UnprotectedHeader{}, 49 | }, 50 | } 51 | } 52 | 53 | // MarshalCBOR encodes Sign1Message into a COSE_Sign1_Tagged object. 54 | func (m *Sign1Message) MarshalCBOR() ([]byte, error) { 55 | content, err := m.getContent() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return encMode.Marshal(cbor.Tag{ 61 | Number: CBORTagSign1Message, 62 | Content: content, 63 | }) 64 | } 65 | 66 | // UnmarshalCBOR decodes a COSE_Sign1_Tagged object into Sign1Message. 67 | func (m *Sign1Message) UnmarshalCBOR(data []byte) error { 68 | if m == nil { 69 | return errors.New("cbor: UnmarshalCBOR on nil Sign1Message pointer") 70 | } 71 | 72 | // fast message check 73 | if !bytes.HasPrefix(data, sign1MessagePrefix) { 74 | return errors.New("cbor: invalid COSE_Sign1_Tagged object") 75 | } 76 | 77 | return m.doUnmarshal(data[1:]) 78 | } 79 | 80 | // Sign signs a Sign1Message using the provided Signer. 81 | // The signature is stored in m.Signature. 82 | // 83 | // Note that m.Signature is only valid as long as m.Headers.Protected and 84 | // m.Payload remain unchanged after calling this method. 85 | // It is possible to modify m.Headers.Unprotected after signing, 86 | // i.e., add counter signatures or timestamps. 87 | // 88 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 89 | func (m *Sign1Message) Sign(rand io.Reader, external []byte, signer Signer) error { 90 | if m == nil { 91 | return errors.New("signing nil Sign1Message") 92 | } 93 | if m.Payload == nil { 94 | return ErrMissingPayload 95 | } 96 | if len(m.Signature) > 0 { 97 | return errors.New("Sign1Message signature already has signature bytes") 98 | } 99 | 100 | // check algorithm if present. 101 | // `alg` header MUST be present if there is no externally supplied data. 102 | alg := signer.Algorithm() 103 | err := m.Headers.ensureSigningAlgorithm(alg, external) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | // sign the message 109 | toBeSigned, err := m.toBeSigned(external) 110 | if err != nil { 111 | return err 112 | } 113 | sig, err := signer.Sign(rand, toBeSigned) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | m.Signature = sig 119 | return nil 120 | } 121 | 122 | // Verify verifies the signature on the Sign1Message returning nil on success or 123 | // a suitable error if verification fails. 124 | // 125 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 126 | func (m *Sign1Message) Verify(external []byte, verifier Verifier) error { 127 | if m == nil { 128 | return errors.New("verifying nil Sign1Message") 129 | } 130 | if m.Payload == nil { 131 | return ErrMissingPayload 132 | } 133 | if len(m.Signature) == 0 { 134 | return ErrEmptySignature 135 | } 136 | 137 | // check algorithm if present. 138 | // `alg` header MUST present if there is no externally supplied data. 139 | alg := verifier.Algorithm() 140 | err := m.Headers.ensureVerificationAlgorithm(alg, external) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | // verify the message 146 | toBeSigned, err := m.toBeSigned(external) 147 | if err != nil { 148 | return err 149 | } 150 | return verifier.Verify(toBeSigned, m.Signature) 151 | } 152 | 153 | // toBeSigned constructs Sig_structure, computes and returns ToBeSigned. 154 | // 155 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 156 | func (m *Sign1Message) toBeSigned(external []byte) ([]byte, error) { 157 | // create a Sig_structure and populate it with the appropriate fields. 158 | // 159 | // Sig_structure = [ 160 | // context : "Signature1", 161 | // body_protected : empty_or_serialized_map, 162 | // external_aad : bstr, 163 | // payload : bstr 164 | // ] 165 | var protected cbor.RawMessage 166 | protected, err := m.Headers.MarshalProtected() 167 | if err != nil { 168 | return nil, err 169 | } 170 | protected, err = deterministicBinaryString(protected) 171 | if err != nil { 172 | return nil, err 173 | } 174 | if external == nil { 175 | external = []byte{} 176 | } 177 | sigStructure := []any{ 178 | "Signature1", // context 179 | protected, // body_protected 180 | external, // external_aad 181 | m.Payload, // payload 182 | } 183 | 184 | // create the value ToBeSigned by encoding the Sig_structure to a byte 185 | // string. 186 | return encMode.Marshal(sigStructure) 187 | } 188 | 189 | func (m *Sign1Message) getContent() (sign1Message, error) { 190 | if m == nil { 191 | return sign1Message{}, errors.New("cbor: MarshalCBOR on nil Sign1Message pointer") 192 | } 193 | if len(m.Signature) == 0 { 194 | return sign1Message{}, ErrEmptySignature 195 | } 196 | protected, unprotected, err := m.Headers.marshal() 197 | if err != nil { 198 | return sign1Message{}, err 199 | } 200 | 201 | content := sign1Message{ 202 | Protected: protected, 203 | Unprotected: unprotected, 204 | Payload: m.Payload, 205 | Signature: m.Signature, 206 | } 207 | 208 | return content, nil 209 | } 210 | 211 | func (m *Sign1Message) doUnmarshal(data []byte) error { 212 | // decode to sign1Message and parse 213 | var raw sign1Message 214 | if err := decModeWithTagsForbidden.Unmarshal(data, &raw); err != nil { 215 | return err 216 | } 217 | if len(raw.Signature) == 0 { 218 | return ErrEmptySignature 219 | } 220 | msg := Sign1Message{ 221 | Headers: Headers{ 222 | RawProtected: raw.Protected, 223 | RawUnprotected: raw.Unprotected, 224 | }, 225 | Payload: raw.Payload, 226 | Signature: raw.Signature, 227 | } 228 | if err := msg.Headers.UnmarshalFromRaw(); err != nil { 229 | return err 230 | } 231 | 232 | *m = msg 233 | return nil 234 | } 235 | 236 | // Sign1 signs a [Sign1Message] using the provided [Signer]. 237 | // 238 | // This method is a wrapper of [Sign1Message.Sign]. 239 | // 240 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 241 | func Sign1(rand io.Reader, signer Signer, headers Headers, payload []byte, external []byte) ([]byte, error) { 242 | msg := Sign1Message{ 243 | Headers: headers, 244 | Payload: payload, 245 | } 246 | err := msg.Sign(rand, external, signer) 247 | if err != nil { 248 | return nil, err 249 | } 250 | return msg.MarshalCBOR() 251 | } 252 | 253 | type UntaggedSign1Message Sign1Message 254 | 255 | // MarshalCBOR encodes UntaggedSign1Message into a COSE_Sign1 object. 256 | func (m *UntaggedSign1Message) MarshalCBOR() ([]byte, error) { 257 | content, err := (*Sign1Message)(m).getContent() 258 | if err != nil { 259 | return nil, err 260 | } 261 | 262 | return encMode.Marshal(content) 263 | } 264 | 265 | // UnmarshalCBOR decodes a COSE_Sign1 object into an UntaggedSign1Message. 266 | func (m *UntaggedSign1Message) UnmarshalCBOR(data []byte) error { 267 | if m == nil { 268 | return errors.New("cbor: UnmarshalCBOR on nil UntaggedSign1Message pointer") 269 | } 270 | 271 | if len(data) == 0 { 272 | return errors.New("cbor: zero length data") 273 | } 274 | 275 | // fast message check - ensure the frist byte indicates a four-element array 276 | if data[0] != sign1MessagePrefix[1] { 277 | return errors.New("cbor: invalid COSE_Sign1 object") 278 | } 279 | 280 | return (*Sign1Message)(m).doUnmarshal(data) 281 | } 282 | 283 | // Sign signs an UntaggedSign1Message using the provided [Signer]. 284 | // The signature is stored in m.Signature. 285 | // 286 | // Note that m.Signature is only valid as long as m.Headers.Protected and 287 | // m.Payload remain unchanged after calling this method. 288 | // It is possible to modify m.Headers.Unprotected after signing, 289 | // i.e., add counter signatures or timestamps. 290 | // 291 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 292 | func (m *UntaggedSign1Message) Sign(rand io.Reader, external []byte, signer Signer) error { 293 | return (*Sign1Message)(m).Sign(rand, external, signer) 294 | } 295 | 296 | // Verify verifies the signature on the UntaggedSign1Message returning nil on success or 297 | // a suitable error if verification fails. 298 | // 299 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 300 | func (m *UntaggedSign1Message) Verify(external []byte, verifier Verifier) error { 301 | return (*Sign1Message)(m).Verify(external, verifier) 302 | } 303 | 304 | // Sign1Untagged signs an UntaggedSign1Message using the provided [Signer]. 305 | // 306 | // This method is a wrapper of [UntaggedSign1Message.Sign]. 307 | // 308 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-4.4 309 | func Sign1Untagged(rand io.Reader, signer Signer, headers Headers, payload []byte, external []byte) ([]byte, error) { 310 | msg := UntaggedSign1Message{ 311 | Headers: headers, 312 | Payload: payload, 313 | } 314 | err := msg.Sign(rand, external, signer) 315 | if err != nil { 316 | return nil, err 317 | } 318 | return msg.MarshalCBOR() 319 | } 320 | -------------------------------------------------------------------------------- /signer.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/ed25519" 7 | "crypto/rsa" 8 | "errors" 9 | "fmt" 10 | "io" 11 | ) 12 | 13 | // Signer is an interface for private keys to sign COSE signatures. 14 | type Signer interface { 15 | // Algorithm returns the signing algorithm associated with the private key. 16 | Algorithm() Algorithm 17 | 18 | // Sign signs message content with the private key, possibly using entropy 19 | // from rand. 20 | // The resulting signature should follow RFC 8152 section 8. 21 | // 22 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8 23 | Sign(rand io.Reader, content []byte) ([]byte, error) 24 | } 25 | 26 | // DigestSigner is an interface for private keys to sign digested COSE 27 | // signatures. 28 | type DigestSigner interface { 29 | // Algorithm returns the signing algorithm associated with the private key. 30 | Algorithm() Algorithm 31 | 32 | // SignDigest signs message digest with the private key, possibly using 33 | // entropy from rand. 34 | // The resulting signature should follow RFC 8152 section 8. 35 | SignDigest(rand io.Reader, digest []byte) ([]byte, error) 36 | } 37 | 38 | // NewSigner returns a signer with a given signing key. 39 | // The signing key can be a golang built-in crypto private key, a key in HSM, or 40 | // a remote KMS. 41 | // 42 | // Developers are encouraged to implement the [cose.Signer] interface instead of 43 | // the [crypto.Signer] interface for better performance. 44 | // 45 | // All signing keys implementing [crypto.Signer] with `Public()` returning a 46 | // public key of type [*rsa.PublicKey], [*ecdsa.PublicKey], or 47 | // [ed25519.PublicKey] are accepted. 48 | // 49 | // The returned signer for rsa and ecdsa keys also implements 50 | // [cose.DigestSigner]. 51 | // 52 | // Note: [*rsa.PrivateKey], [*ecdsa.PrivateKey], and [ed25519.PrivateKey] 53 | // implement [crypto.Signer]. 54 | func NewSigner(alg Algorithm, key crypto.Signer) (Signer, error) { 55 | var errReason string 56 | switch alg { 57 | case AlgorithmPS256, AlgorithmPS384, AlgorithmPS512: 58 | vk, ok := key.Public().(*rsa.PublicKey) 59 | if !ok { 60 | return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) 61 | } 62 | // RFC 8230 section 6.1 requires RSA keys having a minimum size of 2048 63 | // bits. 64 | // Reference: https://www.rfc-editor.org/rfc/rfc8230.html#section-6.1 65 | if vk.N.BitLen() < 2048 { 66 | return nil, errors.New("RSA key must be at least 2048 bits long") 67 | } 68 | return &rsaSigner{ 69 | alg: alg, 70 | key: key, 71 | }, nil 72 | case AlgorithmES256, AlgorithmES384, AlgorithmES512: 73 | vk, ok := key.Public().(*ecdsa.PublicKey) 74 | if !ok { 75 | return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) 76 | } 77 | if sk, ok := key.(*ecdsa.PrivateKey); ok { 78 | return &ecdsaKeySigner{ 79 | alg: alg, 80 | key: sk, 81 | }, nil 82 | } 83 | return &ecdsaCryptoSigner{ 84 | alg: alg, 85 | key: vk, 86 | signer: key, 87 | }, nil 88 | case AlgorithmEdDSA: 89 | if _, ok := key.Public().(ed25519.PublicKey); !ok { 90 | return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) 91 | } 92 | return &ed25519Signer{ 93 | key: key, 94 | }, nil 95 | case AlgorithmReserved: 96 | errReason = "can't be implemented" 97 | case AlgorithmRS256, AlgorithmRS384, AlgorithmRS512: 98 | errReason = "no built-in implementation available" 99 | default: 100 | errReason = "unknown algorithm" 101 | } 102 | return nil, fmt.Errorf("can't create new Signer for %s: %s: %w", alg, errReason, ErrAlgorithmNotSupported) 103 | } 104 | -------------------------------------------------------------------------------- /signer_test.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "encoding/hex" 8 | "io" 9 | "reflect" 10 | "testing" 11 | ) 12 | 13 | func signTestData(t *testing.T, alg Algorithm, key crypto.Signer) (content, sig []byte) { 14 | signer, err := NewSigner(alg, key) 15 | if err != nil { 16 | t.Fatalf("NewSigner() error = %v", err) 17 | } 18 | content = []byte("hello world") 19 | sig, err = signer.Sign(rand.Reader, content) 20 | if err != nil { 21 | t.Fatalf("Sign() error = %v", err) 22 | } 23 | return 24 | } 25 | 26 | func TestNewSigner(t *testing.T) { 27 | // generate ecdsa key 28 | ecdsaKey := generateTestECDSAKey(t) 29 | ecdsaWrappedKey := struct { 30 | crypto.Signer 31 | }{ 32 | Signer: ecdsaKey, 33 | } 34 | 35 | // generate ed25519 key 36 | _, ed25519Key := generateTestEd25519Key(t) 37 | 38 | // generate rsa keys 39 | rsaKey := generateTestRSAKey(t) 40 | rsaKeyLowEntropy, err := rsa.GenerateKey(rand.Reader, 1024) 41 | if err != nil { 42 | t.Fatalf("rsa.GenerateKey() error = %v", err) 43 | } 44 | 45 | // run tests 46 | tests := []struct { 47 | name string 48 | alg Algorithm 49 | key crypto.Signer 50 | want Signer 51 | wantErr string 52 | }{ 53 | { 54 | name: "ecdsa key signer", 55 | alg: AlgorithmES256, 56 | key: ecdsaKey, 57 | want: &ecdsaKeySigner{ 58 | alg: AlgorithmES256, 59 | key: ecdsaKey, 60 | }, 61 | }, 62 | { 63 | name: "ecdsa crypto signer", 64 | alg: AlgorithmES256, 65 | key: ecdsaWrappedKey, 66 | want: &ecdsaCryptoSigner{ 67 | alg: AlgorithmES256, 68 | key: &ecdsaKey.PublicKey, 69 | signer: ecdsaWrappedKey, 70 | }, 71 | }, 72 | { 73 | name: "ecdsa key mismatch", 74 | alg: AlgorithmES256, 75 | key: rsaKey, 76 | wantErr: "ES256: invalid public key", 77 | }, 78 | { 79 | name: "ed25519 signer", 80 | alg: AlgorithmEdDSA, 81 | key: ed25519Key, 82 | want: &ed25519Signer{ 83 | key: ed25519Key, 84 | }, 85 | }, 86 | { 87 | name: "ed25519 key mismatch", 88 | alg: AlgorithmEdDSA, 89 | key: rsaKey, 90 | wantErr: "EdDSA: invalid public key", 91 | }, 92 | { 93 | name: "rsa signer", 94 | alg: AlgorithmPS256, 95 | key: rsaKey, 96 | want: &rsaSigner{ 97 | alg: AlgorithmPS256, 98 | key: rsaKey, 99 | }, 100 | }, 101 | { 102 | name: "rsa key mismatch", 103 | alg: AlgorithmPS256, 104 | key: ecdsaKey, 105 | wantErr: "PS256: invalid public key", 106 | }, 107 | { 108 | name: "rsa key under minimum entropy", 109 | alg: AlgorithmPS256, 110 | key: rsaKeyLowEntropy, 111 | wantErr: "RSA key must be at least 2048 bits long", 112 | }, 113 | { 114 | name: "unsupported rsa signing algorithm", 115 | alg: AlgorithmRS256, 116 | wantErr: "can't create new Signer for RS256: no built-in implementation available: algorithm not supported", 117 | }, 118 | { 119 | name: "reserved algorithm", 120 | alg: AlgorithmReserved, 121 | wantErr: "can't create new Signer for Reserved: can't be implemented: algorithm not supported", 122 | }, 123 | { 124 | name: "unassigned algorithm", 125 | alg: -1, 126 | wantErr: "can't create new Signer for Algorithm(-1): unknown algorithm: algorithm not supported", 127 | }, 128 | } 129 | for _, tt := range tests { 130 | t.Run(tt.name, func(t *testing.T) { 131 | got, err := NewSigner(tt.alg, tt.key) 132 | if err != nil && (err.Error() != tt.wantErr) { 133 | t.Errorf("NewSigner() error = %v, wantErr %v", err, tt.wantErr) 134 | return 135 | } else if err == nil && (tt.wantErr != "") { 136 | t.Errorf("NewSigner() error = %v, wantErr %v", err, tt.wantErr) 137 | return 138 | } 139 | if !reflect.DeepEqual(got, tt.want) { 140 | t.Errorf("NewSigner() = %v, want %v", got, tt.want) 141 | } 142 | }) 143 | } 144 | } 145 | 146 | const algorithmMock Algorithm = -0x6d6f636b 147 | 148 | type mockSigner struct { 149 | t *testing.T 150 | m map[string]string 151 | } 152 | 153 | func newMockSigner(t *testing.T) *mockSigner { 154 | return &mockSigner{ 155 | t: t, 156 | m: make(map[string]string), 157 | } 158 | } 159 | 160 | func (m *mockSigner) setup(content, sig []byte) { 161 | m.m[hex.EncodeToString(content)] = hex.EncodeToString(sig) // deep copy 162 | } 163 | 164 | func (m *mockSigner) Algorithm() Algorithm { 165 | return algorithmMock 166 | } 167 | 168 | func (m *mockSigner) Sign(rand io.Reader, content []byte) ([]byte, error) { 169 | sigHex, ok := m.m[hex.EncodeToString(content)] 170 | if !ok { 171 | m.t.Fatalf("mockSigner: not setup: %v", content) 172 | } 173 | sig, err := hex.DecodeString(sigHex) 174 | if err != nil { 175 | m.t.Fatalf("mockSigner: failed to decode: %v", sigHex) 176 | } 177 | return sig, nil 178 | } 179 | -------------------------------------------------------------------------------- /testdata/sign1-sign-0000.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "D55A49BD-53D9-42B1-9E76-E0CF2AD33E9D", 3 | "title": "Sign1 w/ external input - ECDSA w/ SHA-256 (sign)", 4 | "description": "Sign with one signer using ECDSA w/ SHA-256 supplying external input", 5 | "key": { 6 | "kty": "EC", 7 | "crv": "P-256", 8 | "x": "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU", 9 | "y": "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0", 10 | "d": "jpsQnnGQmL-YBIffH1136cspYG6-0iY7X1fCE9-E9LI" 11 | }, 12 | "alg": "ES256", 13 | "sign1::sign": { 14 | "payload": "546869732069732074686520636f6e74656e742e", 15 | "protectedHeaders": { 16 | "cborHex": "a10126", 17 | "cborDiag": "{1: -7}" 18 | }, 19 | "unprotectedHeaders": { 20 | "cborHex": "a104423131", 21 | "cborDiag": "{4: '11'}" 22 | }, 23 | "tbsHex": { 24 | "cborHex": "846a5369676e61747572653143a101264c11aa22bb33cc44dd5500669954546869732069732074686520636f6e74656e742e", 25 | "cborDiag": "[\"Signature1\", h'A10126', h'11AA22BB33CC44DD55006699', h'546869732069732074686520636F6E74656E742E']" 26 | }, 27 | "external": "11aa22bb33cc44dd55006699", 28 | "detached": false, 29 | "expectedOutput": { 30 | "cborHex": "d28443a10126a10442313154546869732069732074686520636f6e74656e742e58403a7487d9a528cb61dd8e99bd652c12577fc47d70ee5af2e703c420584f060fc7a8d61e4a35862b2b531a8447030ab966aeed8dd45ebc507c761431e349995770", 31 | "cborDiag": "18([h'A10126', {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'3A7487D9A528CB61DD8E99BD652C12577FC47D70EE5AF2E703C420584F060FC7A8D61E4A35862B2B531A8447030AB966AEED8DD45EBC507C761431E349995770'])" 32 | }, 33 | "fixedOutputLength": 32 34 | } 35 | } -------------------------------------------------------------------------------- /testdata/sign1-sign-0001.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "0F78DB1C-C30F-47B1-AF19-6D0C0B2F3803", 3 | "title": "Sign1 - ECDSA w/ SHA-256 (sign)", 4 | "description": "Sign with one signer using ECDSA w/ SHA-256", 5 | "key": { 6 | "kty": "EC", 7 | "crv": "P-256", 8 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 9 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", 10 | "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM" 11 | }, 12 | "alg": "ES256", 13 | "sign1::sign": { 14 | "payload": "546869732069732074686520636f6e74656e742e", 15 | "protectedHeaders": { 16 | "cborHex": "a201260300", 17 | "cborDiag": "{1: -7, 3: 0}" 18 | }, 19 | "unprotectedHeaders": { 20 | "cborHex": "a104423131", 21 | "cborDiag": "{4: '11'}" 22 | }, 23 | "tbsHex": { 24 | "cborHex": "846a5369676e61747572653145a2012603004054546869732069732074686520636f6e74656e742e", 25 | "cborDiag": "[\"Signature1\", h'A201260300', h'', h'546869732069732074686520636F6E74656E742E']" 26 | }, 27 | "detached": false, 28 | "expectedOutput": { 29 | "cborHex": "d28445a201260300a10442313154546869732069732074686520636f6e74656e742e58402ad3b9dcc1e13d04f357e11cc8acd825196620e62f0d8deca72672508b829d90e07a3f23be6aa36fd6ebd31e2ed08d1760bffd981f991bfc94a45199a54875c4", 30 | "cborDiag": "18([h'A201260300', {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'2AD3B9DCC1E13D04F357E11CC8ACD825196620E62F0D8DECA72672508B829D90E07A3F23BE6AA36FD6EBD31E2ED08D1760BFFD981F991BFC94A45199A54875C4'])" 31 | }, 32 | "fixedOutputLength": 34 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /testdata/sign1-sign-0002.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "E693D0C8-C702-4E6C-A70D-0D4DA4C408A0", 3 | "title": "Sign1 - ECDSA w/ SHA-384 (sign)", 4 | "description": "Sign with one signer using ECDSA w/ SHA-384", 5 | "key": { 6 | "kty": "EC", 7 | "kid": "P384", 8 | "crv": "P-384", 9 | "x": "kTJyP2KSsBBhnb4kjWmMF7WHVsY55xUPgb7k64rDcjatChoZ1nvjKmYmPh5STRKc", 10 | "y": "mM0weMVU2DKsYDxDJkEP9hZiRZtB8fPfXbzINZj_fF7YQRynNWedHEyzAJOX2e8s", 11 | "d": "ok3Nq97AXlpEusO7jIy1FZATlBP9PNReMU7DWbkLQ5dU90snHuuHVDjEPmtV0fTo" 12 | }, 13 | "alg": "ES384", 14 | "sign1::sign": { 15 | "payload": "546869732069732074686520636f6e74656e742e", 16 | "protectedHeaders": { 17 | "cborHex": "a1013822", 18 | "cborDiag": "{1: -35}" 19 | }, 20 | "unprotectedHeaders": { 21 | "cborHex": "a1044450333834", 22 | "cborDiag": "{4: 'P384'}" 23 | }, 24 | "tbsHex": { 25 | "cborHex": "846a5369676e61747572653144a10138224054546869732069732074686520636f6e74656e742e", 26 | "cborDiag": "[\"Signature1\", h'A1013822', h'', h'546869732069732074686520636F6E74656E742E']" 27 | }, 28 | "detached": false, 29 | "expectedOutput": { 30 | "cborHex": "d28444a1013822a104445033383454546869732069732074686520636f6e74656e742e5860aa46c1ab71cd3c1e68ed62c27653797cb72cba3a856fd5e2f38794eee0d666e88139ec51fb62466f4865ca56df493905911e329e829c1887f6259681360a8e7f7d3fd080dcb0720066f13e1621656700c99d6e3771ac2549fde998ee9b1e2cad", 31 | "cborDiag": "18([h'A1013822', {4: 'P384'}, h'546869732069732074686520636F6E74656E742E', h'AA46C1AB71CD3C1E68ED62C27653797CB72CBA3A856FD5E2F38794EEE0D666E88139EC51FB62466F4865CA56DF493905911E329E829C1887F6259681360A8E7F7D3FD080DCB0720066F13E1621656700C99D6E3771AC2549FDE998EE9B1E2CAD'])" 32 | }, 33 | "fixedOutputLength": 35 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /testdata/sign1-sign-0003.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "06EFA821-9026-4CDD-A4FB-634103472BC3", 3 | "title": "Sign1 - ECDSA w/ SHA-512 (sign)", 4 | "description": "Sign with one signer using ECDSA w/ SHA-512", 5 | "key": { 6 | "kty": "EC", 7 | "kid": "bilbo.baggins@hobbiton.example", 8 | "use": "sig", 9 | "crv": "P-521", 10 | "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", 11 | "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1", 12 | "d": "AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zbKipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt" 13 | }, 14 | "alg": "ES512", 15 | "sign1::sign": { 16 | "payload": "546869732069732074686520636f6e74656e742e", 17 | "protectedHeaders": { 18 | "cborHex": "a1013823", 19 | "cborDiag": "{1: -36}" 20 | }, 21 | "unprotectedHeaders": { 22 | "cborHex": "a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65", 23 | "cborDiag": "{4: 'bilbo.baggins@hobbiton.example'}" 24 | }, 25 | "tbsHex": { 26 | "cborHex": "846a5369676e61747572653144a10138234054546869732069732074686520636f6e74656e742e", 27 | "cborDiag": "[\"Signature1\", h'A1013823', h'', h'546869732069732074686520636F6E74656E742E']" 28 | }, 29 | "detached": false, 30 | "expectedOutput": { 31 | "cborHex": "d28444a1013823a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e58840128bbda237a1b55568da74cefe02cf2d2a6216f80ac757bea8effc056d2f634f6e257077b0dabe9d4b3689eb8228e20f60bc74ff84ae3a38ee9a69e158cbf80f93a017acf5877e5083548a45143b602ccd776c5eb39537a2e68dc8c47ff62e10fc42f045b781e4313fbf421903785c3dfeb181c3a93b46a67a9b0e82947ee83f7b44cf0", 32 | "cborDiag": "18([h'A1013823', {4: 'bilbo.baggins@hobbiton.example'}, h'546869732069732074686520636F6E74656E742E', h'0128BBDA237A1B55568DA74CEFE02CF2D2A6216F80AC757BEA8EFFC056D2F634F6E257077B0DABE9D4B3689EB8228E20F60BC74FF84AE3A38EE9A69E158CBF80F93A017ACF5877E5083548A45143B602CCD776C5EB39537A2E68DC8C47FF62E10FC42F045B781E4313FBF421903785C3DFEB181C3A93B46A67A9B0E82947EE83F7B44CF0'])" 33 | }, 34 | "fixedOutputLength": 62 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /testdata/sign1-sign-0004.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "D269214C-8717-486F-B1CF-65A0775CB5A3", 3 | "title": "Sign1 - RSASSA-PSS w/ SHA-256 (sign)", 4 | "description": "Sign with one signer using RSASSA-PSS w/ SHA-256", 5 | "key": { 6 | "kty": "RSA", 7 | "d": "Hdi480gmfoi4Q7T9K17bb1drwPTJKS2Tyg1LWhRD7Tnyl6JTpu-wGkz043N11sASTOVBPmnE8Aq80w6hj2VfFZEP19CyFmzkgsUTK43vLYUvoxc9h7sS89_9GMseRPtr5wpPQHlAIBBbkzXAnZ2-isFlY-qhNZzLloW60mYH96VEmP8hboWHU-trra_9VNxDYdPEwNS0B_jBdoWrrEiJoyoWFT1q3oYyZhuytc0LqssOYnCRgfk1QGc12lcL2W9weMsJtoJxperoXs4kije9jGbSzl1DVK1pN7kZYulgAod0MnpRQCaXU3h0KPWgQKMKsgWQBn41ij92zS7jFI9uwQ", 8 | "dp": "LF3_WCJqTsOn_wtWBZ8zOLIGx4vbT8y1evxk_80ZGfJA5IotsfH7aKbO1X0y88N0Gzj3ZoOBRSe_1u_0kVuA62Igg-np58G6CBo1O8ZpLg5NyhxyYjJnzUB_ON7bNJT5BmzvALTclF1Y1vTGJWacF6PNHuElHUWIWEc2z5HBUBE", 9 | "dq": "c-2lsp6ZQCMTl0jpnEo3fh0IE4c19YSFilEoJOv4tFRV7hQBvb-ONEDBjjOeYHm0yV2RU_xthggxFO5dzltCRhYQfTS16HWZgoCnM6SFh1foNfNFJrcJ3xR15PP57hVcf4mamEbSZab5aJIwKURPPPN0w_7LHYitRXgYBygjIaU", 10 | "e": "AQAB", 11 | "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w", 12 | "p": "9PtmTbqO2p7ZV56eSt68z5rfyeBE7Su_X5GRRSFM1DkW0LkFrN3tz0JbJ_e6I0uYhws1J-Z1OBRLzruCJyCYrtv01AfN0unNGA3rSyjZrqytKciqRyahMqDeyr-TXhC6mae6aUvBehlvG33ruqasNetwlQvMSXotzttVwLq371E", 13 | "q": "80dYhBv3ytoLZaqXj3_kQ2K6rBqBdPZV_vkNcbJed7A1yzFcwg5LrOd45p1Th9yhizEwbPjheitfYD9e6eR-xfMq9secsRbaiheW2SBbHpZmQFPmT0kK0kMYIL0eIjFvJUgrbZN1JH0uPsE7QvmxN-kxZGE-rceooovTEqy35S8", 14 | "qi": "gvj49DT8lKqN0EmQcLvy2HDm2bozBo1q_oXvpMqOU2JyHiXigJffBU4dIvPkGU9uLW0CvJXyeFKSPxPsOzrkQHad_n35ypDB5ScccRDDH-ucZKuviiJYvSVBLDOiFP8hzyjQWuNkTrIQEula5FPJkovV1RADekJuNqCHHgIYVmQ", 15 | "kid": "meriadoc.brandybuck@rsa.example" 16 | }, 17 | "alg": "PS256", 18 | "sign1::sign": { 19 | "payload": "546869732069732074686520636f6e74656e742e", 20 | "protectedHeaders": { 21 | "cborHex": "a1013824", 22 | "cborDiag": "{1: -37}" 23 | }, 24 | "unprotectedHeaders": { 25 | "cborHex": "a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65", 26 | "cborDiag": "{4: 'bilbo.baggins@hobbiton.example'}" 27 | }, 28 | "tbsHex": { 29 | "cborHex": "846a5369676e61747572653144a10138244054546869732069732074686520636f6e74656e742e", 30 | "cborDiag": "[\"Signature1\", h'A1013824', h'', h'546869732069732074686520636F6E74656E742E']" 31 | }, 32 | "detached": false, 33 | "expectedOutput": { 34 | "cborHex": "d28444a1013824a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e590100788ef717e37b7a127010f87d458be5788151a734aeaf1ef52a8f7ac40d05daf1f5dc575b29c0bf0e1d326cfcae1af5dcf62b6dc26eb18a35456fd7c3477774d2c664babc5db7618309dc2ba38392c3dba61f0d27974ea5d4df329060334b8bbd9d8f41cc090913cb2cd4470ae2c8560173793e703d2dda52a4e804ececd57db5ba30200b4d0939f89adc1f13f829e516625109684b429a088e0a2766564cfd2ee1a1acc4f20ad981e4bc27e427d754481ca93ee16a7677e24cdd31d03b44fe3260ee35bbb5b57c1264af6ca879f67b39b2680d920252e37b916e970baa4762c65cc4ea071b2ab65c5c992518597c7b7fbf9608564c5c2c3caae9449d46f8df32", 35 | "cborDiag": "18([h'A1013824', {4: 'bilbo.baggins@hobbiton.example'}, h'546869732069732074686520636F6E74656E742E', h'788EF717E37B7A127010F87D458BE5788151A734AEAF1EF52A8F7AC40D05DAF1F5DC575B29C0BF0E1D326CFCAE1AF5DCF62B6DC26EB18A35456FD7C3477774D2C664BABC5DB7618309DC2BA38392C3DBA61F0D27974EA5D4DF329060334B8BBD9D8F41CC090913CB2CD4470AE2C8560173793E703D2DDA52A4E804ECECD57DB5BA30200B4D0939F89ADC1F13F829E516625109684B429A088E0A2766564CFD2EE1A1ACC4F20AD981E4BC27E427D754481CA93EE16A7677E24CDD31D03B44FE3260EE35BBB5B57C1264AF6CA879F67B39B2680D920252E37B916E970BAA4762C65CC4EA071B2AB65C5C992518597C7B7FBF9608564C5C2C3CAAE9449D46F8DF32'])" 36 | }, 37 | "fixedOutputLength": 65 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /testdata/sign1-sign-0005.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "9701444D-EC51-4ED6-859D-D5E2D4C441A8", 3 | "title": "Sign1 - RSASSA-PSS w/ SHA-384 (sign)", 4 | "description": "Sign with one signer using RSASSA-PSS w/ SHA-384", 5 | "key": { 6 | "kty": "RSA", 7 | "d": "Hdi480gmfoi4Q7T9K17bb1drwPTJKS2Tyg1LWhRD7Tnyl6JTpu-wGkz043N11sASTOVBPmnE8Aq80w6hj2VfFZEP19CyFmzkgsUTK43vLYUvoxc9h7sS89_9GMseRPtr5wpPQHlAIBBbkzXAnZ2-isFlY-qhNZzLloW60mYH96VEmP8hboWHU-trra_9VNxDYdPEwNS0B_jBdoWrrEiJoyoWFT1q3oYyZhuytc0LqssOYnCRgfk1QGc12lcL2W9weMsJtoJxperoXs4kije9jGbSzl1DVK1pN7kZYulgAod0MnpRQCaXU3h0KPWgQKMKsgWQBn41ij92zS7jFI9uwQ", 8 | "dp": "LF3_WCJqTsOn_wtWBZ8zOLIGx4vbT8y1evxk_80ZGfJA5IotsfH7aKbO1X0y88N0Gzj3ZoOBRSe_1u_0kVuA62Igg-np58G6CBo1O8ZpLg5NyhxyYjJnzUB_ON7bNJT5BmzvALTclF1Y1vTGJWacF6PNHuElHUWIWEc2z5HBUBE", 9 | "dq": "c-2lsp6ZQCMTl0jpnEo3fh0IE4c19YSFilEoJOv4tFRV7hQBvb-ONEDBjjOeYHm0yV2RU_xthggxFO5dzltCRhYQfTS16HWZgoCnM6SFh1foNfNFJrcJ3xR15PP57hVcf4mamEbSZab5aJIwKURPPPN0w_7LHYitRXgYBygjIaU", 10 | "e": "AQAB", 11 | "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w", 12 | "p": "9PtmTbqO2p7ZV56eSt68z5rfyeBE7Su_X5GRRSFM1DkW0LkFrN3tz0JbJ_e6I0uYhws1J-Z1OBRLzruCJyCYrtv01AfN0unNGA3rSyjZrqytKciqRyahMqDeyr-TXhC6mae6aUvBehlvG33ruqasNetwlQvMSXotzttVwLq371E", 13 | "q": "80dYhBv3ytoLZaqXj3_kQ2K6rBqBdPZV_vkNcbJed7A1yzFcwg5LrOd45p1Th9yhizEwbPjheitfYD9e6eR-xfMq9secsRbaiheW2SBbHpZmQFPmT0kK0kMYIL0eIjFvJUgrbZN1JH0uPsE7QvmxN-kxZGE-rceooovTEqy35S8", 14 | "qi": "gvj49DT8lKqN0EmQcLvy2HDm2bozBo1q_oXvpMqOU2JyHiXigJffBU4dIvPkGU9uLW0CvJXyeFKSPxPsOzrkQHad_n35ypDB5ScccRDDH-ucZKuviiJYvSVBLDOiFP8hzyjQWuNkTrIQEula5FPJkovV1RADekJuNqCHHgIYVmQ", 15 | "kid": "meriadoc.brandybuck@rsa.example" 16 | }, 17 | "alg": "PS384", 18 | "sign1::sign": { 19 | "payload": "546869732069732074686520636f6e74656e742e", 20 | "protectedHeaders": { 21 | "cborHex": "a1013825", 22 | "cborDiag": "{1: -38}" 23 | }, 24 | "unprotectedHeaders": { 25 | "cborHex": "a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65", 26 | "cborDiag": "{4: 'bilbo.baggins@hobbiton.example'}" 27 | }, 28 | "tbsHex": { 29 | "cborHex": "846a5369676e61747572653144a10138254054546869732069732074686520636f6e74656e742e", 30 | "cborDiag": "[\"Signature1\", h'A1013825', h'', h'546869732069732074686520636F6E74656E742E']" 31 | }, 32 | "detached": false, 33 | "expectedOutput": { 34 | "cborHex": "d28444a1013825a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e5901000c4b39bc5dfa27903ccfecb750f13d761cfe213a64eefd63d226281388727284622838431d9d312022f2a2e76270f60e7c3200df2a220f1a4abbc5b130f4aa93066d58ff5326bb4a62328f4b739b105e9ba3ff76b6397024582c10e948a6907154c3f7b8533c845f569c9d7ec0506194e6ae3d792da162fb396d6417510c0dd27f9519161d152ddb9444c20f8aff3b44816f05d7ab44cc00dfdeda4941a7ef4fe56007c1139f17f1b86cb3f00bfce015aa5033ecc92f1b4ca9077aea11007dd0ab309245f549452153a94ce2e2fb3e389023a03d07d31c772126f0817bb7248d43cec2347b377827242b4d41b494798b79c0a5f677b69a90d00d0854c812deb0", 35 | "cborDiag": "18([h'A1013825', {4: h'62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C65'}, h'546869732069732074686520636F6E74656E742E', h'0C4B39BC5DFA27903CCFECB750F13D761CFE213A64EEFD63D226281388727284622838431D9D312022F2A2E76270F60E7C3200DF2A220F1A4ABBC5B130F4AA93066D58FF5326BB4A62328F4B739B105E9BA3FF76B6397024582C10E948A6907154C3F7B8533C845F569C9D7EC0506194E6AE3D792DA162FB396D6417510C0DD27F9519161D152DDB9444C20F8AFF3B44816F05D7AB44CC00DFDEDA4941A7EF4FE56007C1139F17F1B86CB3F00BFCE015AA5033ECC92F1B4CA9077AEA11007DD0AB309245F549452153A94CE2E2FB3E389023A03D07D31C772126F0817BB7248D43CEC2347B377827242B4D41B494798B79C0A5F677B69A90D00D0854C812DEB0'])" 36 | }, 37 | "fixedOutputLength": 65 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /testdata/sign1-sign-0006.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "D669DEF6-6890-4C18-A529-1F3432127D18", 3 | "title": "Sign1 - RSASSA-PSS w/ SHA-512 (sign)", 4 | "description": "Sign with one signer using RSASSA-PSS w/ SHA-512", 5 | "key": { 6 | "kty": "RSA", 7 | "d": "Hdi480gmfoi4Q7T9K17bb1drwPTJKS2Tyg1LWhRD7Tnyl6JTpu-wGkz043N11sASTOVBPmnE8Aq80w6hj2VfFZEP19CyFmzkgsUTK43vLYUvoxc9h7sS89_9GMseRPtr5wpPQHlAIBBbkzXAnZ2-isFlY-qhNZzLloW60mYH96VEmP8hboWHU-trra_9VNxDYdPEwNS0B_jBdoWrrEiJoyoWFT1q3oYyZhuytc0LqssOYnCRgfk1QGc12lcL2W9weMsJtoJxperoXs4kije9jGbSzl1DVK1pN7kZYulgAod0MnpRQCaXU3h0KPWgQKMKsgWQBn41ij92zS7jFI9uwQ", 8 | "dp": "LF3_WCJqTsOn_wtWBZ8zOLIGx4vbT8y1evxk_80ZGfJA5IotsfH7aKbO1X0y88N0Gzj3ZoOBRSe_1u_0kVuA62Igg-np58G6CBo1O8ZpLg5NyhxyYjJnzUB_ON7bNJT5BmzvALTclF1Y1vTGJWacF6PNHuElHUWIWEc2z5HBUBE", 9 | "dq": "c-2lsp6ZQCMTl0jpnEo3fh0IE4c19YSFilEoJOv4tFRV7hQBvb-ONEDBjjOeYHm0yV2RU_xthggxFO5dzltCRhYQfTS16HWZgoCnM6SFh1foNfNFJrcJ3xR15PP57hVcf4mamEbSZab5aJIwKURPPPN0w_7LHYitRXgYBygjIaU", 10 | "e": "AQAB", 11 | "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w", 12 | "p": "9PtmTbqO2p7ZV56eSt68z5rfyeBE7Su_X5GRRSFM1DkW0LkFrN3tz0JbJ_e6I0uYhws1J-Z1OBRLzruCJyCYrtv01AfN0unNGA3rSyjZrqytKciqRyahMqDeyr-TXhC6mae6aUvBehlvG33ruqasNetwlQvMSXotzttVwLq371E", 13 | "q": "80dYhBv3ytoLZaqXj3_kQ2K6rBqBdPZV_vkNcbJed7A1yzFcwg5LrOd45p1Th9yhizEwbPjheitfYD9e6eR-xfMq9secsRbaiheW2SBbHpZmQFPmT0kK0kMYIL0eIjFvJUgrbZN1JH0uPsE7QvmxN-kxZGE-rceooovTEqy35S8", 14 | "qi": "gvj49DT8lKqN0EmQcLvy2HDm2bozBo1q_oXvpMqOU2JyHiXigJffBU4dIvPkGU9uLW0CvJXyeFKSPxPsOzrkQHad_n35ypDB5ScccRDDH-ucZKuviiJYvSVBLDOiFP8hzyjQWuNkTrIQEula5FPJkovV1RADekJuNqCHHgIYVmQ", 15 | "kid": "meriadoc.brandybuck@rsa.example" 16 | }, 17 | "alg": "PS512", 18 | "sign1::sign": { 19 | "payload": "546869732069732074686520636f6e74656e742e", 20 | "protectedHeaders": { 21 | "cborHex": "a1013826", 22 | "cborDiag": "{1: -39}" 23 | }, 24 | "unprotectedHeaders": { 25 | "cborHex": "a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65", 26 | "cborDiag": "{4: 'bilbo.baggins@hobbiton.example'}" 27 | }, 28 | "tbsHex": { 29 | "cborHex": "846a5369676e61747572653144a10138264054546869732069732074686520636f6e74656e742e", 30 | "cborDiag": "[\"Signature1\", h'A1013826', h'', h'546869732069732074686520636F6E74656E742E']" 31 | }, 32 | "detached": false, 33 | "expectedOutput": { 34 | "cborHex": "d28444a1013826a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e590100d154da53f040ca145f6db8ee0bbcf0644df969313ed5f9cfc927a594d14b59001bfa7b0a4676f96ed0906cf4d4b92f3b622d6dc1946686181f6bd43a5f74f800cb9fbba5a431d19ac23a8a6c0efe89756ce6b19a20fabf1df8a0ee55164b0ea449e25f38190c470953e878dd531e8c5d1f5a7d7ec9fc79bb49b1eff605aae6254a4dc4757909730ead4dccd5cab49595270cfb31f6263f068a377144e1663504531cd908ce7810df0b53fd2e12f3bdfbb294680c7737b36e2de1a2b2f212996469306c74955cd7d08379a2655572d8845d542b30f7b4dda2e9438c96fe392f391ba5dd1f2a2487e993daab173da0c33c2dcdf5b173b32cebde71ca0e3a616e23", 35 | "cborDiag": "18([h'A1013826', {4: h'62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C65'}, h'546869732069732074686520636F6E74656E742E', h'D154DA53F040CA145F6DB8EE0BBCF0644DF969313ED5F9CFC927A594D14B59001BFA7B0A4676F96ED0906CF4D4B92F3B622D6DC1946686181F6BD43A5F74F800CB9FBBA5A431D19AC23A8A6C0EFE89756CE6B19A20FABF1DF8A0EE55164B0EA449E25F38190C470953E878DD531E8C5D1F5A7D7EC9FC79BB49B1EFF605AAE6254A4DC4757909730EAD4DCCD5CAB49595270CFB31F6263F068A377144E1663504531CD908CE7810DF0B53FD2E12F3BDFBB294680C7737B36E2DE1A2B2F212996469306C74955CD7D08379A2655572D8845D542B30F7B4DDA2E9438C96FE392F391BA5DD1F2A2487E993DAAB173DA0C33C2DCDF5B173B32CEBDE71CA0E3A616E23'])" 36 | }, 37 | "fixedOutputLength": 65 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /testdata/sign1-verify-0000.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "66584A57-390B-4A52-B7B6-B7CA4FC4204F", 3 | "title": "Sign1 w/ external input - ECDSA w/ SHA-256 (verify)", 4 | "description": "Verify signature with one signer using ECDSA w/ SHA-256 supplying external input", 5 | "key": { 6 | "kty": "EC", 7 | "crv": "P-256", 8 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 9 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4" 10 | }, 11 | "alg": "ES256", 12 | "sign1::verify": { 13 | "taggedCOSESign1": { 14 | "cborHex": "d28443a10126a10442313154546869732069732074686520636f6e74656e742e58403a7487d9a528cb61dd8e99bd652c12577fc47d70ee5af2e703c420584f060fc7a8d61e4a35862b2b531a8447030ab966aeed8dd45ebc507c761431e349995770", 15 | "cborDiag": "18([h'A10126', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'3A7487D9A528CB61DD8E99BD652C12577FC47D70EE5AF2E703C420584F060FC7A8D61E4A35862B2B531A8447030AB966AEED8DD45EBC507C761431E349995770'])" 16 | }, 17 | "external": "11aa22bb33cc44dd55006699", 18 | "shouldVerify": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/sign1-verify-0001.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "2AF74107-34AB-4DD5-BC3C-E83895CAE1A4", 3 | "title": "Sign1 - ECDSA w/ SHA-256 (verify)", 4 | "description": "Verify signature with one signer using ECDSA w/ SHA-256", 5 | "key": { 6 | "kty": "EC", 7 | "crv": "P-256", 8 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 9 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4" 10 | }, 11 | "alg": "ES256", 12 | "sign1::verify": { 13 | "taggedCOSESign1": { 14 | "cborHex": "d28445a201260300a10442313154546869732069732074686520636f6e74656e742e58402ad3b9dcc1e13d04f357e11cc8acd825196620e62f0d8deca72672508b829d90e07a3f23be6aa36fd6ebd31e2ed08d1760bffd981f991bfc94a45199a54875c4", 15 | "cborDiag": "18([h'A201260300', {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'2AD3B9DCC1E13D04F357E11CC8ACD825196620E62F0D8DECA72672508B829D90E07A3F23BE6AA36FD6EBD31E2ED08D1760BFFD981F991BFC94A45199A54875C4'])" 16 | }, 17 | "shouldVerify": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/sign1-verify-0002.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "C5763BDB-5A23-4E9E-9AA2-463A8B107033", 3 | "title": "Sign1 - ECDSA w/ SHA-384 (verify)", 4 | "description": "Verify signature with one signer using ECDSA w/ SHA-384", 5 | "key": { 6 | "kty": "EC", 7 | "kid": "P384", 8 | "crv": "P-384", 9 | "x": "kTJyP2KSsBBhnb4kjWmMF7WHVsY55xUPgb7k64rDcjatChoZ1nvjKmYmPh5STRKc", 10 | "y": "mM0weMVU2DKsYDxDJkEP9hZiRZtB8fPfXbzINZj_fF7YQRynNWedHEyzAJOX2e8s" 11 | }, 12 | "alg": "ES384", 13 | "sign1::verify": { 14 | "taggedCOSESign1": { 15 | "cborHex": "d28444a1013822a104445033383454546869732069732074686520636f6e74656e742e5860aa46c1ab71cd3c1e68ed62c27653797cb72cba3a856fd5e2f38794eee0d666e88139ec51fb62466f4865ca56df493905911e329e829c1887f6259681360a8e7f7d3fd080dcb0720066f13e1621656700c99d6e3771ac2549fde998ee9b1e2cad", 16 | "cborDiag": "18([h'A1013822', {4: 'P384'}, h'546869732069732074686520636F6E74656E742E', h'AA46C1AB71CD3C1E68ED62C27653797CB72CBA3A856FD5E2F38794EEE0D666E88139EC51FB62466F4865CA56DF493905911E329E829C1887F6259681360A8E7F7D3FD080DCB0720066F13E1621656700C99D6E3771AC2549FDE998EE9B1E2CAD'])" 17 | }, 18 | "shouldVerify": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /testdata/sign1-verify-0003.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "5F6E65B5-1E2F-4242-87CE-C76E26E927D8", 3 | "title": "Sign1 - ECDSA w/ SHA-512 (verify)", 4 | "description": "Verify signature with one signer using ECDSA w/ SHA-512", 5 | "key": { 6 | "kty": "EC", 7 | "kid": "bilbo.baggins@hobbiton.example", 8 | "use": "sig", 9 | "crv": "P-521", 10 | "x": "AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt", 11 | "y": "AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1" 12 | }, 13 | "alg": "ES512", 14 | "sign1::verify": { 15 | "taggedCOSESign1": { 16 | "cborHex": "d28444a1013823a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e58840128bbda237a1b55568da74cefe02cf2d2a6216f80ac757bea8effc056d2f634f6e257077b0dabe9d4b3689eb8228e20f60bc74ff84ae3a38ee9a69e158cbf80f93a017acf5877e5083548a45143b602ccd776c5eb39537a2e68dc8c47ff62e10fc42f045b781e4313fbf421903785c3dfeb181c3a93b46a67a9b0e82947ee83f7b44cf0", 17 | "cborDiag": "18([h'A1013823', {4: 'bilbo.baggins@hobbiton.example'}, h'546869732069732074686520636F6E74656E742E', h'0128BBDA237A1B55568DA74CEFE02CF2D2A6216F80AC757BEA8EFFC056D2F634F6E257077B0DABE9D4B3689EB8228E20F60BC74FF84AE3A38EE9A69E158CBF80F93A017ACF5877E5083548A45143B602CCD776C5EB39537A2E68DC8C47FF62E10FC42F045B781E4313FBF421903785C3DFEB181C3A93B46A67A9B0E82947EE83F7B44CF0'])" 18 | }, 19 | "shouldVerify": true 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /testdata/sign1-verify-0004.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "842E5FF9-74A4-45D4-B00F-F091925CF4F3", 3 | "title": "Sign1 - RSASSA-PSS w/ SHA-256 (verify)", 4 | "description": "Verify signature with one signer using RSASSA-PSS w/ SHA-256", 5 | "key": { 6 | "kty": "RSA", 7 | "e": "AQAB", 8 | "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w", 9 | "kid": "meriadoc.brandybuck@rsa.example" 10 | }, 11 | "alg": "PS256", 12 | "sign1::verify": { 13 | "taggedCOSESign1": { 14 | "cborHex": "d28444a1013824a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e590100788ef717e37b7a127010f87d458be5788151a734aeaf1ef52a8f7ac40d05daf1f5dc575b29c0bf0e1d326cfcae1af5dcf62b6dc26eb18a35456fd7c3477774d2c664babc5db7618309dc2ba38392c3dba61f0d27974ea5d4df329060334b8bbd9d8f41cc090913cb2cd4470ae2c8560173793e703d2dda52a4e804ececd57db5ba30200b4d0939f89adc1f13f829e516625109684b429a088e0a2766564cfd2ee1a1acc4f20ad981e4bc27e427d754481ca93ee16a7677e24cdd31d03b44fe3260ee35bbb5b57c1264af6ca879f67b39b2680d920252e37b916e970baa4762c65cc4ea071b2ab65c5c992518597c7b7fbf9608564c5c2c3caae9449d46f8df32", 15 | "cborDiag": "18([h'A1013824', {4: 'bilbo.baggins@hobbiton.example'}, h'546869732069732074686520636F6E74656E742E', h'788EF717E37B7A127010F87D458BE5788151A734AEAF1EF52A8F7AC40D05DAF1F5DC575B29C0BF0E1D326CFCAE1AF5DCF62B6DC26EB18A35456FD7C3477774D2C664BABC5DB7618309DC2BA38392C3DBA61F0D27974EA5D4DF329060334B8BBD9D8F41CC090913CB2CD4470AE2C8560173793E703D2DDA52A4E804ECECD57DB5BA30200B4D0939F89ADC1F13F829E516625109684B429A088E0A2766564CFD2EE1A1ACC4F20AD981E4BC27E427D754481CA93EE16A7677E24CDD31D03B44FE3260EE35BBB5B57C1264AF6CA879F67B39B2680D920252E37B916E970BAA4762C65CC4EA071B2AB65C5C992518597C7B7FBF9608564C5C2C3CAAE9449D46F8DF32'])" 16 | }, 17 | "shouldVerify": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/sign1-verify-0005.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "98C2AB20-415D-4190-96DD-868EF3998AA1", 3 | "title": "Sign1 - RSASSA-PSS w/ SHA-384 (verify)", 4 | "description": "Verify signature with one signer using RSASSA-PSS w/ SHA-384", 5 | "key": { 6 | "kty": "RSA", 7 | "e": "AQAB", 8 | "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w", 9 | "kid": "meriadoc.brandybuck@rsa.example" 10 | }, 11 | "alg": "PS384", 12 | "sign1::verify": { 13 | "taggedCOSESign1": { 14 | "cborHex": "d28444a1013825a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e5901000c4b39bc5dfa27903ccfecb750f13d761cfe213a64eefd63d226281388727284622838431d9d312022f2a2e76270f60e7c3200df2a220f1a4abbc5b130f4aa93066d58ff5326bb4a62328f4b739b105e9ba3ff76b6397024582c10e948a6907154c3f7b8533c845f569c9d7ec0506194e6ae3d792da162fb396d6417510c0dd27f9519161d152ddb9444c20f8aff3b44816f05d7ab44cc00dfdeda4941a7ef4fe56007c1139f17f1b86cb3f00bfce015aa5033ecc92f1b4ca9077aea11007dd0ab309245f549452153a94ce2e2fb3e389023a03d07d31c772126f0817bb7248d43cec2347b377827242b4d41b494798b79c0a5f677b69a90d00d0854c812deb0", 15 | "cborDiag": "18([h'A1013825', {4: h'62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C65'}, h'546869732069732074686520636F6E74656E742E', h'0C4B39BC5DFA27903CCFECB750F13D761CFE213A64EEFD63D226281388727284622838431D9D312022F2A2E76270F60E7C3200DF2A220F1A4ABBC5B130F4AA93066D58FF5326BB4A62328F4B739B105E9BA3FF76B6397024582C10E948A6907154C3F7B8533C845F569C9D7EC0506194E6AE3D792DA162FB396D6417510C0DD27F9519161D152DDB9444C20F8AFF3B44816F05D7AB44CC00DFDEDA4941A7EF4FE56007C1139F17F1B86CB3F00BFCE015AA5033ECC92F1B4CA9077AEA11007DD0AB309245F549452153A94CE2E2FB3E389023A03D07D31C772126F0817BB7248D43CEC2347B377827242B4D41B494798B79C0A5F677B69A90D00D0854C812DEB0'])" 16 | }, 17 | "shouldVerify": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/sign1-verify-0006.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "A2D89260-B966-4FE4-8931-959BAD58126C", 3 | "title": "Sign1 - RSASSA-PSS w/ SHA-512 (verify)", 4 | "description": "Verify signature with one signer using RSASSA-PSS w/ SHA-512", 5 | "key": { 6 | "kty": "RSA", 7 | "e": "AQAB", 8 | "n": "6M7oif8zc9GmjmOZZD5dLhhY-xSZvsIOP4fAXJ0osOYYi5NjEAGtTpTJG35dDvzslxzEVOrVtA-_lRkvp3xT8KXqdGhiNH9T5rK6nGZ1eLqghM9zkVrwrC2cc4t1TvdQFiB9IRrFHPXP98C9vcWBJH_5VjpKeTSgPu2TqAgdqrto2QKI4n_BfKIu2NSwKmShlogTR0vdEL16y251Es6jWRygxSLg3_KU2j8Wu48GArYOBA7tIIDH3qtvYKw9NurJNxG7Iql4IXTR6b5zRb3Ic8bKnMe6JNzjWShf0Vv46Zqf-oRFADyi6rPEMyQaRnXIqRHrKw3kUcXmXMphT79k3w", 9 | "kid": "meriadoc.brandybuck@rsa.example" 10 | }, 11 | "alg": "PS512", 12 | "sign1::verify": { 13 | "taggedCOSESign1": { 14 | "cborHex": "d28444a1013826a104581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c6554546869732069732074686520636f6e74656e742e590100d154da53f040ca145f6db8ee0bbcf0644df969313ed5f9cfc927a594d14b59001bfa7b0a4676f96ed0906cf4d4b92f3b622d6dc1946686181f6bd43a5f74f800cb9fbba5a431d19ac23a8a6c0efe89756ce6b19a20fabf1df8a0ee55164b0ea449e25f38190c470953e878dd531e8c5d1f5a7d7ec9fc79bb49b1eff605aae6254a4dc4757909730ead4dccd5cab49595270cfb31f6263f068a377144e1663504531cd908ce7810df0b53fd2e12f3bdfbb294680c7737b36e2de1a2b2f212996469306c74955cd7d08379a2655572d8845d542b30f7b4dda2e9438c96fe392f391ba5dd1f2a2487e993daab173da0c33c2dcdf5b173b32cebde71ca0e3a616e23", 15 | "cborDiag": "18([h'A1013826', {4: h'62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C65'}, h'546869732069732074686520636F6E74656E742E', h'D154DA53F040CA145F6DB8EE0BBCF0644DF969313ED5F9CFC927A594D14B59001BFA7B0A4676F96ED0906CF4D4B92F3B622D6DC1946686181F6BD43A5F74F800CB9FBBA5A431D19AC23A8A6C0EFE89756CE6B19A20FABF1DF8A0EE55164B0EA449E25F38190C470953E878DD531E8C5D1F5A7D7EC9FC79BB49B1EFF605AAE6254A4DC4757909730EAD4DCCD5CAB49595270CFB31F6263F068A377144E1663504531CD908CE7810DF0B53FD2E12F3BDFBB294680C7737B36E2DE1A2B2F212996469306C74955CD7D08379A2655572D8845D542B30F7B4DDA2E9438C96FE392F391BA5DD1F2A2487E993DAAB173DA0C33C2DCDF5B173B32CEBDE71CA0E3A616E23'])" 16 | }, 17 | "shouldVerify": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/sign1-verify-negative-0000.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "5921EA1A-A39A-462E-B6CE-2680000A79CD", 3 | "title": "Protected header outer type is not bstr ", 4 | "description": "The protected header is not serialised as bstr, instead it is kept as map, which the Sign1 parser should reject", 5 | "key": { 6 | "kty": "EC", 7 | "crv": "P-256", 8 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 9 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4" 10 | }, 11 | "alg": "ES256", 12 | "sign1::verify": { 13 | "taggedCOSESign1": { 14 | "cborHex": "d284a10126a10442313154546869732069732074686520636f6e74656e742e5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 15 | "cborDiag": "18([{1: -7}, {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'])" 16 | }, 17 | "shouldVerify": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/sign1-verify-negative-0001.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "10F06D49-9AD3-427C-9496-B7EAD5EE721C", 3 | "title": "Protected header inner type is not map", 4 | "description": "The protected header is serialised as bstr but the embedded type is not a map, which the Sign1 parser should reject", 5 | "key": { 6 | "kty": "EC", 7 | "crv": "P-256", 8 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 9 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4" 10 | }, 11 | "alg": "ES256", 12 | "sign1::verify": { 13 | "taggedCOSESign1": { 14 | "cborHex": "d28443820126a10442313154546869732069732074686520636f6e74656e742e5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 15 | "cborDiag": "18([h'820126', {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'])" 16 | }, 17 | "shouldVerify": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/sign1-verify-negative-0002.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "A658B110-BEA2-4C6D-A9AA-C10BE3643389", 3 | "title": "Protected header map contains duplicate entries", 4 | "description": "The protected header map contains a duplicate label (alg), which the Sign1 parser should reject", 5 | "key": { 6 | "kty": "EC", 7 | "crv": "P-256", 8 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 9 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4" 10 | }, 11 | "alg": "ES256", 12 | "sign1::verify": { 13 | "taggedCOSESign1": { 14 | "cborHex": "d28445a201260126a10442313154546869732069732074686520636f6e74656e742e5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 15 | "cborDiag": "18([h'A201260126', {4: '11'}, h'546869732069732074686520636F6E74656E742E', h'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'])" 16 | }, 17 | "shouldVerify": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /testdata/sign1-verify-negative-0003.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "13A8356D-5CCF-423E-8EB7-29F8A9EC64D5", 3 | "title": "Unprotected header map contains duplicate entries", 4 | "description": "The unprotected header map contains a duplicate label (kid), which the Sign1 parser should reject", 5 | "key": { 6 | "kty": "EC", 7 | "crv": "P-256", 8 | "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", 9 | "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4" 10 | }, 11 | "alg": "ES256", 12 | "sign1::verify": { 13 | "taggedCOSESign1": { 14 | "cborHex": "d28443a10126a2044231310442313254546869732069732074686520636f6e74656e742e5840ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 15 | "cborDiag": "18([h'A10126', {4: '11', 4: '12'}, h'546869732069732074686520636F6E74656E742E', h'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'])" 16 | }, 17 | "shouldVerify": false 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /verifier.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/ed25519" 7 | "crypto/rsa" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // Verifier is an interface for public keys to verify COSE signatures. 13 | type Verifier interface { 14 | // Algorithm returns the signing algorithm associated with the public key. 15 | Algorithm() Algorithm 16 | 17 | // Verify verifies message content with the public key, returning nil for 18 | // success. 19 | // Otherwise, it returns [ErrVerification]. 20 | // 21 | // Reference: https://datatracker.ietf.org/doc/html/rfc8152#section-8 22 | Verify(content, signature []byte) error 23 | } 24 | 25 | // DigestVerifier is an interface for public keys to verify digested COSE 26 | // signatures. 27 | type DigestVerifier interface { 28 | // Algorithm returns the signing algorithm associated with the public key. 29 | Algorithm() Algorithm 30 | 31 | // VerifyDigest verifies message digest with the public key, returning nil 32 | // for success. 33 | // Otherwise, it returns [ErrVerification]. 34 | VerifyDigest(digest, signature []byte) error 35 | } 36 | 37 | // NewVerifier returns a verifier with a given public key. 38 | // Only golang built-in crypto public keys of type [*rsa.PublicKey], 39 | // [*ecdsa.PublicKey], and [ed25519.PublicKey] are accepted. 40 | // When [*ecdsa.PublicKey] is specified, its curve must be supported by 41 | // crypto/ecdh. 42 | // 43 | // The returned signer for rsa and ecdsa keys also implements 44 | // [cose.DigestSigner]. 45 | func NewVerifier(alg Algorithm, key crypto.PublicKey) (Verifier, error) { 46 | var errReason string 47 | switch alg { 48 | case AlgorithmPS256, AlgorithmPS384, AlgorithmPS512: 49 | vk, ok := key.(*rsa.PublicKey) 50 | if !ok { 51 | return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) 52 | } 53 | // RFC 8230 section 6.1 requires RSA keys having a minimum size of 2048 54 | // bits. 55 | // Reference: https://www.rfc-editor.org/rfc/rfc8230.html#section-6.1 56 | if vk.N.BitLen() < 2048 { 57 | return nil, errors.New("RSA key must be at least 2048 bits long") 58 | } 59 | return &rsaVerifier{ 60 | alg: alg, 61 | key: vk, 62 | }, nil 63 | case AlgorithmES256, AlgorithmES384, AlgorithmES512: 64 | vk, ok := key.(*ecdsa.PublicKey) 65 | if !ok { 66 | return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) 67 | } 68 | if _, err := vk.ECDH(); err != nil { 69 | if err.Error() == "ecdsa: invalid public key" { 70 | return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) 71 | } 72 | return nil, fmt.Errorf("%v: %w: %v", alg, ErrInvalidPubKey, err) 73 | } 74 | return &ecdsaVerifier{ 75 | alg: alg, 76 | key: vk, 77 | }, nil 78 | case AlgorithmEdDSA: 79 | vk, ok := key.(ed25519.PublicKey) 80 | if !ok { 81 | return nil, fmt.Errorf("%v: %w", alg, ErrInvalidPubKey) 82 | } 83 | return &ed25519Verifier{ 84 | key: vk, 85 | }, nil 86 | case AlgorithmReserved: 87 | errReason = "can't be implemented" 88 | case AlgorithmRS256, AlgorithmRS384, AlgorithmRS512: 89 | errReason = "no built-in implementation available" 90 | default: 91 | errReason = "unknown algorithm" 92 | } 93 | return nil, fmt.Errorf("can't create new Verifier for %s: %s: %w", alg, errReason, ErrAlgorithmNotSupported) 94 | } 95 | -------------------------------------------------------------------------------- /verifier_test.go: -------------------------------------------------------------------------------- 1 | package cose 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "encoding/base64" 10 | "math/big" 11 | "reflect" 12 | "testing" 13 | ) 14 | 15 | func mustBase64ToBigInt(s string) *big.Int { 16 | val, err := base64.RawURLEncoding.DecodeString(s) 17 | if err != nil { 18 | panic(err) 19 | } 20 | return new(big.Int).SetBytes(val) 21 | } 22 | 23 | func generateBogusECKey() *ecdsa.PublicKey { 24 | return &ecdsa.PublicKey{ 25 | Curve: elliptic.P256(), 26 | // x-coord is not on curve p-256 27 | X: mustBase64ToBigInt("MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqx7D4"), 28 | Y: mustBase64ToBigInt("4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"), 29 | } 30 | } 31 | 32 | func TestNewVerifier(t *testing.T) { 33 | // generate ecdsa key 34 | ecdsaKey := generateTestECDSAKey(t).Public().(*ecdsa.PublicKey) 35 | 36 | // generate ed25519 key 37 | ed25519Key, _ := generateTestEd25519Key(t) 38 | 39 | // generate rsa keys 40 | rsaKey := generateTestRSAKey(t).Public().(*rsa.PublicKey) 41 | var rsaKeyLowEntropy *rsa.PublicKey 42 | if key, err := rsa.GenerateKey(rand.Reader, 1024); err != nil { 43 | t.Fatalf("rsa.GenerateKey() error = %v", err) 44 | } else { 45 | rsaKeyLowEntropy = &key.PublicKey 46 | } 47 | 48 | // craft an EC public key with the x-coord not on curve 49 | ecdsaKeyPointNotOnCurve := generateBogusECKey() 50 | 51 | // craft an EC public key with a curve not supported by crypto/ecdh 52 | ecdsaKeyUnsupportedCurve := &ecdsa.PublicKey{ 53 | Curve: ecdsaKey.Curve.Params(), 54 | X: ecdsaKey.X, 55 | Y: ecdsaKey.Y, 56 | } 57 | 58 | // run tests 59 | tests := []struct { 60 | name string 61 | alg Algorithm 62 | key crypto.PublicKey 63 | want Verifier 64 | wantErr string 65 | }{ 66 | { 67 | name: "ecdsa key verifier", 68 | alg: AlgorithmES256, 69 | key: ecdsaKey, 70 | want: &ecdsaVerifier{ 71 | alg: AlgorithmES256, 72 | key: ecdsaKey, 73 | }, 74 | }, 75 | { 76 | name: "ecdsa invalid public key", 77 | alg: AlgorithmES256, 78 | key: rsaKey, 79 | wantErr: "ES256: invalid public key", 80 | }, 81 | { 82 | name: "ed25519 verifier", 83 | alg: AlgorithmEdDSA, 84 | key: ed25519Key, 85 | want: &ed25519Verifier{ 86 | key: ed25519Key, 87 | }, 88 | }, 89 | { 90 | name: "ed25519 invalid public key", 91 | alg: AlgorithmEdDSA, 92 | key: rsaKey, 93 | wantErr: "EdDSA: invalid public key", 94 | }, 95 | { 96 | name: "rsa verifier", 97 | alg: AlgorithmPS256, 98 | key: rsaKey, 99 | want: &rsaVerifier{ 100 | alg: AlgorithmPS256, 101 | key: rsaKey, 102 | }, 103 | }, 104 | { 105 | name: "rsa invalid public key", 106 | alg: AlgorithmPS256, 107 | key: ecdsaKey, 108 | wantErr: "PS256: invalid public key", 109 | }, 110 | { 111 | name: "rsa key under minimum entropy", 112 | alg: AlgorithmPS256, 113 | key: rsaKeyLowEntropy, 114 | wantErr: "RSA key must be at least 2048 bits long", 115 | }, 116 | { 117 | name: "unsupported rsa signing algorithm", 118 | alg: AlgorithmRS256, 119 | wantErr: "can't create new Verifier for RS256: no built-in implementation available: algorithm not supported", 120 | }, 121 | { 122 | name: "reserved algorithm", 123 | alg: AlgorithmReserved, 124 | wantErr: "can't create new Verifier for Reserved: can't be implemented: algorithm not supported", 125 | }, 126 | { 127 | name: "unassigned algorithm", 128 | alg: -1, 129 | wantErr: "can't create new Verifier for Algorithm(-1): unknown algorithm: algorithm not supported", 130 | }, 131 | { 132 | name: "bogus ecdsa public key (point not on curve)", 133 | alg: AlgorithmES256, 134 | key: ecdsaKeyPointNotOnCurve, 135 | wantErr: "ES256: invalid public key", 136 | }, 137 | { 138 | name: "ecdsa public key with unsupported curve", 139 | alg: AlgorithmES256, 140 | key: ecdsaKeyUnsupportedCurve, 141 | wantErr: "ES256: invalid public key: ecdsa: unsupported curve by crypto/ecdh", 142 | }, 143 | } 144 | for _, tt := range tests { 145 | t.Run(tt.name, func(t *testing.T) { 146 | got, err := NewVerifier(tt.alg, tt.key) 147 | if err != nil && (err.Error() != tt.wantErr) { 148 | t.Errorf("NewVerifier() error = %v, wantErr %v", err, tt.wantErr) 149 | return 150 | } else if err == nil && (tt.wantErr != "") { 151 | t.Errorf("NewVerifier() error = %v, wantErr %v", err, tt.wantErr) 152 | return 153 | } 154 | if !reflect.DeepEqual(got, tt.want) { 155 | t.Errorf("NewVerifier() = %v, want %v", got, tt.want) 156 | } 157 | }) 158 | } 159 | } 160 | --------------------------------------------------------------------------------