├── .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 | [](https://pkg.go.dev/github.com/veraison/go-cose)
4 | [](https://github.com/veraison/go-cose/actions?query=workflow%3Aci)
5 | [](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 |
--------------------------------------------------------------------------------