├── .gitignore
├── Makefile
├── .github
├── ISSUE_TEMPLATE
│ ├── 4_security_issue_disclosure.md
│ ├── 3_docs_wiki_or_website_issue.md
│ ├── 1_bug_report.md
│ └── 2_feature_request.md
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ ├── codeql.yml
│ └── ci-cover.yml
├── key
├── chacha20poly1305
│ ├── register.go
│ └── chacha20poly1305.go
├── cbor_test.go
├── ed25519
│ └── register.go
├── aesgcm
│ ├── register.go
│ └── aes_gcm.go
├── hmac
│ ├── register.go
│ └── hmac.go
├── random.go
├── aesmac
│ ├── register.go
│ └── aes_mac.go
├── key_ops.go
├── random_test.go
├── key_crv.go
├── key_ops_test.go
├── ecdsa
│ └── register.go
├── key_crv_test.go
├── interface_mac.go
├── aesccm
│ ├── register.go
│ └── aes_ccm.go
├── interface_encryption.go
├── key_alg.go
├── keyset.go
├── cbor.go
├── key_alg_test.go
├── hkdf
│ ├── hkdf.go
│ ├── example_test.go
│ ├── hkdf_aes.go
│ └── hkdf_test.go
├── interface_signing_test.go
├── bytestr_test.go
├── interface_signing.go
├── keyset_test.go
├── bytestr.go
├── registry.go
├── key.go
├── cosemap_test.go
└── registry_test.go
├── SECURITY.md
├── go.mod
├── iana
├── tag.go
├── curve.go
├── operation.go
├── claim.go
├── header.go
├── algorithm.go
└── key.go
├── LICENSE
├── cwt
├── claims.go
├── claims_map.go
├── claims_map_test.go
├── example_test.go
├── claims_test.go
├── validator.go
└── validator_test.go
├── go.sum
├── cose
├── tag_prefix.go
├── mac0_example_test.go
├── encrypt0_example_test.go
├── sign1_example_test.go
├── header.go
├── sign_example_test.go
├── encrypt_example_test.go
├── header_test.go
├── recipient.go
├── kdf_context.go
├── recipient_test.go
├── mac0.go
└── sign1.go
└── CODE_OF_CONDUCT.md
/.gitignore:
--------------------------------------------------------------------------------
1 | debug/
2 | coverage.out
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | go test -v -failfast -tags=test --race ./...
3 |
4 | .PHONY: test
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/4_security_issue_disclosure.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F513 Security issue disclosure"
3 | about: Report a security issue in fxamacker/cbor
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
17 |
--------------------------------------------------------------------------------
/key/chacha20poly1305/register.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package chacha20poly1305
5 |
6 | import (
7 | "github.com/ldclabs/cose/iana"
8 | "github.com/ldclabs/cose/key"
9 | )
10 |
11 | func init() {
12 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmChaCha20Poly1305, New)
13 | }
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/3_docs_wiki_or_website_issue.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F4DA Docs, wiki, or website issue"
3 | about: Report an issue regarding documentation, wiki, or website
4 | title: 'docs: '
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **What is the URL of the content?**
11 |
12 |
13 | **Please describe the problem.**
14 |
15 |
16 | **Screenshot (if applicable).**
17 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | Security fixes are provided for the latest released version of ldclabs/cose.
4 |
5 | If the security vulnerability is already known to the public, then you can open an issue as a bug report.
6 |
7 | To report security vulnerabilities not yet known to the public, please email txr1883@gmail.com and allow time for the problem to be resolved before reporting it to the public.
8 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
2 |
3 | version: 2
4 | updates:
5 | - package-ecosystem: "gomod"
6 | directory: "/" # Location of package manifests
7 | schedule:
8 | interval: "monthly"
9 |
10 | - package-ecosystem: "github-actions"
11 | directory: "/"
12 | schedule:
13 | interval: "monthly"
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/ldclabs/cose
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/fxamacker/cbor/v2 v2.7.0
7 | github.com/stretchr/testify v1.9.0
8 | golang.org/x/crypto v0.28.0
9 | )
10 |
11 | require (
12 | github.com/davecgh/go-spew v1.1.1 // indirect
13 | github.com/pmezard/go-difflib v1.0.0 // indirect
14 | github.com/x448/float16 v0.8.4 // indirect
15 | golang.org/x/sys v0.26.0 // indirect
16 | gopkg.in/yaml.v3 v3.0.1 // indirect
17 | )
18 |
--------------------------------------------------------------------------------
/key/cbor_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestCBOREdgeCase(t *testing.T) {
13 | assert := assert.New(t)
14 |
15 | data, err := MarshalCBOR(func() {})
16 | assert.Error(err)
17 | assert.Nil(data)
18 |
19 | assert.Panics(func() {
20 | MustMarshalCBOR(func() {})
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/key/ed25519/register.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package ed25519
5 |
6 | import (
7 | "github.com/ldclabs/cose/iana"
8 | "github.com/ldclabs/cose/key"
9 | )
10 |
11 | func init() {
12 | key.RegisterSigner(iana.KeyTypeOKP, iana.AlgorithmEdDSA, iana.EllipticCurveEd25519, NewSigner)
13 |
14 | key.RegisterVerifier(iana.KeyTypeOKP, iana.AlgorithmEdDSA, iana.EllipticCurveEd25519, NewVerifier)
15 | }
16 |
--------------------------------------------------------------------------------
/key/aesgcm/register.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package aesgcm
5 |
6 | import (
7 | "github.com/ldclabs/cose/iana"
8 | "github.com/ldclabs/cose/key"
9 | )
10 |
11 | func init() {
12 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmA128GCM, New)
13 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmA192GCM, New)
14 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmA256GCM, New)
15 | }
16 |
--------------------------------------------------------------------------------
/key/hmac/register.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package hmac
5 |
6 | import (
7 | "github.com/ldclabs/cose/iana"
8 | "github.com/ldclabs/cose/key"
9 | )
10 |
11 | func init() {
12 | key.RegisterMACer(iana.KeyTypeSymmetric, iana.AlgorithmHMAC_256_64, New)
13 | key.RegisterMACer(iana.KeyTypeSymmetric, iana.AlgorithmHMAC_256_256, New)
14 | key.RegisterMACer(iana.KeyTypeSymmetric, iana.AlgorithmHMAC_384_384, New)
15 | key.RegisterMACer(iana.KeyTypeSymmetric, iana.AlgorithmHMAC_512_512, New)
16 | }
17 |
--------------------------------------------------------------------------------
/key/random.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "crypto/rand"
8 | "encoding/binary"
9 | )
10 |
11 | // GetRandomBytes randomly generates n bytes.
12 | func GetRandomBytes(n uint16) []byte {
13 | buf := make([]byte, n)
14 | rand.Read(buf) // err should never happen
15 | return buf
16 | }
17 |
18 | // GetRandomUint32 randomly generates an unsigned 32-bit integer.
19 | func GetRandomUint32() uint32 {
20 | b := GetRandomBytes(4)
21 | return binary.BigEndian.Uint32(b)
22 | }
23 |
--------------------------------------------------------------------------------
/key/aesmac/register.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package aesmac
5 |
6 | import (
7 | "github.com/ldclabs/cose/iana"
8 | "github.com/ldclabs/cose/key"
9 | )
10 |
11 | func init() {
12 | key.RegisterMACer(iana.KeyTypeSymmetric, iana.AlgorithmAES_MAC_128_64, New)
13 | key.RegisterMACer(iana.KeyTypeSymmetric, iana.AlgorithmAES_MAC_256_64, New)
14 | key.RegisterMACer(iana.KeyTypeSymmetric, iana.AlgorithmAES_MAC_128_128, New)
15 | key.RegisterMACer(iana.KeyTypeSymmetric, iana.AlgorithmAES_MAC_256_128, New)
16 | }
17 |
--------------------------------------------------------------------------------
/key/key_ops.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | // Ops represents the key operations.
7 | type Ops []int
8 |
9 | // Has returns true if the given operation is in the operations.
10 | func (os Ops) Has(op int) bool {
11 | for _, o := range os {
12 | if o == op {
13 | return true
14 | }
15 | }
16 | return false
17 | }
18 |
19 | // EmptyOrHas returns true if the operations is empty,
20 | // or the given operation is in the operations.
21 | func (os Ops) EmptyOrHas(op int) bool {
22 | return len(os) == 0 || os.Has(op)
23 | }
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/1_bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F41E Bug report"
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | ldclabs/cose version: v1.x.x
12 | A clear and concise description of what the bug is.
13 |
14 | **To Reproduce**
15 | Code to reproduce the behavior:
16 | ```go
17 | // your code.
18 | ```
19 | **Expected behavior**
20 | A clear and concise description of what you expected to happen.
21 |
22 | **Screenshots**
23 | If applicable, add screenshots to help explain your problem.
24 |
25 | **Additional context**
26 | Add any other context about the problem here.
27 |
--------------------------------------------------------------------------------
/key/random_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | )
11 |
12 | func TestGetRandomBytes(t *testing.T) {
13 | assert := assert.New(t)
14 |
15 | data := GetRandomBytes(8)
16 | assert.Equal(8, len(data))
17 |
18 | data2 := GetRandomBytes(8)
19 | assert.Equal(8, len(data))
20 |
21 | assert.NotEqual(data, data2)
22 | }
23 |
24 | func TestGetRandomUint32(t *testing.T) {
25 | assert := assert.New(t)
26 |
27 | u1 := GetRandomUint32()
28 | u2 := GetRandomUint32()
29 | assert.NotEqual(u1, u2)
30 | }
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/2_feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "\U0001F4A1 Feature request"
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/iana/tag.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package iana
5 |
6 | // CBOR tag values for COSE structures.
7 | //
8 | // From IANA registry https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml
9 | // as of 2022-12-19.
10 | const (
11 | // COSE Single Recipient Encrypted Data Object
12 | CBORTagCOSEEncrypt0 = 16
13 | // COSE Mac w/o Recipients Object
14 | CBORTagCOSEMac0 = 17
15 | // COSE Single Signer Data Object
16 | CBORTagCOSESign1 = 18
17 | // CBOR Web Token (CWT)
18 | CBORTagCWT = 61
19 | // COSE Encrypted Data Object
20 | CBORTagCOSEEncrypt = 96
21 | // COSE MACed Data Object
22 | CBORTagCOSEMac = 97
23 | // COSE Signed Data Object
24 | CBORTagCOSESign = 98
25 | )
26 |
--------------------------------------------------------------------------------
/key/key_crv.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "github.com/ldclabs/cose/iana"
8 | )
9 |
10 | // CrvAlg returns the algorithm that matched the key's curve.
11 | func CrvAlg(c int) Alg {
12 | switch c {
13 | case iana.EllipticCurveP_256:
14 | return iana.AlgorithmES256
15 | case iana.EllipticCurveP_384:
16 | return iana.AlgorithmES384
17 | case iana.EllipticCurveP_521:
18 | return iana.AlgorithmES512
19 | case iana.EllipticCurveEd25519:
20 | return iana.AlgorithmEdDSA
21 | case iana.EllipticCurveEd448:
22 | return iana.AlgorithmEdDSA
23 | case iana.EllipticCurveSecp256k1:
24 | return iana.AlgorithmES256K
25 | default:
26 | return iana.AlgorithmReserved
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/key/key_ops_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 |
11 | "github.com/ldclabs/cose/iana"
12 | )
13 |
14 | func TestKeyOps(t *testing.T) {
15 | assert := assert.New(t)
16 |
17 | var ops Ops
18 | assert.False(ops.Has(iana.KeyOperationSign))
19 | assert.True(ops.EmptyOrHas(iana.KeyOperationSign))
20 |
21 | ops = Ops{1, iana.KeyOperationVerify}
22 | assert.True(ops.Has(iana.KeyOperationSign))
23 | assert.True(ops.EmptyOrHas(iana.KeyOperationSign))
24 |
25 | assert.True(ops.Has(iana.KeyOperationVerify))
26 | assert.True(ops.EmptyOrHas(iana.KeyOperationVerify))
27 |
28 | assert.False(ops.Has(iana.KeyOperationEncrypt))
29 | assert.False(ops.EmptyOrHas(iana.KeyOperationEncrypt))
30 | }
31 |
--------------------------------------------------------------------------------
/key/ecdsa/register.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package ecdsa
5 |
6 | import (
7 | "github.com/ldclabs/cose/iana"
8 | "github.com/ldclabs/cose/key"
9 | )
10 |
11 | func init() {
12 | key.RegisterSigner(iana.KeyTypeEC2, iana.AlgorithmES256, iana.EllipticCurveP_256, NewSigner)
13 | key.RegisterSigner(iana.KeyTypeEC2, iana.AlgorithmES384, iana.EllipticCurveP_384, NewSigner)
14 | key.RegisterSigner(iana.KeyTypeEC2, iana.AlgorithmES512, iana.EllipticCurveP_521, NewSigner)
15 |
16 | key.RegisterVerifier(iana.KeyTypeEC2, iana.AlgorithmES256, iana.EllipticCurveP_256, NewVerifier)
17 | key.RegisterVerifier(iana.KeyTypeEC2, iana.AlgorithmES384, iana.EllipticCurveP_384, NewVerifier)
18 | key.RegisterVerifier(iana.KeyTypeEC2, iana.AlgorithmES512, iana.EllipticCurveP_521, NewVerifier)
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - 'main'
6 | jobs:
7 | # Test on various OS with default Go version.
8 | tests:
9 | name: Test on ${{matrix.os}}
10 | runs-on: ${{ matrix.os }}
11 | strategy:
12 | matrix:
13 | os: [ubuntu-latest, macos-latest, windows-latest]
14 | go-version: ['1.20.x', '1.21.x']
15 |
16 | steps:
17 | - name: Install Go
18 | uses: actions/setup-go@v5
19 | with:
20 | go-version: ${{ matrix.go-version }}
21 |
22 | - name: Checkout code
23 | uses: actions/checkout@v4
24 | with:
25 | fetch-depth: 1
26 |
27 | - name: Print Go version
28 | run: go version
29 |
30 | - name: Get dependencies
31 | run: go get -v -t -d ./...
32 |
33 | - name: Run tests
34 | run: go test -v -failfast -tags=test -timeout="3m" -race ./...
35 |
--------------------------------------------------------------------------------
/iana/curve.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package iana
5 |
6 | // IANA-registered COSE elliptic curves.
7 | //
8 | // From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves
9 | // as of 2022-12-19.
10 | const (
11 | EllipticCurveReserved = 0
12 | // EC2: NIST P-256 also known as secp256r1
13 | EllipticCurveP_256 = 1
14 | // EC2: NIST P-384 also known as secp384r1
15 | EllipticCurveP_384 = 2
16 | // EC2: NIST P-521 also known as secp521r1
17 | EllipticCurveP_521 = 3
18 | // OKP: X25519 for use w/ ECDH only
19 | EllipticCurveX25519 = 4
20 | // OKP: X448 for use w/ ECDH only
21 | EllipticCurveX448 = 5
22 | // OKP: Ed25519 for use w/ EdDSA only
23 | EllipticCurveEd25519 = 6
24 | // OKP: Ed448 for use w/ EdDSA only
25 | EllipticCurveEd448 = 7
26 | // EC2: SECG secp256k1 curve
27 | EllipticCurveSecp256k1 = 8
28 | )
29 |
--------------------------------------------------------------------------------
/key/key_crv_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/ldclabs/cose/iana"
10 | "github.com/stretchr/testify/assert"
11 | )
12 |
13 | func TestCrvAlg(t *testing.T) {
14 | assert := assert.New(t)
15 |
16 | for _, tc := range []struct {
17 | input int
18 | output Alg
19 | }{
20 | {iana.EllipticCurveP_256, iana.AlgorithmES256},
21 | {iana.EllipticCurveP_384, iana.AlgorithmES384},
22 | {iana.EllipticCurveP_521, iana.AlgorithmES512},
23 | {iana.EllipticCurveEd25519, iana.AlgorithmEdDSA},
24 | {iana.EllipticCurveEd448, iana.AlgorithmEdDSA},
25 | {iana.EllipticCurveSecp256k1, iana.AlgorithmES256K},
26 | {0, iana.AlgorithmReserved},
27 | {9, iana.AlgorithmReserved},
28 | {-1, iana.AlgorithmReserved},
29 | } {
30 | assert.Equal(tc.output, CrvAlg(tc.input))
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: CodeQL
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 | schedule:
9 | - cron: '26 4 * * 3'
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ 'go' ]
24 |
25 | steps:
26 | - name: Checkout repository
27 | uses: actions/checkout@v4
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v2
31 | with:
32 | languages: ${{ matrix.language }}
33 |
34 | - name: Autobuild
35 | uses: github/codeql-action/autobuild@v2
36 |
37 | - name: Perform CodeQL Analysis
38 | uses: github/codeql-action/analyze@v2
39 | with:
40 | category: "/language:${{matrix.language}}"
41 |
--------------------------------------------------------------------------------
/key/interface_mac.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | // MACer is the MAC interface for MAC objects.
7 | // It is used in COSE_Mac and COSE_Mac0.
8 | //
9 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-message-authentication-code.
10 | type MACer interface {
11 | // MACCreate computes message authentication code (MAC) for the given data.
12 | MACCreate(data []byte) ([]byte, error)
13 |
14 | // MACVerify verifies whether the given MAC is a correct message authentication code (MAC) for the given data.
15 | MACVerify(data, mac []byte) error
16 |
17 | // Key returns the key in the MACer.
18 | // If the key's "key_ops" field is present, it MUST include "MAC create":9 when creating an HMAC authentication tag.
19 | // If the key's "key_ops" field is present, it MUST include "MAC verify":10 when verifying an HMAC authentication tag.
20 | Key() Key
21 | }
22 |
--------------------------------------------------------------------------------
/key/aesccm/register.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package aesccm
5 |
6 | import (
7 | "github.com/ldclabs/cose/iana"
8 | "github.com/ldclabs/cose/key"
9 | )
10 |
11 | func init() {
12 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmAES_CCM_16_64_128, New)
13 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmAES_CCM_16_64_256, New)
14 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmAES_CCM_64_64_128, New)
15 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmAES_CCM_64_64_256, New)
16 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmAES_CCM_16_128_128, New)
17 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmAES_CCM_16_128_256, New)
18 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmAES_CCM_64_128_128, New)
19 | key.RegisterEncryptor(iana.KeyTypeSymmetric, iana.AlgorithmAES_CCM_64_128_256, New)
20 | }
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022-present LDC Labs
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/iana/operation.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package iana
5 |
6 | // Key operation values.
7 | //
8 | // See https://datatracker.ietf.org/doc/html/rfc9052#name-key-operation-values
9 | const (
10 | // Key is used to create signatures. Requires private key fields.
11 | KeyOperationSign = 1
12 | // Key is used for verification of signatures.
13 | KeyOperationVerify = 2
14 | // Key is used for key transport encryption.
15 | KeyOperationEncrypt = 3
16 | // Key is used for key transport decryption. Requires private key fields.
17 | KeyOperationDecrypt = 4
18 | // Key is used for key wrap encryption.
19 | KeyOperationWrapKey = 5
20 | // Key is used for key wrap decryption. Requires private key fields.
21 | KeyOperationUnwrapKey = 6
22 | // Key is used for deriving keys. Requires private key fields.
23 | KeyOperationDeriveKey = 7
24 | // Key is used for deriving bits not to be used as a key. Requires private key fields.
25 | KeyOperationDeriveBits = 8
26 | // Key is used for creating MACs.
27 | KeyOperationMacCreate = 9
28 | // Key is used for validating MACs.
29 | KeyOperationMacVerify = 10
30 | )
31 |
--------------------------------------------------------------------------------
/.github/workflows/ci-cover.yml:
--------------------------------------------------------------------------------
1 | name: CI Cover
2 | on:
3 | push:
4 | tags:
5 | - '*'
6 | jobs:
7 | # Test on various OS with default Go version.
8 | tests:
9 | name: Test on ${{matrix.os}}
10 | runs-on: ${{ matrix.os }}
11 | strategy:
12 | matrix:
13 | os: [ubuntu-latest]
14 | go-version: ['1.21.x']
15 |
16 | steps:
17 | - name: Install Go
18 | uses: actions/setup-go@v5
19 | with:
20 | go-version: ${{ matrix.go-version }}
21 |
22 | - name: Checkout code
23 | uses: actions/checkout@v4
24 | with:
25 | fetch-depth: 1
26 |
27 | - name: Print Go version
28 | run: go version
29 |
30 | - name: Get dependencies
31 | run: go get -v -t -d ./...
32 |
33 | - name: Run tests
34 | run: go test -v -failfast -tags=test -timeout="3m" -coverprofile="./coverage.out" -covermode="atomic" ./...
35 |
36 | - name: Upload coverage to Codecov
37 | uses: codecov/codecov-action@v4
38 | with:
39 | token: ${{ secrets.CODECOV_TOKEN }}
40 | files: ./coverage.out
41 | flags: unittests
42 | name: codecov-umbrella
43 | verbose: true
44 |
--------------------------------------------------------------------------------
/cwt/claims.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | // Package cwt implements CBOR Web Token (CWT) as defined in RFC8392.
5 | // https://datatracker.ietf.org/doc/html/rfc8392.
6 | package cwt
7 |
8 | import (
9 | "github.com/ldclabs/cose/key"
10 | )
11 |
12 | // Claims represents a set of common claims for CWT.
13 | type Claims struct {
14 | Issuer string `cbor:"1,keyasint,omitempty" json:"iss,omitempty"`
15 | Subject string `cbor:"2,keyasint,omitempty" json:"sub,omitempty"`
16 | Audience string `cbor:"3,keyasint,omitempty" json:"aud,omitempty"`
17 | Expiration uint64 `cbor:"4,keyasint,omitempty" json:"exp,omitempty"` // seconds since epoch
18 | NotBefore uint64 `cbor:"5,keyasint,omitempty" json:"nbf,omitempty"` // seconds since epoch
19 | IssuedAt uint64 `cbor:"6,keyasint,omitempty" json:"iat,omitempty"` // seconds since epoch
20 | CWTID key.ByteStr `cbor:"7,keyasint,omitempty" json:"cti,omitempty"`
21 | }
22 |
23 | // Bytesify returns a CBOR-encoded byte slice.
24 | // It returns nil if MarshalCBOR failed.
25 | func (c *Claims) Bytesify() []byte {
26 | b, _ := key.MarshalCBOR(c)
27 | return b
28 | }
29 |
--------------------------------------------------------------------------------
/key/interface_encryption.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | // Encryptor is the encrypting and decrypting interface for content encryption.
7 | // It is used in COSE_Encrypt and COSE_Encrypt0.
8 | //
9 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-content-encryption-algorith.
10 | type Encryptor interface {
11 | // Encrypt encrypts a plaintext with the given nonce and additional data.
12 | // It returns the ciphertext or error.
13 | Encrypt(nonce, plaintext, additionalData []byte) (ciphertext []byte, err error)
14 |
15 | // Decrypt decrypts a ciphertext with the given nonce and additional data.
16 | // It returns the corresponding plaintext or error.
17 | Decrypt(nonce, ciphertext, additionalData []byte) (plaintext []byte, err error)
18 |
19 | // NonceSize returns the size of the nonce for encrypting and decrypting.
20 | NonceSize() int
21 |
22 | // Key returns the symmetric key in the Encryptor.
23 | // If the key's "key_ops" field is present, it MUST include "encrypt":3 when encrypting an plaintext.
24 | // If the key's "key_ops" field is present, it MUST include "decrypt":4 when decrypting an ciphertext.
25 | Key() Key
26 | }
27 |
--------------------------------------------------------------------------------
/key/key_alg.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "crypto"
8 | "fmt"
9 |
10 | "github.com/ldclabs/cose/iana"
11 | )
12 |
13 | // Alg represents an IANA algorithm entry in the COSE Algorithms registry.
14 | //
15 | // Reference https://www.iana.org/assignments/cose/cose.xhtml#algorithms
16 | type Alg int
17 |
18 | // HashFunc returns the hash associated with the algorithm supported.
19 | func (a Alg) HashFunc() crypto.Hash {
20 | switch a {
21 | case iana.AlgorithmES256, iana.AlgorithmHMAC_256_64, iana.AlgorithmHMAC_256_256:
22 | return crypto.SHA256
23 | case iana.AlgorithmES384, iana.AlgorithmHMAC_384_384:
24 | return crypto.SHA384
25 | case iana.AlgorithmES512, iana.AlgorithmHMAC_512_512:
26 | return crypto.SHA512
27 | default:
28 | return 0
29 | }
30 | }
31 |
32 | // ComputeHash computes a hash of the given data using the given hash.
33 | func ComputeHash(h crypto.Hash, data []byte) ([]byte, error) {
34 | if !h.Available() {
35 | return nil, fmt.Errorf("cose/key: ComputeHash: hash function %d is not available", h)
36 | }
37 |
38 | hh := h.New()
39 | hh.Write(data) // err should never happen
40 | return hh.Sum(nil), nil
41 | }
42 |
--------------------------------------------------------------------------------
/key/keyset.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import "bytes"
7 |
8 | // KeySet is a set of Keys.
9 | type KeySet []Key
10 |
11 | // Lookup returns the first key matching the given key id.
12 | // return nil if there are no keys matching the key id
13 | func (ks KeySet) Lookup(kid []byte) Key {
14 | for _, k := range ks {
15 | if bytes.Equal(k.Kid(), kid) {
16 | return k
17 | }
18 | }
19 |
20 | return nil
21 | }
22 |
23 | // Signers returns the signers for the keys in the KeySet.
24 | func (ks KeySet) Signers() (Signers, error) {
25 | signers := make(Signers, 0, len(ks))
26 | for _, k := range ks {
27 | signer, err := k.Signer()
28 | if err != nil {
29 | return nil, err
30 | }
31 | signers = append(signers, signer)
32 | }
33 |
34 | return signers, nil
35 | }
36 |
37 | // Verifiers returns the verifiers for the keys in the KeySet.
38 | func (ks KeySet) Verifiers() (Verifiers, error) {
39 | verifiers := make(Verifiers, 0, len(ks))
40 | for _, k := range ks {
41 | verifier, err := k.Verifier()
42 | if err != nil {
43 | return nil, err
44 | }
45 | verifiers = append(verifiers, verifier)
46 | }
47 |
48 | return verifiers, nil
49 | }
50 |
--------------------------------------------------------------------------------
/key/cbor.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "github.com/fxamacker/cbor/v2"
8 | )
9 |
10 | var encOpts = cbor.EncOptions{
11 | Sort: cbor.SortBytewiseLexical,
12 | IndefLength: cbor.IndefLengthForbidden,
13 | }
14 | var encMode, _ = encOpts.EncMode()
15 |
16 | var decOpts = cbor.DecOptions{
17 | DupMapKey: cbor.DupMapKeyEnforcedAPF,
18 | IndefLength: cbor.IndefLengthForbidden,
19 | }
20 | var decMode, _ = decOpts.DecMode()
21 |
22 | // MarshalCBOR marshals value with the special cbor.EncOptions.
23 | func MarshalCBOR(v any) ([]byte, error) {
24 | data, err := encMode.Marshal(v)
25 | if err != nil {
26 | return nil, err
27 | }
28 | return data, nil
29 | }
30 |
31 | // MustMarshalCBOR marshals value with the special cbor.EncOptions.
32 | // It will panic if marshaling failed.
33 | func MustMarshalCBOR(v any) []byte {
34 | data, err := encMode.Marshal(v)
35 | if err != nil {
36 | panic(err)
37 | }
38 | return data
39 | }
40 |
41 | // UnmarshalCBOR unmarshals data into value with the special cbor.DecOptions.
42 | func UnmarshalCBOR(data []byte, v any) error {
43 | return decMode.Unmarshal(data, v)
44 | }
45 |
46 | // ValidCBOR returns true if data is valid CBOR.
47 | func ValidCBOR(data []byte) error {
48 | return decMode.Wellformed(data)
49 | }
50 |
--------------------------------------------------------------------------------
/key/key_alg_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "crypto"
8 | "testing"
9 |
10 | "github.com/ldclabs/cose/iana"
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | func TestAlg(t *testing.T) {
15 | assert := assert.New(t)
16 |
17 | for _, tc := range []struct {
18 | input Alg
19 | output crypto.Hash
20 | }{
21 | {iana.AlgorithmES256, crypto.SHA256},
22 | {iana.AlgorithmHMAC_256_64, crypto.SHA256},
23 | {iana.AlgorithmHMAC_256_256, crypto.SHA256},
24 | {iana.AlgorithmES384, crypto.SHA384},
25 | {iana.AlgorithmHMAC_384_384, crypto.SHA384},
26 | {iana.AlgorithmES512, crypto.SHA512},
27 | {iana.AlgorithmHMAC_512_512, crypto.SHA512},
28 | {0, 0},
29 | {9, 0},
30 | {-1, 0},
31 | } {
32 | assert.Equal(tc.output, tc.input.HashFunc())
33 | }
34 | }
35 |
36 | func TestComputeHash(t *testing.T) {
37 | assert := assert.New(t)
38 |
39 | sum, err := ComputeHash(0, []byte("hello"))
40 | assert.ErrorContains(err, "hash function 0 is not available")
41 | assert.Nil(sum)
42 |
43 | sum, err = ComputeHash(crypto.SHA256, []byte("hello"))
44 | assert.NoError(err)
45 | assert.Equal(32, len(sum))
46 |
47 | sum, err = ComputeHash(crypto.SHA384, []byte("hello"))
48 | assert.NoError(err)
49 | assert.Equal(48, len(sum))
50 |
51 | sum, err = ComputeHash(crypto.SHA512, []byte("hello"))
52 | assert.NoError(err)
53 | assert.Equal(64, len(sum))
54 | }
55 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
4 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
8 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
9 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
10 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
11 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
12 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
13 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
14 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
17 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
19 |
--------------------------------------------------------------------------------
/key/hkdf/hkdf.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | // Package hkdf implements key derivation function HKDF for COSE as defined in RFC9053.
5 | // https://datatracker.ietf.org/doc/html/rfc9053#name-key-derivation-functions-kd
6 | package hkdf
7 |
8 | import (
9 | "crypto/aes"
10 | "crypto/sha256"
11 | "crypto/sha512"
12 | "io"
13 |
14 | "golang.org/x/crypto/hkdf"
15 | )
16 |
17 | // HKDF256 derives a key from the given secret, salt, info and key size, using HKDF-SHA-256.
18 | func HKDF256(secret, salt, info []byte, keySize int) ([]byte, error) {
19 | key := make([]byte, keySize)
20 | if _, err := io.ReadFull(hkdf.New(sha256.New, secret, salt, info), key); err != nil {
21 | return nil, err
22 | }
23 | return key, nil
24 | }
25 |
26 | // HKDF512 derives a key from the given secret, salt, info and key size, using HKDF-SHA-512.
27 | func HKDF512(secret, salt, info []byte, keySize int) ([]byte, error) {
28 | key := make([]byte, keySize)
29 | if _, err := io.ReadFull(hkdf.New(sha512.New, secret, salt, info), key); err != nil {
30 | return nil, err
31 | }
32 | return key, nil
33 | }
34 |
35 | // HKDFAES derives a key from the given secret, info and key size.
36 | // The secret should be the AES key, either 16, or 32 bytes to select HKDF-AES-128, or HKDF-AES-256.
37 | func HKDFAES(secret, info []byte, keySize int) ([]byte, error) {
38 | block, err := aes.NewCipher(secret)
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | key := make([]byte, keySize)
44 | if _, err := io.ReadFull(NewAES(block, info), key); err != nil {
45 | return nil, err
46 | }
47 | return key, nil
48 | }
49 |
--------------------------------------------------------------------------------
/key/hkdf/example_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package hkdf_test
5 |
6 | import (
7 | "fmt"
8 |
9 | "github.com/ldclabs/cose/cose"
10 | "github.com/ldclabs/cose/iana"
11 | "github.com/ldclabs/cose/key"
12 |
13 | "github.com/ldclabs/cose/key/hkdf"
14 | )
15 |
16 | func ExampleHKDF256() {
17 | // Create a KDF Context
18 | kdfContext := cose.KDFContext{
19 | AlgorithmID: iana.AlgorithmA128GCM,
20 | SuppPubInfo: cose.SuppPubInfo{
21 | KeyDataLength: 128,
22 | Protected: cose.Headers{
23 | iana.HeaderParameterAlg: iana.AlgorithmECDH_ES_HKDF_256,
24 | },
25 | },
26 | }
27 | ctxData, err := key.MarshalCBOR(kdfContext)
28 | if err != nil {
29 | panic(err)
30 | }
31 |
32 | // Derive a key
33 | secret := key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6")
34 | k, err := hkdf.HKDF256(secret, nil, ctxData, 128/8)
35 | if err != nil {
36 | panic(err)
37 | }
38 | fmt.Printf("key: %X\n", k)
39 | // key: 56074D506729CA40C4B4FE50C6439893
40 |
41 | // Output:
42 | // key: 56074D506729CA40C4B4FE50C6439893
43 | }
44 |
45 | func ExampleHKDFAES() {
46 | // Create a KDF Context
47 | kdfContext := cose.KDFContext{
48 | AlgorithmID: iana.AlgorithmAES_CCM_16_64_128,
49 | SuppPubInfo: cose.SuppPubInfo{
50 | KeyDataLength: 128,
51 | Protected: cose.Headers{
52 | iana.HeaderParameterAlg: iana.AlgorithmDirect_HKDF_AES_128,
53 | },
54 | },
55 | }
56 | ctxData, err := key.MarshalCBOR(kdfContext)
57 | if err != nil {
58 | panic(err)
59 | }
60 |
61 | // Derive a key
62 | secret := key.Base64Bytesify("hJtXIZ2uSN5kbQfbtTNWbg")
63 | k, err := hkdf.HKDFAES(secret, ctxData, 128/8)
64 | if err != nil {
65 | panic(err)
66 | }
67 | fmt.Printf("key: %X\n", k)
68 | // key: F0CCBAF836D73DA63ED8508EF966EEC9
69 |
70 | // Output:
71 | // key: F0CCBAF836D73DA63ED8508EF966EEC9
72 | }
73 |
--------------------------------------------------------------------------------
/key/hkdf/hkdf_aes.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package hkdf
5 |
6 | import (
7 | "crypto/aes"
8 | "crypto/cipher"
9 | "errors"
10 | "io"
11 | )
12 |
13 | // NewAES returns a Reader, from which keys can be read, using the given cipher.Block (as
14 | // AES-CBC-MAC PRF) and context info. Context info can be nil.
15 | func NewAES(block cipher.Block, info []byte) io.Reader {
16 | return &aesHKDF{block: block, info: info, counter: 1}
17 | }
18 |
19 | type aesHKDF struct {
20 | block cipher.Block
21 |
22 | info []byte
23 | counter byte
24 |
25 | buf []byte
26 | prev []byte
27 | output []byte
28 | }
29 |
30 | var fixedIV = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
31 |
32 | func (f *aesHKDF) Read(p []byte) (int, error) {
33 | // Check whether enough data can be generated
34 | need := len(p)
35 | remains := len(f.output) + int(255-f.counter+1)*aes.BlockSize
36 | if remains < need {
37 | return 0, errors.New("cose/key/hkdf: entropy limit reached")
38 | }
39 |
40 | // Read any leftover from the buffer
41 | n := copy(p, f.output)
42 | p = p[n:]
43 |
44 | // Fill the rest of the buffer
45 | for len(p) > 0 {
46 | inputSize := len(f.prev) + len(f.info) + 1
47 | x := inputSize % aes.BlockSize
48 | if x > 0 {
49 | inputSize += aes.BlockSize - x
50 | }
51 |
52 | if cap(f.buf) < inputSize {
53 | f.buf = make([]byte, 0, inputSize)
54 | }
55 |
56 | f.buf = append(f.buf[:0], f.prev...)
57 | f.buf = append(f.buf, f.info...)
58 | f.buf = append(f.buf, f.counter)
59 | f.buf = append(f.buf, fixedIV[:aes.BlockSize-x]...)
60 |
61 | mode := cipher.NewCBCEncrypter(f.block, fixedIV)
62 | mode.CryptBlocks(f.buf, f.buf)
63 |
64 | // Read digest
65 | f.prev = append(f.prev[:0], f.buf[len(f.buf)-aes.BlockSize:]...)
66 | f.counter++
67 |
68 | // Copy the new batch into p
69 | f.output = f.prev
70 | n = copy(p, f.output)
71 | p = p[n:]
72 | }
73 |
74 | // Save leftovers for next run
75 | f.output = f.output[n:]
76 | return need, nil
77 | }
78 |
--------------------------------------------------------------------------------
/cose/tag_prefix.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose
5 |
6 | import "bytes"
7 |
8 | // RemoveCBORTag removes the CWT_Tag / COSE_Sign_Tag / COSE_Sign1_Tag /
9 | // COSE_Encrypt_Tag / COSE_Encrypt0_Tag / COSE_Mac_Tag / COSE_Mac0_Tag from the data.
10 | func RemoveCBORTag(data []byte) []byte {
11 | if bytes.HasPrefix(data, cwtPrefix) {
12 | data = data[2:]
13 | }
14 |
15 | switch {
16 | case bytes.HasPrefix(data, sign1MessagePrefix) ||
17 | bytes.HasPrefix(data, mac0MessagePrefix) ||
18 | bytes.HasPrefix(data, encrypt0MessagePrefix):
19 | data = data[1:]
20 | case bytes.HasPrefix(data, signMessagePrefix) ||
21 | bytes.HasPrefix(data, macMessagePrefix) ||
22 | bytes.HasPrefix(data, encryptMessagePrefix):
23 | data = data[2:]
24 | }
25 |
26 | return data
27 | }
28 |
29 | // cwtPrefix represents the fixed prefix of CWT CBOR tag.
30 | // https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml#tags
31 | var cwtPrefix = []byte{
32 | 0xd8, 0x3d, // #6.61
33 | }
34 |
35 | // sign1MessagePrefix represents the fixed prefix of COSE_Sign1_Tagged.
36 | var sign1MessagePrefix = []byte{
37 | 0xd2, // #6.18
38 | 0x84, // array of length 4
39 | }
40 |
41 | // signMessagePrefix represents the fixed prefix of COSE_Sign_Tagged.
42 | var signMessagePrefix = []byte{
43 | 0xd8, 0x62, // #6.98
44 | 0x84, // Array of length 4
45 | }
46 |
47 | // mac0MessagePrefix represents the fixed prefix of COSE_Mac0_Tagged.
48 | var mac0MessagePrefix = []byte{
49 | 0xd1, // #6.17
50 | 0x84, // array of length 4
51 | }
52 |
53 | // macMessagePrefix represents the fixed prefix of COSE_Mac_Tagged.
54 | var macMessagePrefix = []byte{
55 | 0xd8, 0x61, // #6.97
56 | 0x85, // array of length 5
57 | }
58 |
59 | // encrypt0MessagePrefix represents the fixed prefix of COSE_Encrypt0_Tagged.
60 | var encrypt0MessagePrefix = []byte{
61 | 0xd0, // #6.16
62 | 0x83, // array of length 3
63 | }
64 |
65 | // encryptMessagePrefix represents the fixed prefix of COSE_Encrypt_Tagged.
66 | var encryptMessagePrefix = []byte{
67 | 0xd8, 0x60, // #6.96
68 | 0x84, // array of length 4
69 | }
70 |
--------------------------------------------------------------------------------
/cose/mac0_example_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose_test
5 |
6 | import (
7 | "fmt"
8 |
9 | "github.com/fxamacker/cbor/v2"
10 | "github.com/ldclabs/cose/cose"
11 | "github.com/ldclabs/cose/iana"
12 | "github.com/ldclabs/cose/key"
13 | _ "github.com/ldclabs/cose/key/aesmac"
14 | )
15 |
16 | func ExampleMac0Message() {
17 | // load key
18 | k := key.Key{
19 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
20 | iana.KeyParameterKid: []byte("our-secret"),
21 | iana.KeyParameterAlg: iana.AlgorithmAES_MAC_256_64,
22 | iana.SymmetricKeyParameterK: key.Base64Bytesify("hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"),
23 | }
24 |
25 | macer, err := k.MACer()
26 | if err != nil {
27 | panic(err)
28 | }
29 |
30 | // create a COSE_Mac0 message
31 | obj := &cose.Mac0Message[[]byte]{
32 | Unprotected: cose.Headers{},
33 | Payload: []byte("This is the content."),
34 | }
35 |
36 | // compute MAC
37 | err = obj.Compute(macer, nil)
38 | if err != nil {
39 | panic(err)
40 | }
41 | fmt.Printf("Tag: %x\n", obj.Tag())
42 | // Tag: 726043745027214f
43 |
44 | // encode COSE_Mac0 message
45 | coseData, err := cbor.Marshal(obj)
46 | if err != nil {
47 | panic(err)
48 | }
49 |
50 | // decode a COSE_Mac0 message
51 | var obj3 cose.Mac0Message[[]byte]
52 | cbor.Unmarshal(coseData, &obj3)
53 | if err != nil {
54 | panic(err)
55 | }
56 |
57 | // verify MAC
58 | err = obj3.Verify(macer, nil)
59 | if err != nil {
60 | panic(err)
61 | }
62 | fmt.Printf("Payload: %s\n", string(obj3.Payload))
63 | // Payload: This is the content.
64 | fmt.Printf("Tag: %x\n", obj3.Tag())
65 | // Tag: 726043745027214f
66 |
67 | // or verify and decode a COSE_Mac0 message
68 | obj2, err := cose.VerifyMac0Message[[]byte](macer, coseData, nil)
69 | if err != nil {
70 | panic(err)
71 | }
72 | fmt.Printf("Payload: %s\n", string(obj2.Payload))
73 | // Payload: This is the content.
74 | fmt.Printf("Tag: %x\n", obj2.Tag())
75 | // Tag: 726043745027214f
76 |
77 | // Output:
78 | // Tag: 726043745027214f
79 | // Payload: This is the content.
80 | // Tag: 726043745027214f
81 | // Payload: This is the content.
82 | // Tag: 726043745027214f
83 | }
84 |
--------------------------------------------------------------------------------
/key/interface_signing_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key_test
5 |
6 | import (
7 | "fmt"
8 | "testing"
9 |
10 | "github.com/stretchr/testify/assert"
11 | "github.com/stretchr/testify/require"
12 |
13 | "github.com/ldclabs/cose/iana"
14 | "github.com/ldclabs/cose/key"
15 | "github.com/ldclabs/cose/key/ecdsa"
16 | "github.com/ldclabs/cose/key/ed25519"
17 | )
18 |
19 | func TestSigners(t *testing.T) {
20 | assert := assert.New(t)
21 |
22 | k1, err := ed25519.GenerateKey()
23 | require.NoError(t, err)
24 | s1, err := k1.Signer()
25 | require.NoError(t, err)
26 |
27 | k2, err := ecdsa.GenerateKey(iana.AlgorithmES256)
28 | require.NoError(t, err)
29 | s2, err := k2.Signer()
30 | require.NoError(t, err)
31 |
32 | ss := key.Signers{s1, s2}
33 | assert.Nil(ss.Lookup([]byte{1, 2, 3}))
34 | assert.Equal(fmt.Sprintf("%p", s1), fmt.Sprintf("%p", ss.Lookup(k1.Kid())))
35 | assert.Equal(fmt.Sprintf("%p", s2), fmt.Sprintf("%p", ss.Lookup(k2.Kid())))
36 |
37 | ks := ss.KeySet()
38 | assert.Equal(2, len(ks))
39 | assert.Equal(key.MustMarshalCBOR(k1), key.MustMarshalCBOR(ks[0]))
40 | assert.Equal(key.MustMarshalCBOR(k2), key.MustMarshalCBOR(ks[1]))
41 | }
42 |
43 | func TestVerifiers(t *testing.T) {
44 | assert := assert.New(t)
45 |
46 | k1, err := ed25519.GenerateKey()
47 | require.NoError(t, err)
48 | v1, err := k1.Verifier()
49 | require.NoError(t, err)
50 |
51 | k2, err := ecdsa.GenerateKey(iana.AlgorithmES256)
52 | require.NoError(t, err)
53 | v2, err := k2.Verifier()
54 | require.NoError(t, err)
55 |
56 | vs := key.Verifiers{v1, v2}
57 | assert.Nil(vs.Lookup([]byte{1, 2, 3}))
58 | assert.Equal(fmt.Sprintf("%p", v1), fmt.Sprintf("%p", vs.Lookup(k1.Kid())))
59 | assert.Equal(fmt.Sprintf("%p", v2), fmt.Sprintf("%p", vs.Lookup(k2.Kid())))
60 |
61 | ks := vs.KeySet()
62 | assert.Equal(2, len(ks))
63 | assert.NotEqual(key.MustMarshalCBOR(k1), key.MustMarshalCBOR(ks[0]))
64 | pk1, err := ed25519.ToPublicKey(k1)
65 | require.NoError(t, err)
66 | assert.Equal(key.MustMarshalCBOR(pk1), key.MustMarshalCBOR(ks[0]))
67 |
68 | assert.NotEqual(key.MustMarshalCBOR(k2), key.MustMarshalCBOR(ks[1]))
69 | pk2, err := ecdsa.ToPublicKey(k2)
70 | require.NoError(t, err)
71 | assert.Equal(key.MustMarshalCBOR(pk2), key.MustMarshalCBOR(ks[1]))
72 | }
73 |
--------------------------------------------------------------------------------
/cose/encrypt0_example_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose_test
5 |
6 | import (
7 | "fmt"
8 |
9 | "github.com/ldclabs/cose/cose"
10 | "github.com/ldclabs/cose/cwt"
11 | "github.com/ldclabs/cose/iana"
12 | "github.com/ldclabs/cose/key"
13 | _ "github.com/ldclabs/cose/key/aesccm"
14 | )
15 |
16 | func ExampleEncrypt0Message() {
17 | // load key
18 | k := key.Key{
19 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
20 | iana.KeyParameterKid: []byte("our-secret2"),
21 | iana.KeyParameterAlg: iana.AlgorithmAES_CCM_16_64_128,
22 | iana.SymmetricKeyParameterK: key.Base64Bytesify("hJtXhkV8FJG-Onbc6mxCcY"),
23 | }
24 |
25 | encryptor, err := k.Encryptor()
26 | if err != nil {
27 | panic(err)
28 | }
29 |
30 | // create a claim set
31 | claims := cwt.Claims{
32 | Issuer: "ldc:ca",
33 | Subject: "ldc:chain",
34 | Audience: "ldc:txpool",
35 | Expiration: 1670123579,
36 | CWTID: []byte{1, 2, 3, 4},
37 | }
38 |
39 | // create a COSE_Encrypt0 message
40 | obj := &cose.Encrypt0Message[cwt.Claims]{
41 | Payload: claims,
42 | }
43 |
44 | // encrypt and encode COSE_Encrypt0 message
45 | cwtData, err := obj.EncryptAndEncode(encryptor, []byte("some external data."))
46 | if err != nil {
47 | panic(err)
48 | }
49 |
50 | // will generate a random IV for each encryption when not set.
51 | // iv, _ := obj.Unprotected.GetBytes(iana.HeaderParameterIV)
52 | // fmt.Printf("IV: %x\n", iv)
53 | // IV: ab1ec0651cc7878ab5410eb366
54 |
55 | fmt.Printf("CWT(%d bytes): %x...\n", len(cwtData), cwtData[:20])
56 | // CWT(89 bytes): d08343a1010aa2044b6f75722d73656372657432...
57 |
58 | obj2, err := cose.DecryptEncrypt0Message[cwt.Claims](encryptor, cwtData, []byte("some external data."))
59 | if err != nil {
60 | panic(err)
61 | }
62 | fmt.Printf("Payload: %#v\n", obj2.Payload)
63 | // Payload: cwt.Claims{Issuer:"ldc:ca", Subject:"ldc:chain", Audience:"ldc:txpool", Expiration:0x638c103b, NotBefore:0x0, IssuedAt:0x0, CWTID:key.ByteStr{0x1, 0x2, 0x3, 0x4}}
64 |
65 | // Output:
66 | // CWT(89 bytes): d08343a1010aa2044b6f75722d73656372657432...
67 | // Payload: cwt.Claims{Issuer:"ldc:ca", Subject:"ldc:chain", Audience:"ldc:txpool", Expiration:0x638c103b, NotBefore:0x0, IssuedAt:0x0, CWTID:key.ByteStr{0x1, 0x2, 0x3, 0x4}}
68 | }
69 |
--------------------------------------------------------------------------------
/key/bytestr_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "encoding/base64"
8 | "encoding/json"
9 | "testing"
10 |
11 | "github.com/fxamacker/cbor/v2"
12 | "github.com/stretchr/testify/assert"
13 | "github.com/stretchr/testify/require"
14 | )
15 |
16 | func TestByteStr(t *testing.T) {
17 | assert := assert.New(t)
18 |
19 | bstr := ByteStr(GetRandomBytes(8))
20 |
21 | str := bstr.String()
22 | b64str := bstr.Base64()
23 |
24 | text, err := bstr.MarshalText()
25 | require.NoError(t, err)
26 | assert.Equal(str, string(text))
27 | {
28 | var bstr2 ByteStr
29 | require.NoError(t, bstr2.UnmarshalText(text))
30 | assert.Equal([]byte(bstr), []byte(bstr2))
31 | }
32 |
33 | jsonstr, err := json.Marshal(bstr)
34 | require.NoError(t, err)
35 | assert.Equal(`"`+str+`"`, string(jsonstr))
36 | {
37 | var bstr2 ByteStr
38 | require.NoError(t, json.Unmarshal(jsonstr, &bstr2))
39 | assert.Equal([]byte(bstr), []byte(bstr2))
40 | }
41 |
42 | data := HexBytesify(str)
43 | assert.Equal([]byte(bstr), data)
44 |
45 | data = Base64Bytesify(b64str)
46 | assert.Equal([]byte(bstr), data)
47 |
48 | data, err = cbor.Marshal(bstr)
49 | require.NoError(t, err)
50 | {
51 | var bstr2 ByteStr
52 | require.NoError(t, cbor.Unmarshal(data, &bstr2))
53 | assert.Equal([]byte(bstr), []byte(bstr2))
54 | }
55 | }
56 |
57 | func TestHexBytesify(t *testing.T) {
58 | assert := assert.New(t)
59 |
60 | data := HexBytesify("az")
61 | assert.Nil(data)
62 |
63 | data = HexBytesify("0aff")
64 | assert.Equal([]byte{0x0a, 0xff}, data)
65 | assert.Equal(data, HexBytesify("0AFF"))
66 | }
67 |
68 | func TestBase64Bytesify(t *testing.T) {
69 | assert := assert.New(t)
70 |
71 | a := base64.URLEncoding.EncodeToString([]byte{0x0a, 0xff})
72 | b := base64.RawURLEncoding.EncodeToString([]byte{0x0a, 0xff})
73 | assert.NotEqual(a, b)
74 |
75 | assert.Equal([]byte{0x0a, 0xff}, Base64Bytesify(a))
76 | assert.Equal([]byte{0x0a, 0xff}, Base64Bytesify(b))
77 | assert.Nil(Base64Bytesify(a[1:]))
78 | }
79 |
80 | func TestSumKid(t *testing.T) {
81 | assert := assert.New(t)
82 |
83 | assert.Equal(20, len(SumKid(nil)))
84 | assert.Equal(SumKid(nil), SumKid([]byte{}))
85 |
86 | assert.Equal(20, len(SumKid(GetRandomBytes(8))))
87 | assert.Equal(20, len(SumKid(GetRandomBytes(32))))
88 | assert.NotEqual(SumKid(GetRandomBytes(8)), SumKid(GetRandomBytes(8)))
89 | }
90 |
--------------------------------------------------------------------------------
/key/interface_signing.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import "bytes"
7 |
8 | // Signer is the signing interface for signing objects.
9 | // It is used in COSE_Sign and COSE_Sign1.
10 | //
11 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-signature-algorithms.
12 | type Signer interface {
13 | // Computes the digital signature for data.
14 | Sign(data []byte) ([]byte, error)
15 |
16 | // Key returns the private key in the Signer.
17 | // If the key's "key_ops" field is present, it MUST include "sign":1.
18 | Key() Key
19 | }
20 |
21 | // Verifier is the verifying interface for signing objects.
22 | //
23 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-signature-algorithms.
24 | type Verifier interface {
25 | // Verifies returns nil if signature is a valid signature for data; otherwise returns an error.
26 | Verify(data, signature []byte) error
27 |
28 | // Key returns the public key in the Verifier.
29 | // The key returned by this method should not include private key bytes.
30 | // If the key's "key_ops" field is present, it MUST include "verify":12.
31 | Key() Key
32 | }
33 |
34 | // Signers is a list of signers to be used for signing with one or more signers.
35 | //
36 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-signing-with-one-or-more-si.
37 | type Signers []Signer
38 |
39 | // Lookup returns the Signer for the given key ID.
40 | func (ss Signers) Lookup(kid []byte) Signer {
41 | for _, s := range ss {
42 | if bytes.Equal(s.Key().Kid(), kid) {
43 | return s
44 | }
45 | }
46 | return nil
47 | }
48 |
49 | // KeySet returns a set of private keys from the Signers.
50 | func (ss Signers) KeySet() KeySet {
51 | ks := make(KeySet, len(ss))
52 | for i, v := range ss {
53 | ks[i] = v.Key()
54 | }
55 | return ks
56 | }
57 |
58 | // Verifiers is a list of verifiers to be used for verifying with one or more verifiers.
59 | //
60 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-signing-with-one-or-more-si.
61 | type Verifiers []Verifier
62 |
63 | // Lookup returns the Verifier for the given key ID.
64 | func (vs Verifiers) Lookup(kid []byte) Verifier {
65 | for _, v := range vs {
66 | if bytes.Equal(v.Key().Kid(), kid) {
67 | return v
68 | }
69 | }
70 | return nil
71 | }
72 |
73 | // KeySet returns a set of public keys from the Verifiers.
74 | func (vs Verifiers) KeySet() KeySet {
75 | ks := make(KeySet, len(vs))
76 | for i, v := range vs {
77 | ks[i] = v.Key()
78 | }
79 | return ks
80 | }
81 |
--------------------------------------------------------------------------------
/cwt/claims_map.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cwt
5 |
6 | import (
7 | "github.com/ldclabs/cose/key"
8 | )
9 |
10 | // ClaimsMap supports full claims for CWT.
11 | //
12 | // Reference https://www.iana.org/assignments/cwt/cwt.xhtml
13 | type ClaimsMap key.CoseMap
14 |
15 | // Has returns true if the ClaimsMap has the given claim.
16 | func (cm ClaimsMap) Has(claim any) bool {
17 | return key.CoseMap(cm).Has(claim)
18 | }
19 |
20 | // Get returns the value of the given claim.
21 | func (cm ClaimsMap) Get(claim any) any {
22 | return key.CoseMap(cm).Get(claim)
23 | }
24 |
25 | // Set sets the claim. claim key should be int or string.
26 | func (cm ClaimsMap) Set(p, value any) error {
27 | return key.CoseMap(cm).Set(p, value)
28 | }
29 |
30 | // GetBool returns the value of the given claim as a bool, or a error.
31 | func (cm ClaimsMap) GetBool(claim any) (bool, error) {
32 | return key.CoseMap(cm).GetBool(claim)
33 | }
34 |
35 | // GetInt returns the value of the given claim as a int, or a error.
36 | func (cm ClaimsMap) GetInt(claim any) (int, error) {
37 | return key.CoseMap(cm).GetInt(claim)
38 | }
39 |
40 | // GetInt64 returns the value of the given claim as a int64, or a error.
41 | func (cm ClaimsMap) GetInt64(claim any) (int64, error) {
42 | return key.CoseMap(cm).GetInt64(claim)
43 | }
44 |
45 | // GetUint64 returns the value of the given claim as a uint64, or a error.
46 | func (cm ClaimsMap) GetUint64(claim any) (uint64, error) {
47 | return key.CoseMap(cm).GetUint64(claim)
48 | }
49 |
50 | // GetBytes returns the value of the given claim as a slice of bytes, or a error.
51 | func (cm ClaimsMap) GetBytes(claim any) ([]byte, error) {
52 | return key.CoseMap(cm).GetBytes(claim)
53 | }
54 |
55 | // GetString returns the value of the given claim as a string, or a error.
56 | func (cm ClaimsMap) GetString(claim any) (string, error) {
57 | return key.CoseMap(cm).GetString(claim)
58 | }
59 |
60 | // GetMap returns the value of the given parameter as a key.CoseMap, or a error.
61 | func (cm ClaimsMap) GetMap(claim any) (key.CoseMap, error) {
62 | return key.CoseMap(cm).GetMap(claim)
63 | }
64 |
65 | // MarshalCBOR implements the CBOR Marshaler interface for ClaimsMap.
66 | func (cm ClaimsMap) MarshalCBOR() ([]byte, error) {
67 | return key.CoseMap(cm).MarshalCBOR()
68 | }
69 |
70 | // UnmarshalCBOR implements the CBOR Unmarshaler interface for ClaimsMap.
71 | func (cm *ClaimsMap) UnmarshalCBOR(data []byte) error {
72 | return (*key.CoseMap)(cm).UnmarshalCBOR(data)
73 | }
74 |
75 | // Bytesify returns a CBOR-encoded byte slice.
76 | // It returns nil if MarshalCBOR failed.
77 | func (cm ClaimsMap) Bytesify() []byte {
78 | return key.CoseMap(cm).Bytesify()
79 | }
80 |
--------------------------------------------------------------------------------
/cose/sign1_example_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose_test
5 |
6 | import (
7 | "fmt"
8 |
9 | "github.com/ldclabs/cose/cose"
10 | "github.com/ldclabs/cose/iana"
11 | "github.com/ldclabs/cose/key"
12 | _ "github.com/ldclabs/cose/key/ecdsa"
13 | )
14 |
15 | func ExampleSign1Message() {
16 | // load Key
17 | var k key.Key
18 | err := key.UnmarshalCBOR(key.HexBytesify("A60102024231312001215820BAC5B11CAD8F99F9C72B05CF4B9E26D244DC189F745228255A219A86D6A09EFF22582020138BF82DC1B6D562BE0FA54AB7804A3A64B6D72CCFED6B6FB6ED28BBFC117E23582057C92077664146E876760C9520D054AA93C3AFB04E306705DB6090308507B4D3"), &k)
19 | if err != nil {
20 | panic(err)
21 | }
22 |
23 | // k is:
24 | // key.Key{
25 | // iana.KeyParameterKty: iana.KeyTypeEC2,
26 | // iana.KeyParameterKid: []byte("11"),
27 | // iana.EC2KeyParameterCrv: iana.EllipticCurveP_256,
28 | // iana.EC2KeyParameterX: key.Base64Bytesify("usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8"),
29 | // iana.EC2KeyParameterY: key.Base64Bytesify("IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4"),
30 | // iana.EC2KeyParameterD: key.Base64Bytesify("V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"),
31 | // }
32 |
33 | // create a Sign1Message object.
34 | obj := &cose.Sign1Message[[]byte]{
35 | Protected: cose.Headers{
36 | iana.HeaderParameterAlg: iana.AlgorithmES256,
37 | },
38 | Unprotected: cose.Headers{
39 | iana.HeaderParameterKid: k.Kid(),
40 | },
41 | Payload: []byte("This is the content."),
42 | }
43 |
44 | signer, err := k.Signer()
45 | if err != nil {
46 | panic(err)
47 | }
48 |
49 | externalData := key.HexBytesify("11aa22bb33cc44dd55006699")
50 | // sign the message
51 | coseData, err := obj.SignAndEncode(signer, externalData)
52 | if err != nil {
53 | panic(err)
54 | }
55 |
56 | fmt.Printf("COSE(%d bytes): %x...\n", len(coseData), coseData[:32])
57 | // COSE(98 bytes): d28443a10126a10442313154546869732069732074686520636f6e74656e742e...
58 |
59 | verifier, err := k.Verifier()
60 | if err != nil {
61 | panic(err)
62 | }
63 |
64 | obj2, err := cose.VerifySign1Message[[]byte](verifier, coseData, externalData)
65 | if err != nil {
66 | panic(err)
67 | }
68 | fmt.Printf("Payload: %s\n", string(obj2.Payload))
69 | // Payload: This is the content.
70 |
71 | // verify with a different external data should fail
72 | obj3, err := cose.VerifySign1Message[[]byte](verifier, coseData, []byte("some other external data."))
73 | if obj3 != nil {
74 | panic("should be nil")
75 | }
76 | fmt.Println(err)
77 | // cose/key/ecdsa: Verifier.Verify: invalid signature
78 |
79 | // Output:
80 | // COSE(98 bytes): d28443a10126a10442313154546869732069732074686520636f6e74656e742e...
81 | // Payload: This is the content.
82 | // cose/key/ecdsa: Verifier.Verify: invalid signature
83 | }
84 |
--------------------------------------------------------------------------------
/key/keyset_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key_test
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/stretchr/testify/assert"
10 | "github.com/stretchr/testify/require"
11 |
12 | "github.com/ldclabs/cose/iana"
13 | "github.com/ldclabs/cose/key"
14 | _ "github.com/ldclabs/cose/key/ecdsa"
15 | "github.com/ldclabs/cose/key/ed25519"
16 | _ "github.com/ldclabs/cose/key/hmac"
17 | )
18 |
19 | func TestKeySet(t *testing.T) {
20 | assert := assert.New(t)
21 |
22 | k1 := key.Key{
23 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
24 | iana.KeyParameterKid: key.HexBytesify("53796d6d6574726963323536"),
25 | iana.KeyParameterAlg: iana.AlgorithmHMAC_256_64,
26 | iana.SymmetricKeyParameterK: key.HexBytesify("403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388"),
27 | }
28 |
29 | k2 := key.Key{
30 | iana.KeyParameterKty: iana.KeyTypeEC2,
31 | iana.KeyParameterKid: key.HexBytesify("4173796d6d65747269634543445341323536"),
32 | iana.KeyParameterAlg: iana.AlgorithmES256,
33 | iana.EC2KeyParameterCrv: iana.EllipticCurveP_256,
34 | iana.EC2KeyParameterX: key.HexBytesify("143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f"),
35 | iana.EC2KeyParameterY: key.HexBytesify("60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168db9529971a36e7b9"),
36 | iana.EC2KeyParameterD: key.HexBytesify("6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19"),
37 | }
38 |
39 | ks := key.KeySet{k1, k2}
40 |
41 | k := ks.Lookup([]byte{1, 2, 3})
42 | assert.Nil(k)
43 |
44 | k = ks.Lookup(k1.Kid())
45 | assert.Equal(key.MustMarshalCBOR(k1), key.MustMarshalCBOR(k))
46 |
47 | k = ks.Lookup(k2.Kid())
48 | assert.Equal(key.MustMarshalCBOR(k2), key.MustMarshalCBOR(k))
49 |
50 | _, err := ks.Signers()
51 | assert.ErrorContains(err, "kty(4)_alg(4) is not registered")
52 |
53 | _, err = ks.Verifiers()
54 | assert.ErrorContains(err, "kty(4)_alg(4) is not registered")
55 |
56 | k1, err = ed25519.GenerateKey()
57 | require.NoError(t, err)
58 | ks[0] = k1
59 |
60 | ks2 := key.KeySet{}
61 | err = key.UnmarshalCBOR(key.MustMarshalCBOR(ks), &ks2)
62 | require.NoError(t, err, "Marshal and Unmarshal KeySet should work")
63 | require.Equal(t, 2, len(ks2))
64 |
65 | k = ks2.Lookup(k1.Kid())
66 | assert.Equal(key.MustMarshalCBOR(k1), key.MustMarshalCBOR(k))
67 |
68 | k = ks2.Lookup(k2.Kid())
69 | assert.Equal(key.MustMarshalCBOR(k2), key.MustMarshalCBOR(k))
70 |
71 | assert.Equal(key.MustMarshalCBOR(ks), key.MustMarshalCBOR(ks2))
72 |
73 | signers, err := ks.Signers()
74 | require.NoError(t, err)
75 | assert.Equal(2, len(signers))
76 | assert.Equal(k1.Kid(), signers[0].Key().Kid())
77 | assert.Equal(k2.Kid(), signers[1].Key().Kid())
78 |
79 | verifiers, err := ks.Verifiers()
80 | require.NoError(t, err)
81 | assert.Equal(2, len(verifiers))
82 | assert.Equal(k1.Kid(), verifiers[0].Key().Kid())
83 | assert.Equal(k2.Kid(), verifiers[1].Key().Kid())
84 | }
85 |
--------------------------------------------------------------------------------
/key/bytestr.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "encoding/base64"
8 | "encoding/hex"
9 | "errors"
10 | "strings"
11 |
12 | "golang.org/x/crypto/sha3"
13 | )
14 |
15 | // ByteStr represents a byte string.
16 | type ByteStr []byte
17 |
18 | // String returns the hex string representation of the byte string.
19 | func (bstr ByteStr) String() string {
20 | return hex.EncodeToString(bstr)
21 | }
22 |
23 | // Base64 returns the raw base64url string representation of the byte string.
24 | func (bstr ByteStr) Base64() string {
25 | return base64.RawURLEncoding.EncodeToString(bstr)
26 | }
27 |
28 | // MarshalText implements encoding/text interface for ByteStr.
29 | func (bstr ByteStr) MarshalText() ([]byte, error) {
30 | return []byte(hex.EncodeToString(bstr)), nil
31 | }
32 |
33 | // UnmarshalText implements encoding/text interface for ByteStr.
34 | func (bstr *ByteStr) UnmarshalText(text []byte) error {
35 | if bstr == nil {
36 | return errors.New("cose/key: ByteStr: UnmarshalText on nil pointer")
37 | }
38 |
39 | data, err := hex.DecodeString(string(text))
40 | if err == nil {
41 | *bstr = append((*bstr)[0:0], data...)
42 | }
43 | return err
44 | }
45 |
46 | // MarshalJSON implements encoding/json interface for ByteStr.
47 | func (bstr ByteStr) MarshalJSON() ([]byte, error) {
48 | return []byte(`"` + hex.EncodeToString(bstr) + `"`), nil
49 | }
50 |
51 | // UnmarshalJSON implements encoding/json interface for ByteStr.
52 | func (bstr *ByteStr) UnmarshalJSON(data []byte) error {
53 | if bstr == nil {
54 | return errors.New("cose/key: ByteStr: UnmarshalJSON on nil pointer")
55 | }
56 | s := string(data)
57 | if s == "null" {
58 | return nil
59 | }
60 |
61 | if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
62 | return errors.New("cose/key: ByteStr: UnmarshalJSON with invalid data")
63 | }
64 | data, err := hex.DecodeString(string(data[1 : len(data)-1]))
65 | if err == nil {
66 | *bstr = append((*bstr)[0:0], data...)
67 | }
68 | return err
69 | }
70 |
71 | // HexBytesify converts a hex string to []byte.
72 | // It returns nil if the string is not a valid hex string.
73 | func HexBytesify(h string) []byte {
74 | b, err := hex.DecodeString(h)
75 | if err != nil {
76 | return nil
77 | }
78 | return b
79 | }
80 |
81 | // Base64Bytesify converts a base64url string to []byte.
82 | // It returns nil if the string is not a valid base64url string.
83 | func Base64Bytesify(s string) []byte {
84 | enc := base64.RawURLEncoding
85 | if strings.Contains(s, "=") {
86 | enc = base64.URLEncoding
87 | }
88 |
89 | b, err := enc.DecodeString(s)
90 | if err != nil {
91 | return nil
92 | }
93 | return b
94 | }
95 |
96 | // SumKid returns a 20 bytes kid with given data.
97 | func SumKid(data []byte) ByteStr {
98 | sum := sha3.Sum256(data)
99 | id := make([]byte, 20)
100 | copy(id, sum[:])
101 | return id
102 | }
103 |
104 | // UnwrapBytes returns the data if err is nil, otherwise it panics.
105 | func UnwrapBytes(data []byte, err error) []byte {
106 | if err != nil {
107 | panic(err)
108 | }
109 | return data
110 | }
111 |
--------------------------------------------------------------------------------
/cose/header.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | // Package cose implements CBOR Object Signing and Encryption (COSE) as defined in RFC9052.
5 | // https://datatracker.ietf.org/doc/html/rfc9052.
6 | package cose
7 |
8 | import "github.com/ldclabs/cose/key"
9 |
10 | // Headers represents a COSE Generic_Headers structure.
11 | type Headers key.CoseMap
12 |
13 | // Has returns true if the Headers has the given parameter.
14 | func (h Headers) Has(p any) bool {
15 | return key.CoseMap(h).Has(p)
16 | }
17 |
18 | // Get returns the value of the given parameter.
19 | func (h Headers) Get(p any) any {
20 | return key.CoseMap(h).Get(p)
21 | }
22 |
23 | // Set sets the parameter. parameter key should be int or string.
24 | func (h Headers) Set(p, value any) error {
25 | return key.CoseMap(h).Set(p, value)
26 | }
27 |
28 | // GetBool returns the value of the given parameter as a bool, or a error.
29 | func (h Headers) GetBool(p any) (bool, error) {
30 | return key.CoseMap(h).GetBool(p)
31 | }
32 |
33 | // GetInt returns the value of the given parameter as a int, or a error.
34 | func (h Headers) GetInt(p any) (int, error) {
35 | return key.CoseMap(h).GetInt(p)
36 | }
37 |
38 | // GetInt64 returns the value of the given parameter as a int64, or a error.
39 | func (h Headers) GetInt64(p any) (int64, error) {
40 | return key.CoseMap(h).GetInt64(p)
41 | }
42 |
43 | // GetUint64 returns the value of the given parameter as a uint64, or a error.
44 | func (h Headers) GetUint64(p any) (uint64, error) {
45 | return key.CoseMap(h).GetUint64(p)
46 | }
47 |
48 | // GetBytes returns the value of the given parameter as a slice of bytes, or a error.
49 | func (h Headers) GetBytes(p any) ([]byte, error) {
50 | return key.CoseMap(h).GetBytes(p)
51 | }
52 |
53 | // GetString returns the value of the given parameter as a string, or a error.
54 | func (h Headers) GetString(p any) (string, error) {
55 | return key.CoseMap(h).GetString(p)
56 | }
57 |
58 | // GetMap returns the value of the given parameter as a key.CoseMap, or a error.
59 | func (h Headers) GetMap(p any) (key.CoseMap, error) {
60 | return key.CoseMap(h).GetMap(p)
61 | }
62 |
63 | // MarshalCBOR implements the CBOR Marshaler interface for Headers.
64 | func (h Headers) MarshalCBOR() ([]byte, error) {
65 | return key.CoseMap(h).MarshalCBOR()
66 | }
67 |
68 | // UnmarshalCBOR implements the CBOR Unmarshaler interface for Headers.
69 | func (h *Headers) UnmarshalCBOR(data []byte) error {
70 | return (*key.CoseMap)(h).UnmarshalCBOR(data)
71 | }
72 |
73 | // Bytesify returns a CBOR-encoded byte slice.
74 | // It returns nil if MarshalCBOR failed.
75 | func (h Headers) Bytesify() []byte {
76 | return key.CoseMap(h).Bytesify()
77 | }
78 |
79 | // Bytesify returns a CBOR-encoded byte slice.
80 | // It returns ([]byte{}, nil) if Headers is nil or empty.
81 | func (h Headers) Bytes() ([]byte, error) {
82 | if len(h) == 0 {
83 | return []byte{}, nil
84 | }
85 | return h.MarshalCBOR()
86 | }
87 |
88 | // HeadersFromBytes decode bytes into a Headers.
89 | // It returns (Headers{}, nil) if data is nil or empty.
90 | func HeadersFromBytes(data []byte) (Headers, error) {
91 | h := Headers{}
92 | if len(data) > 0 {
93 | if err := h.UnmarshalCBOR(data); err != nil {
94 | return nil, err
95 | }
96 | }
97 |
98 | return h, nil
99 | }
100 |
--------------------------------------------------------------------------------
/iana/claim.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package iana
5 |
6 | // CBOR Web Token (CWT) Claims
7 | // From IANA registry https://www.iana.org/assignments/cwt/cwt.xhtml
8 | // as of 2022-12-19.
9 | const (
10 | // Health certificate ("hcert": map).
11 | CWTClaimHCert = -260
12 | // Challenge nonce ("EUPHNonce": bstr).
13 | CWTClaimEUPHNonce = -259
14 | // Signing prefix for multi-app restricted operating environment ("EATMAROEPrefix": bstr).
15 | CWTClaimEATMAROEPrefix = -258
16 | // FIDO Device Onboarding EAT ("EAT-FDO": array).
17 | CWTClaimEATFDO = -257
18 |
19 | // Reserved value.
20 | CWTClaimReserved = 0
21 |
22 | // Issuer ("iss": tstr).
23 | CWTClaimIss = 1
24 | // Subject ("sub": tstr)
25 | CWTClaimSub = 2
26 | // Audience ("aud": tstr)
27 | CWTClaimAud = 3
28 | // Expiration Time, as seconds since UNIX epoch ("exp": int/float)
29 | CWTClaimExp = 4
30 | // Not Before, as seconds since UNIX epoch ("nbf": int/float)
31 | CWTClaimNbf = 5
32 | // Issued at, as seconds since UNIX epoch ("iat": int/float)
33 | CWTClaimIat = 6
34 | // CWT ID ("cti": bstr)
35 | CWTClaimCti = 7
36 | // Confirmation ("cnf": map)
37 | CWTClaimCnf = 8
38 | // Scope of an access token ("scope": bstr/tstr)
39 | CWTClaimScope = 9
40 | // Nonce ("nonce": bstr) TEMPORARY, expires 2023-03-23
41 | CWTClaimNonce = 10
42 |
43 | // The ACE profile a token is supposed to be used with ("ace_profile": int)
44 | CWTClaimACEProfile = 38
45 | // The client-nonce sent to the AS by the RS via the client ("cnonce": bstr)
46 | CWTClaimCNonce = 39
47 | // The expiration time of a token measured from when it was received at the RS in seconds ("exi": int)
48 | CWTClaimExi = 40
49 |
50 | // The Universal Entity ID ("ueid": bstr) TEMPORARY, expires 2023-03-23
51 | CWTClaimUEID = 256
52 | // Hardware OEM ID ("sueids": map) TEMPORARY, expires 2023-03-23
53 | CWTClaimSUEIDs = 257
54 | // Hardware OEM ID ("oemid": bstr/int) TEMPORARY, expires 2023-03-23
55 | CWTClaimOEMID = 258
56 | // Model identifier for hardware ("hwmodel": bstr) TEMPORARY, expires 2023-03-23
57 | CWTClaimHWModel = 259
58 | // Hardware Version Identifier ("hwversion": array) TEMPORARY, expires 2023-03-23
59 | CWTClaimHWVersion = 260
60 | // Indicate whether the boot was secure ("secboot": bool) TEMPORARY, expires 2023-03-23
61 | CWTClaimSecureBoot = 262
62 | // Indicate status of debug facilities ("dbgstat": int) TEMPORARY, expires 2023-03-23
63 | CWTClaimDebugStatus = 263
64 | // The geographic location ("location": map) TEMPORARY, expires 2023-03-23
65 | CWTClaimLocation = 264
66 | // Indicates the EAT profile followed ("eat_profile": uri/oid) TEMPORARY, expires 2023-03-23
67 | CWTClaimProfile = 265
68 | // The section containing submodules ("submods": map) TEMPORARY, expires 2023-03-23
69 | CWTClaimSubmodules = 266
70 |
71 | // Reference
72 | // PSA Client ID (N/A: signed integer)
73 | CWTClaimPSAClientID = 2394
74 | // PSA Security Lifecycle (N/A: unsigned integer)
75 | CWTClaimPSASecurityLifecycle = 2395
76 | // PSA Implementation ID (N/A: bstr)
77 | CWTClaimPSAImplementationID = 2396
78 | // PSA Boot Seed (N/A: bstr)
79 | CWTClaimPSABootSeed = 2397
80 | // PSA Certification Reference (N/A: tstr)
81 | CWTClaimPSACertificationReference = 2398
82 | // PSA Software Components (N/A: array)
83 | CWTClaimPSASoftwareComponents = 2399
84 | // PSA Verification Service Indicator (N/A: tstr)
85 | CWTClaimPSAVerificationServiceIndicator = 2400
86 | )
87 |
--------------------------------------------------------------------------------
/cwt/claims_map_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cwt
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/ldclabs/cose/iana"
10 | "github.com/ldclabs/cose/key"
11 | _ "github.com/ldclabs/cose/key/ecdsa"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func TestClaimsMap(t *testing.T) {
17 | assert := assert.New(t)
18 |
19 | cm := ClaimsMap{}
20 | assert.False(cm.Has(iana.CWTClaimReserved))
21 |
22 | cm = ClaimsMap{iana.CWTClaimReserved: true}
23 | assert.True(cm.Has(iana.CWTClaimReserved))
24 |
25 | vbool, err := cm.GetBool(iana.CWTClaimReserved)
26 | assert.NoError(err)
27 | assert.Equal(true, vbool)
28 |
29 | cm = ClaimsMap{iana.CWTClaimReserved: 1}
30 | vint, err := cm.GetInt(iana.CWTClaimReserved)
31 | assert.NoError(err)
32 | assert.Equal(1, vint)
33 |
34 | cm = ClaimsMap{iana.CWTClaimReserved: 1}
35 | vint64, err := cm.GetInt64(iana.CWTClaimReserved)
36 | assert.NoError(err)
37 | assert.Equal(int64(1), vint64)
38 |
39 | cm = ClaimsMap{iana.CWTClaimReserved: 1}
40 | vuint64, err := cm.GetUint64(iana.CWTClaimReserved)
41 | assert.NoError(err)
42 | assert.Equal(uint64(1), vuint64)
43 |
44 | cm = ClaimsMap{iana.CWTClaimReserved: []byte{1, 2, 3, 4}}
45 | vbytes, err := cm.GetBytes(iana.CWTClaimReserved)
46 | assert.NoError(err)
47 | assert.Equal([]byte{1, 2, 3, 4}, vbytes)
48 |
49 | cm = ClaimsMap{iana.CWTClaimReserved: "hello"}
50 | vstr, err := cm.GetString(iana.CWTClaimReserved)
51 | assert.NoError(err)
52 | assert.Equal("hello", vstr)
53 |
54 | var p *ClaimsMap
55 | assert.ErrorContains(p.UnmarshalCBOR([]byte{0xa0}), "nil CoseMap")
56 |
57 | cm = ClaimsMap{
58 | iana.CWTClaimIss: "issuer",
59 | iana.CWTClaimSub: "subject",
60 | iana.CWTClaimAud: "audience",
61 | }
62 |
63 | data, err := key.MarshalCBOR(cm)
64 | assert.NoError(err)
65 |
66 | var cm2 ClaimsMap
67 | assert.NoError(cm2.UnmarshalCBOR(data))
68 | assert.Equal(data, cm2.Bytesify())
69 |
70 | var cm3 Claims
71 | assert.NoError(key.UnmarshalCBOR(data, &cm3))
72 | assert.Equal(data, cm3.Bytesify())
73 |
74 | cm = ClaimsMap{
75 | iana.CWTClaimIss: "issuer",
76 | iana.CWTClaimSub: "subject",
77 | iana.CWTClaimAud: []string{"audience"},
78 | iana.CWTClaimExp: 1000,
79 | iana.CWTClaimCti: []byte{1, 2, 3, 4},
80 | iana.CWTClaimEUPHNonce: []byte{5, 6, 7, 8},
81 | iana.CWTClaimHCert: key.Key{
82 | iana.KeyParameterKty: iana.KeyTypeEC2,
83 | iana.KeyParameterKid: []byte("11"),
84 | iana.EC2KeyParameterCrv: iana.EllipticCurveP_256,
85 | iana.EC2KeyParameterX: key.Base64Bytesify("usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8"),
86 | iana.EC2KeyParameterY: key.Base64Bytesify("IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4"),
87 | iana.EC2KeyParameterD: key.Base64Bytesify("V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"),
88 | },
89 | }
90 |
91 | k, err := cm.GetMap(iana.CWTClaimHCert)
92 | assert.NoError(err)
93 | x, err := k.GetBytes(iana.EC2KeyParameterX)
94 | assert.NoError(err)
95 | assert.Equal(key.Base64Bytesify("usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8"), x)
96 |
97 | data, err = cm.MarshalCBOR()
98 | assert.NoError(err)
99 |
100 | var cm4 ClaimsMap
101 | assert.NoError(cm4.UnmarshalCBOR(data))
102 | assert.Equal(data, cm4.Bytesify())
103 | nonce, _ := cm4.GetBytes(iana.CWTClaimEUPHNonce)
104 | assert.Equal([]byte{5, 6, 7, 8}, nonce)
105 |
106 | k, err = cm4.GetMap(iana.CWTClaimHCert)
107 | assert.NoError(err)
108 | x, err = k.GetBytes(iana.EC2KeyParameterX)
109 | assert.NoError(err)
110 | assert.Equal(key.Base64Bytesify("usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8"), x)
111 | }
112 |
--------------------------------------------------------------------------------
/cose/sign_example_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose_test
5 |
6 | import (
7 | "fmt"
8 |
9 | "github.com/fxamacker/cbor/v2"
10 | "github.com/ldclabs/cose/cose"
11 | "github.com/ldclabs/cose/key"
12 | _ "github.com/ldclabs/cose/key/ecdsa"
13 | )
14 |
15 | func ExampleSignMessage() {
16 | // load KeySet
17 | ks := key.KeySet{}
18 | err := key.UnmarshalCBOR(key.HexBytesify("82a60102024231312001215820bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff22582020138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e23582057c92077664146e876760c9520d054aa93c3afb04e306705db6090308507b4d3a6010202581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65200321584172992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad22584201dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d947523584200085138ddabf5ca975f5860f91a08e91d6d5f9a76ad4018766a476680b55cd339e8ab6c72b5facdb2a2a50ac25bd086647dd3e2e6e99e84ca2c3609fdf177feb26d"), &ks)
19 | if err != nil {
20 | panic(err)
21 | }
22 |
23 | // ks is:
24 | // key.KeySet{
25 | // key.Key{
26 | // iana.KeyParameterKty: iana.KeyTypeEC2,
27 | // iana.KeyParameterKid: []byte("11"),
28 | // iana.EC2KeyParameterCrv: iana.EllipticCurveP_256,
29 | // iana.EC2KeyParameterX: key.Base64Bytesify("usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8"),
30 | // iana.EC2KeyParameterY: key.Base64Bytesify("IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4"),
31 | // iana.EC2KeyParameterD: key.Base64Bytesify("V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"),
32 | // },
33 | // key.Key{
34 | // iana.KeyParameterKty: iana.KeyTypeEC2,
35 | // iana.KeyParameterKid: []byte("bilbo.baggins@hobbiton.example"),
36 | // iana.EC2KeyParameterCrv: iana.EllipticCurveP_521,
37 | // iana.EC2KeyParameterX: key.Base64Bytesify("cpkss6wI7PPlxj3t7A1RqMH3nvL4L5Tzxze_XeeYZnHqxiX-gle70DlGRMqqOq-PJ6RYX7vK0PJFdiAIXlyPQq0"),
38 | // iana.EC2KeyParameterY: key.Base64Bytesify("AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1"),
39 | // iana.EC2KeyParameterD: key.Base64Bytesify("AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zbKipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt"),
40 | // },
41 | // }
42 |
43 | // create a SignMessage object.
44 | obj := &cose.SignMessage[[]byte]{
45 | Payload: []byte("This is the content."),
46 | }
47 |
48 | // get the signers from the KeySet
49 | signers, err := ks.Signers()
50 | if err != nil {
51 | panic(err)
52 | }
53 |
54 | // sign the message
55 | err = obj.WithSign(signers, []byte("some external data."))
56 | if err != nil {
57 | panic(err)
58 | }
59 |
60 | coseData, err := cbor.Marshal(obj)
61 | if err != nil {
62 | panic(err)
63 | }
64 |
65 | fmt.Printf("COSE(%d bytes): %x...\n", len(coseData), coseData[:32])
66 | // COSE(277 bytes): d8628440a054546869732069732074686520636f6e74656e742e828343a10126...
67 |
68 | verifiers, err := ks.Verifiers()
69 | if err != nil {
70 | panic(err)
71 | }
72 | obj2, err := cose.VerifySignMessage[[]byte](verifiers, coseData, []byte("some external data."))
73 | if err != nil {
74 | panic(err)
75 | }
76 | fmt.Printf("Payload: %s\n", string(obj2.Payload))
77 | // Payload: This is the content.
78 |
79 | // verify with a different external data should fail
80 | obj3, err := cose.VerifySignMessage[[]byte](verifiers, coseData, []byte("some other external data."))
81 | if obj3 != nil {
82 | panic("should be nil")
83 | }
84 | fmt.Println(err)
85 | // cose/key/ecdsa: Verifier.Verify: invalid signature
86 |
87 | // Output:
88 | // COSE(277 bytes): d8628440a054546869732069732074686520636f6e74656e742e828343a10126...
89 | // Payload: This is the content.
90 | // cose/key/ecdsa: Verifier.Verify: invalid signature
91 | }
92 |
--------------------------------------------------------------------------------
/cose/encrypt_example_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose_test
5 |
6 | import (
7 | "fmt"
8 |
9 | "github.com/ldclabs/cose/cose"
10 | "github.com/ldclabs/cose/iana"
11 | "github.com/ldclabs/cose/key"
12 | "github.com/ldclabs/cose/key/aesgcm"
13 | "github.com/ldclabs/cose/key/ecdh"
14 | "github.com/ldclabs/cose/key/ecdsa"
15 | "github.com/ldclabs/cose/key/hkdf"
16 | )
17 |
18 | func ExampleEncryptMessage() {
19 | keyR := key.Key{
20 | iana.KeyParameterKty: iana.KeyTypeEC2,
21 | iana.KeyParameterKid: []byte("meriadoc.brandybuck@buckland.example"),
22 | iana.EC2KeyParameterCrv: iana.EllipticCurveP_256,
23 | iana.EC2KeyParameterX: key.Base64Bytesify("Ze2loSV3wrroKUN_4zhwGhCqo3Xhu1td4QjeQ5wIVR0"),
24 | iana.EC2KeyParameterY: key.Base64Bytesify("HlLtdXARY_f55A3fnzQbPcm6hgr34Mp8p-nuzQCE0Zw"),
25 | iana.EC2KeyParameterD: key.Base64Bytesify("r_kHyZ-a06rmxM3yESK84r1otSg-aQcVStkRhA-iCM8"),
26 | }
27 |
28 | keyS := key.Key{
29 | iana.KeyParameterKty: iana.KeyTypeEC2,
30 | iana.KeyParameterKid: []byte("meriadoc.brandybuck@buckland.example"),
31 | iana.EC2KeyParameterCrv: iana.EllipticCurveP_256,
32 | iana.EC2KeyParameterX: key.Base64Bytesify("mPUKT_bAWGHIhg0TpjjqVsP1rXWQu_vwVOHHtNkdYoA"),
33 | iana.EC2KeyParameterY: key.Base64Bytesify("8BQAsImGeAS46fyWw5MhYfGTT0IjBpFw2SS34Dv4Irs"),
34 | }
35 |
36 | ecdher, err := ecdh.NewECDHer(keyR)
37 | if err != nil {
38 | panic(err)
39 | }
40 |
41 | // shared secret by ECDH-ES
42 | secret, err := ecdher.ECDH(keyS)
43 | if err != nil {
44 | panic(err)
45 | }
46 |
47 | // derive the key from the shared secret
48 | kdfContext := cose.KDFContext{
49 | AlgorithmID: iana.AlgorithmA128GCM,
50 | SuppPubInfo: cose.SuppPubInfo{
51 | KeyDataLength: 128,
52 | Protected: cose.Headers{
53 | iana.HeaderParameterAlg: iana.AlgorithmECDH_ES_HKDF_256,
54 | },
55 | },
56 | }
57 | ctxData, err := key.MarshalCBOR(kdfContext)
58 | if err != nil {
59 | panic(err)
60 | }
61 | cek, err := hkdf.HKDF256(secret, nil, ctxData, 128/8)
62 | if err != nil {
63 | panic(err)
64 | }
65 | fmt.Printf("Derived key: %X\n", cek)
66 | // Derived key: 56074D506729CA40C4B4FE50C6439893
67 |
68 | gcmkey, err := aesgcm.KeyFrom(iana.AlgorithmA128GCM, cek)
69 | if err != nil {
70 | panic(err)
71 | }
72 |
73 | encryptor, err := gcmkey.Encryptor()
74 | if err != nil {
75 | panic(err)
76 | }
77 |
78 | obj := &cose.EncryptMessage[[]byte]{
79 | Payload: []byte("Encryption example for spec - Direct ECDH"),
80 | }
81 |
82 | err = obj.Encrypt(encryptor, nil)
83 | if err != nil {
84 | panic(err)
85 | }
86 |
87 | ck, err := ecdsa.ToCompressedKey(keyS)
88 | if err != nil {
89 | panic(err)
90 | }
91 | rp := &cose.Recipient{
92 | Protected: cose.Headers{iana.HeaderParameterAlg: iana.AlgorithmECDH_ES_HKDF_256},
93 | Unprotected: cose.Headers{
94 | iana.HeaderAlgorithmParameterEphemeralKey: ck,
95 | iana.HeaderParameterKid: []byte("meriadoc.brandybuck@buckland.example"),
96 | },
97 | }
98 | err = obj.AddRecipient(rp)
99 | if err != nil {
100 | panic(err)
101 | }
102 |
103 | output, err := key.MarshalCBOR(obj)
104 | if err != nil {
105 | panic(err)
106 | }
107 | fmt.Printf("COSE(%d bytes): %x...\n", len(output), output[:20])
108 | // COSE(194 bytes): d8608443a10101a20454adbfc99d99420ac0920c...
109 |
110 | var obj2 cose.EncryptMessage[[]byte]
111 | err = key.UnmarshalCBOR(output, &obj2)
112 | if err != nil {
113 | panic(err)
114 | }
115 | err = obj2.Decrypt(encryptor, nil)
116 | if err != nil {
117 | panic(err)
118 | }
119 | fmt.Printf("Decrypt Payload: %q\n", string(obj2.Payload))
120 | // Decrypt Payload: "Encryption example for spec - Direct ECDH"
121 |
122 | // Output:
123 | // Derived key: 56074D506729CA40C4B4FE50C6439893
124 | // COSE(194 bytes): d8608443a10101a20454adbfc99d99420ac0920c...
125 | // Decrypt Payload: "Encryption example for spec - Direct ECDH"
126 | }
127 |
--------------------------------------------------------------------------------
/cose/header_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/ldclabs/cose/iana"
10 | "github.com/ldclabs/cose/key"
11 | _ "github.com/ldclabs/cose/key/ecdsa"
12 |
13 | "github.com/stretchr/testify/assert"
14 | )
15 |
16 | func TestHeaders(t *testing.T) {
17 | assert := assert.New(t)
18 |
19 | h := Headers{}
20 | assert.False(h.Has(iana.HeaderParameterReserved))
21 |
22 | h = Headers{iana.HeaderParameterReserved: true}
23 | assert.True(h.Has(iana.HeaderParameterReserved))
24 |
25 | vbool, err := h.GetBool(iana.HeaderParameterReserved)
26 | assert.NoError(err)
27 | assert.Equal(true, vbool)
28 |
29 | h = Headers{iana.HeaderParameterReserved: 1}
30 | vint, err := h.GetInt(iana.HeaderParameterReserved)
31 | assert.NoError(err)
32 | assert.Equal(1, vint)
33 |
34 | h = Headers{iana.HeaderParameterReserved: 1}
35 | vint64, err := h.GetInt64(iana.HeaderParameterReserved)
36 | assert.NoError(err)
37 | assert.Equal(int64(1), vint64)
38 |
39 | h = Headers{iana.HeaderParameterReserved: 1}
40 | vuint64, err := h.GetUint64(iana.HeaderParameterReserved)
41 | assert.NoError(err)
42 | assert.Equal(uint64(1), vuint64)
43 |
44 | h = Headers{iana.HeaderParameterReserved: []byte{1, 2, 3, 4}}
45 | vbytes, err := h.GetBytes(iana.HeaderParameterReserved)
46 | assert.NoError(err)
47 | assert.Equal([]byte{1, 2, 3, 4}, vbytes)
48 |
49 | h = Headers{iana.HeaderParameterReserved: "hello"}
50 | vstr, err := h.GetString(iana.HeaderParameterReserved)
51 | assert.NoError(err)
52 | assert.Equal("hello", vstr)
53 |
54 | var p *Headers
55 | assert.ErrorContains(p.UnmarshalCBOR([]byte{0xa0}), "nil CoseMap")
56 |
57 | h = Headers{
58 | iana.HeaderParameterAlg: iana.AlgorithmECDH_ES_HKDF_256,
59 | iana.HeaderParameterKid: []byte{1, 2, 3, 4},
60 | }
61 |
62 | data, err := key.MarshalCBOR(h)
63 | assert.NoError(err)
64 |
65 | var h2 Headers
66 | assert.NoError(h2.UnmarshalCBOR(data))
67 | assert.Equal(data, h2.Bytesify())
68 |
69 | var h3 Headers
70 | assert.NoError(key.UnmarshalCBOR(data, &h3))
71 | assert.Equal(data, h3.Bytesify())
72 |
73 | h = Headers{
74 | iana.HeaderAlgorithmParameterEphemeralKey: key.Key{
75 | iana.KeyParameterKty: iana.KeyTypeEC2,
76 | iana.EC2KeyParameterCrv: iana.EllipticCurveP_521,
77 | iana.EC2KeyParameterX: key.HexBytesify("0043B12669ACAC3FD27898FFBA0BCD2E6C366D53BC4DB71F909A759304ACFB5E18CDC7BA0B13FF8C7636271A6924B1AC63C02688075B55EF2D613574E7DC242F79C3"),
78 | iana.EC2KeyParameterY: true,
79 | },
80 | iana.HeaderParameterKid: []byte("bilbo.baggins@hobbiton.example"),
81 | }
82 |
83 | data, err = h.MarshalCBOR()
84 | assert.NoError(err)
85 |
86 | var h4 Headers
87 | assert.NoError(h4.UnmarshalCBOR(data))
88 | assert.Equal(data, h4.Bytesify())
89 | kid, _ := h4.GetBytes(iana.HeaderParameterKid)
90 | assert.Equal([]byte("bilbo.baggins@hobbiton.example"), kid)
91 | im, err := h4.GetMap(iana.HeaderAlgorithmParameterEphemeralKey)
92 | assert.NoError(err)
93 | x, err := im.GetBytes(iana.EC2KeyParameterX)
94 | assert.NoError(err)
95 | assert.Equal(key.HexBytesify("0043B12669ACAC3FD27898FFBA0BCD2E6C366D53BC4DB71F909A759304ACFB5E18CDC7BA0B13FF8C7636271A6924B1AC63C02688075B55EF2D613574E7DC242F79C3"), x)
96 | }
97 |
98 | func TestHeaderBytes(t *testing.T) {
99 | assert := assert.New(t)
100 |
101 | var h Headers
102 | data, err := h.Bytes()
103 | assert.NoError(err)
104 | assert.Equal([]byte{}, data)
105 |
106 | h = Headers{}
107 | data, err = h.Bytes()
108 | assert.NoError(err)
109 | assert.Equal([]byte{}, data)
110 |
111 | h = Headers{iana.HeaderParameterReserved: true}
112 | data, err = h.Bytes()
113 | assert.NoError(err)
114 | assert.Equal(data, key.MustMarshalCBOR(h))
115 |
116 | h, err = HeadersFromBytes(nil)
117 | assert.NoError(err)
118 | assert.Equal(Headers{}, h)
119 |
120 | h, err = HeadersFromBytes([]byte{})
121 | assert.NoError(err)
122 | assert.Equal(Headers{}, h)
123 |
124 | h, err = HeadersFromBytes(data)
125 | assert.NoError(err)
126 | assert.NotEqual(Headers{}, h)
127 | assert.True(h.Has(iana.HeaderParameterReserved))
128 |
129 | data[1] = 0xf5
130 | _, err = HeadersFromBytes(data)
131 | assert.ErrorContains(err, "invalid key")
132 |
133 | h = Headers{iana.HeaderParameterReserved: func() {}}
134 | _, err = h.Bytes()
135 | assert.ErrorContains(err, "cbor: ")
136 | }
137 |
--------------------------------------------------------------------------------
/cwt/example_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cwt_test
5 |
6 | import (
7 | "fmt"
8 | "time"
9 |
10 | "github.com/ldclabs/cose/cose"
11 | "github.com/ldclabs/cose/cwt"
12 | "github.com/ldclabs/cose/iana"
13 | "github.com/ldclabs/cose/key"
14 | "github.com/ldclabs/cose/key/ecdsa"
15 | "github.com/ldclabs/cose/key/ed25519"
16 | )
17 |
18 | func ExampleClaims() {
19 | // Create a ed25519 signer key
20 | privKey, err := ed25519.GenerateKey()
21 | if err != nil {
22 | panic(err)
23 | }
24 | signer, err := privKey.Signer()
25 | if err != nil {
26 | panic(err)
27 | }
28 |
29 | // Create a verifier key
30 | pubKey, err := ed25519.ToPublicKey(privKey)
31 | if err != nil {
32 | panic(err)
33 | }
34 | verifier, err := pubKey.Verifier()
35 | if err != nil {
36 | panic(err)
37 | }
38 |
39 | // create a claims set
40 | claims := cwt.Claims{
41 | Issuer: "ldc:ca",
42 | Subject: "ldc:chain",
43 | Audience: "ldc:txpool",
44 | Expiration: 1670123579,
45 | CWTID: []byte{1, 2, 3, 4},
46 | }
47 |
48 | // sign with Sign1Message
49 | obj := cose.Sign1Message[cwt.Claims]{Payload: claims}
50 | cwtData, err := obj.SignAndEncode(signer, nil)
51 | if err != nil {
52 | panic(err)
53 | }
54 |
55 | // decode and verify the cwt
56 | obj2, err := cose.VerifySign1Message[cwt.Claims](verifier, cwtData, nil)
57 | if err != nil {
58 | panic(err)
59 | }
60 |
61 | // validate the cwt's claims
62 | validator, err := cwt.NewValidator(&cwt.ValidatorOpts{
63 | ExpectedIssuer: "ldc:ca",
64 | ExpectedAudience: "ldc:txpool",
65 | ClockSkew: time.Minute,
66 | })
67 | if err != nil {
68 | panic(err)
69 | }
70 |
71 | err = validator.Validate(&obj2.Payload)
72 | fmt.Printf("Validate Claims: %v\n", err)
73 | // Validate Claims: cose/cwt: Validator.Validate: token has expired
74 |
75 | cborData, err := key.MarshalCBOR(obj2.Payload)
76 | // cborData, err := cbor.Marshal(myClaims)
77 | if err != nil {
78 | panic(err)
79 | }
80 | fmt.Printf("CBOR(%d bytes): %x\n", len(cborData), cborData)
81 | // CBOR(44 bytes): a501666c64633a636102696c64633a636861696e036a6c64633a7478706f6f6c041a638c103b074401020304
82 |
83 | // Output:
84 | // Validate Claims: cose/cwt: Validator.Validate: token has expired
85 | // CBOR(44 bytes): a501666c64633a636102696c64633a636861696e036a6c64633a7478706f6f6c041a638c103b074401020304
86 | }
87 |
88 | func ExampleClaimsMap() {
89 | // Create a ed25519 signer key
90 | privKey1, err := ed25519.GenerateKey()
91 | if err != nil {
92 | panic(err)
93 | }
94 | privKey2, err := ecdsa.GenerateKey(iana.AlgorithmES256)
95 | if err != nil {
96 | panic(err)
97 | }
98 | ks := key.KeySet{privKey1, privKey2}
99 |
100 | // create a claims set
101 | claims := cwt.ClaimsMap{
102 | iana.CWTClaimIss: "ldc:ca",
103 | iana.CWTClaimSub: "ldc:chain",
104 | iana.CWTClaimAud: "ldc:txpool",
105 | iana.CWTClaimExp: 1670123579,
106 | iana.CWTClaimScope: "read,write",
107 | }
108 |
109 | // Sign the claims
110 | signers, err := ks.Signers()
111 | if err != nil {
112 | panic(err)
113 | }
114 | // sign with SignMessage
115 | obj := cose.SignMessage[cwt.ClaimsMap]{Payload: claims}
116 | cwtData, err := obj.SignAndEncode(signers, nil)
117 | if err != nil {
118 | panic(err)
119 | }
120 |
121 | // decode and verify the cwt
122 | verifiers, err := ks.Verifiers()
123 | if err != nil {
124 | panic(err)
125 | }
126 | obj2, err := cose.VerifySignMessage[cwt.ClaimsMap](verifiers, cwtData, nil)
127 | if err != nil {
128 | panic(err)
129 | }
130 |
131 | // Validate the claims
132 | validator, err := cwt.NewValidator(&cwt.ValidatorOpts{
133 | ExpectedIssuer: "ldc:ca",
134 | ExpectedAudience: "ldc:txpool",
135 | ClockSkew: time.Minute,
136 | })
137 | if err != nil {
138 | panic(err)
139 | }
140 |
141 | err = validator.ValidateMap(obj2.Payload)
142 | fmt.Printf("Validate Claims: %v\n", err)
143 | // Validate Claims: cose/cwt: Validator.Validate: token has expired
144 |
145 | cborData, err := key.MarshalCBOR(obj2.Payload)
146 | // cborData, err := cbor.Marshal(myClaims)
147 | if err != nil {
148 | panic(err)
149 | }
150 | fmt.Printf("CBOR(%d bytes): %x\n", len(cborData), cborData)
151 | // CBOR(50 bytes): a501666c64633a636102696c64633a636861696e036a6c64633a7478706f6f6c041a638c103b096a726561642c7772697465
152 |
153 | // Output:
154 | // Validate Claims: cose/cwt: Validator.Validate: token has expired
155 | // CBOR(50 bytes): a501666c64633a636102696c64633a636861696e036a6c64633a7478706f6f6c041a638c103b096a726561642c7772697465
156 | }
157 |
--------------------------------------------------------------------------------
/iana/header.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package iana
5 |
6 | // IANA-registered COSE header parameters.
7 | //
8 | // From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#header-parameters
9 | // as of 2022-12-19.
10 | const (
11 | // Reserved
12 | HeaderParameterReserved = 0
13 | // Cryptographic algorithm to use
14 | //
15 | // Associated value of type int / tstr
16 | //
17 | // It is a protected header parameter https://datatracker.ietf.org/doc/html/rfc9052#name-common-cose-header-paramete
18 | HeaderParameterAlg = 1
19 | // Critical headers to be understood
20 | //
21 | // Associated value of type [+ label]
22 | //
23 | // It is a protected header parameter
24 | HeaderParameterCrit = 2
25 | // Content type of the payload
26 | //
27 | // Associated value of type tstr / uint
28 | HeaderParameterContentType = 3
29 | // Key identifier
30 | //
31 | // Associated value of type bstr
32 | HeaderParameterKid = 4
33 | // Full Initialization Vector
34 | //
35 | // Associated value of type bstr
36 | HeaderParameterIV = 5
37 | // Partial Initialization Vector
38 | //
39 | // Associated value of type bstr
40 | HeaderParameterPartialIV = 6
41 | // CBOR-encoded signature structure
42 | //
43 | // Associated value of type COSE_Signature / [+ COSE_Signature ]
44 | HeaderParameterCounterSignature = 7
45 | // Counter signature with implied signer and headers
46 | //
47 | // Associated value of type bstr
48 | HeaderParameterCounterSignature0 = 9
49 | // Identifies the context for the key identifier
50 | //
51 | // Associated value of type bstr
52 | HeaderParameterKidContext = 10
53 | // V2 countersignature attribute
54 | //
55 | // Associated value of type COSE_Countersignature / [+ COSE_Countersignature]
56 | HeaderParameterCountersignatureV2 = 11
57 | // V2 Abbreviated Countersignature
58 | //
59 | // Associated value of type COSE_Countersignature0
60 | HeaderParameterCountersignature0V2 = 11
61 | // An unordered bag of X.509 certificates
62 | //
63 | // Associated value of type COSE_X509
64 | HeaderParameterX5Bag = 32
65 | // An ordered chain of X.509 certificates
66 | //
67 | // Associated value of type COSE_X509
68 | HeaderParameterX5Chain = 33
69 | // Hash of an X.509 certificate
70 | //
71 | // Associated value of type COSE_CertHash
72 | HeaderParameterX5T = 34
73 | // URI pointing to an X.509 certificate
74 | //
75 | // Associated value of type uri
76 | HeaderParameterX5U = 35
77 | // Challenge Nonce
78 | //
79 | // Associated value of type bstr
80 | HeaderParameterCuphNonce = 256
81 | // Public Key
82 | //
83 | // Associated value of type array
84 | HeaderParameterCuphOwnerPubKey = 257
85 | )
86 |
87 | // IANA-registered COSE header algorithm parameters.
88 | //
89 | // From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#header-algorithm-parameters
90 | // as of 2022-12-19.
91 | const (
92 | // static key X.509 certificate chain
93 | //
94 | // Associated value of type COSE_X509
95 | HeaderAlgorithmParameterX5ChainSender = -29
96 | // URI for the sender's X.509 certificate
97 | //
98 | // Associated value of type uri
99 | HeaderAlgorithmParameterX5USender = -28
100 | // Thumbprint for the sender's X.509 certificate
101 | //
102 | // Associated value of type COSE_CertHash
103 | HeaderAlgorithmParameterX5TSender = -27
104 | // Party V other provided information
105 | //
106 | // Associated value of type bstr
107 | HeaderAlgorithmParameterPartyVOther = -26
108 | // Party V provided nonce
109 | //
110 | // Associated value of type bstr / int
111 | HeaderAlgorithmParameterPartyVNonce = -25
112 | // Party V identity information
113 | //
114 | // Associated value of type bstr
115 | HeaderAlgorithmParameterPartyVIdentity = -24
116 | // Party U other provided information
117 | //
118 | // Associated value of type bstr
119 | HeaderAlgorithmParameterPartyUOther = -23
120 | // Party U provided nonce
121 | //
122 | // Associated value of type bstr / int
123 | HeaderAlgorithmParameterPartyUNonce = -22
124 | // Party U identity information
125 | //
126 | // Associated value of type bstr
127 | HeaderAlgorithmParameterPartyUIdentity = -21
128 | // Random salt
129 | //
130 | // Associated value of type bstr
131 | HeaderAlgorithmParameterSalt = -20
132 | // Static public key identifier for the sender
133 | //
134 | // Associated value of type bstr
135 | HeaderAlgorithmParameterStaticKeyId = -3
136 | // Static public key for the sender
137 | //
138 | // Associated value of type COSE_Key
139 | HeaderAlgorithmParameterStaticKey = -2
140 | // Ephemeral public key for the sender
141 | //
142 | // Associated value of type COSE_Key
143 | HeaderAlgorithmParameterEphemeralKey = -1
144 | )
145 |
--------------------------------------------------------------------------------
/cose/recipient.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose
5 |
6 | import (
7 | "errors"
8 | "fmt"
9 |
10 | "github.com/ldclabs/cose/key"
11 | )
12 |
13 | // Recipient represents a COSE_recipient object.
14 | //
15 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-enveloped-cose-structure.
16 | type Recipient struct {
17 | Protected Headers
18 | Unprotected Headers
19 | Ciphertext []byte
20 |
21 | context string // "Enc_Recipient", "Mac_Recipient", "Rec_Recipient"
22 | recipients []*Recipient
23 | }
24 |
25 | // AddRecipient add a Recipient to the COSE_Recipient object.
26 | //
27 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-two-layers-of-recipient-inf.
28 | func (m *Recipient) AddRecipient(recipient *Recipient) error {
29 | if recipient == nil {
30 | return errors.New("cose/cose: Recipient.AddRecipient: nil Recipient")
31 | }
32 | if recipient == m {
33 | return errors.New("cose/cose: Recipient.AddRecipient: should not add itself")
34 | }
35 | if recipient.context != "" {
36 | return fmt.Errorf("cose/cose: Recipient.AddRecipient: should not have %q context",
37 | recipient.context)
38 | }
39 |
40 | if len(recipient.recipients) > 0 {
41 | return errors.New("cose/cose: Recipient.AddRecipient: should not have nested recipients")
42 | }
43 |
44 | recipient.context = "Rec_Recipient"
45 | m.recipients = append(m.recipients, recipient)
46 | return nil
47 | }
48 |
49 | func (m *Recipient) Recipients() []*Recipient {
50 | return m.recipients
51 | }
52 |
53 | // MarshalCBOR implements the CBOR Marshaler interface for Recipient.
54 | func (m *Recipient) MarshalCBOR() ([]byte, error) {
55 | mm0 := &recipientMessage0{
56 | Unprotected: m.Unprotected,
57 | Ciphertext: m.Ciphertext,
58 | }
59 | var err error
60 | if mm0.Protected, err = m.Protected.Bytes(); err != nil {
61 | return nil, err
62 | }
63 | if mm0.Unprotected == nil {
64 | mm0.Unprotected = Headers{}
65 | }
66 |
67 | if len(m.recipients) == 0 {
68 | return key.MarshalCBOR(mm0)
69 | }
70 |
71 | mm := &recipientMessage{
72 | Protected: mm0.Protected,
73 | Unprotected: mm0.Unprotected,
74 | Ciphertext: mm0.Ciphertext,
75 | Recipients: m.recipients,
76 | }
77 |
78 | return key.MarshalCBOR(mm)
79 | }
80 |
81 | // UnmarshalCBOR implements the CBOR Unmarshaler interface for Recipient.
82 | func (m *Recipient) UnmarshalCBOR(data []byte) error {
83 | if m == nil {
84 | return errors.New("cose/cose: Recipient.UnmarshalCBOR: nil Recipient")
85 | }
86 | if len(data) == 0 {
87 | return errors.New("cose/cose: Recipient.UnmarshalCBOR: empty data")
88 | }
89 |
90 | var err error
91 | switch data[0] {
92 | case 0x83: // array(3)
93 | mm := &recipientMessage0{}
94 | if err = key.UnmarshalCBOR(data, mm); err != nil {
95 | return err
96 | }
97 |
98 | if m.Protected, err = HeadersFromBytes(mm.Protected); err != nil {
99 | return err
100 | }
101 |
102 | m.Unprotected = mm.Unprotected
103 | m.Ciphertext = mm.Ciphertext
104 |
105 | case 0x84:
106 | mm := &recipientMessage{}
107 | if err = key.UnmarshalCBOR(data, mm); err != nil {
108 | return err
109 | }
110 | if len(mm.Recipients) == 0 {
111 | return errors.New("cose/cose: Recipient.UnmarshalCBOR: no recipients")
112 | }
113 | for _, r := range mm.Recipients {
114 | if r == nil {
115 | return errors.New("cose/cose: Recipient.UnmarshalCBOR: nil Recipient")
116 | }
117 | if len(r.recipients) > 0 {
118 | return errors.New("cose/cose: Recipient.UnmarshalCBOR: should not have nested recipients")
119 | }
120 | }
121 |
122 | if m.Protected, err = HeadersFromBytes(mm.Protected); err != nil {
123 | return err
124 | }
125 |
126 | m.Unprotected = mm.Unprotected
127 | m.Ciphertext = mm.Ciphertext
128 | m.recipients = mm.Recipients
129 |
130 | default:
131 | return errors.New("cose/cose: Recipient.UnmarshalCBOR: invalid data")
132 | }
133 |
134 | return nil
135 | }
136 |
137 | // Bytesify returns a CBOR-encoded byte slice.
138 | // It returns nil if MarshalCBOR failed.
139 | func (m *Recipient) Bytesify() []byte {
140 | b, _ := m.MarshalCBOR()
141 | return b
142 | }
143 |
144 | // recipientMessage represents a COSE_recipient structure to encode and decode.
145 | type recipientMessage struct {
146 | _ struct{} `cbor:",toarray"`
147 | Protected []byte
148 | Unprotected Headers
149 | Ciphertext []byte // can be nil
150 | Recipients []*Recipient
151 | }
152 |
153 | // recipientMessage0 represents a COSE_recipient structure without sub recipients to encode and decode.
154 | type recipientMessage0 struct {
155 | _ struct{} `cbor:",toarray"`
156 | Protected []byte
157 | Unprotected Headers
158 | Ciphertext []byte // can be nil
159 | }
160 |
--------------------------------------------------------------------------------
/cose/kdf_context.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose
5 |
6 | import (
7 | "errors"
8 |
9 | "github.com/ldclabs/cose/key"
10 | )
11 |
12 | // KDFContext represents a COSE_KDF_Context object.
13 | //
14 | // Reference https://datatracker.ietf.org/doc/html/rfc9053#name-context-information-structu
15 | type KDFContext struct {
16 | AlgorithmID int
17 | PartyUInfo PartyInfo
18 | PartyVInfo PartyInfo
19 | SuppPubInfo SuppPubInfo
20 | SuppPrivInfo []byte
21 | }
22 |
23 | type kdfContext0 struct {
24 | _ struct{} `cbor:",toarray"`
25 | AlgorithmID int
26 | PartyUInfo PartyInfo
27 | PartyVInfo PartyInfo
28 | SuppPubInfo SuppPubInfo
29 | }
30 |
31 | type kdfContext1 struct {
32 | _ struct{} `cbor:",toarray"`
33 | AlgorithmID int
34 | PartyUInfo PartyInfo
35 | PartyVInfo PartyInfo
36 | SuppPubInfo SuppPubInfo
37 | SuppPrivInfo []byte
38 | }
39 |
40 | // MarshalCBOR implements the CBOR Marshaler interface for KDFContext.
41 | func (m KDFContext) MarshalCBOR() ([]byte, error) {
42 | if m.SuppPrivInfo == nil {
43 | return key.MarshalCBOR(kdfContext0{
44 | AlgorithmID: m.AlgorithmID,
45 | PartyUInfo: m.PartyUInfo,
46 | PartyVInfo: m.PartyVInfo,
47 | SuppPubInfo: m.SuppPubInfo,
48 | })
49 | }
50 |
51 | return key.MarshalCBOR(kdfContext1{
52 | AlgorithmID: m.AlgorithmID,
53 | PartyUInfo: m.PartyUInfo,
54 | PartyVInfo: m.PartyVInfo,
55 | SuppPubInfo: m.SuppPubInfo,
56 | SuppPrivInfo: m.SuppPrivInfo,
57 | })
58 | }
59 |
60 | // UnmarshalCBOR implements the CBOR Unmarshaler interface for KDFContext.
61 | func (m *KDFContext) UnmarshalCBOR(data []byte) error {
62 | if m == nil {
63 | return errors.New("cose/cose: KDFContext.UnmarshalCBOR: nil KDFContext")
64 | }
65 | if len(data) == 0 {
66 | return errors.New("cose/cose: KDFContext.UnmarshalCBOR: empty data")
67 | }
68 |
69 | switch data[0] {
70 | case 0x84:
71 | v := &kdfContext0{}
72 | if err := key.UnmarshalCBOR(data, v); err != nil {
73 | return err
74 | }
75 | m.AlgorithmID = v.AlgorithmID
76 | m.PartyUInfo = v.PartyUInfo
77 | m.PartyVInfo = v.PartyVInfo
78 | m.SuppPubInfo = v.SuppPubInfo
79 |
80 | case 0x85:
81 | v := &kdfContext1{}
82 | if err := key.UnmarshalCBOR(data, v); err != nil {
83 | return err
84 | }
85 | m.AlgorithmID = v.AlgorithmID
86 | m.PartyUInfo = v.PartyUInfo
87 | m.PartyVInfo = v.PartyVInfo
88 | m.SuppPubInfo = v.SuppPubInfo
89 | m.SuppPrivInfo = v.SuppPrivInfo
90 |
91 | default:
92 | return errors.New("cose/cose: KDFContext.UnmarshalCBOR: invalid data")
93 | }
94 |
95 | return nil
96 | }
97 |
98 | // SuppPubInfo represents a SuppPubInfo object.
99 | type SuppPubInfo struct {
100 | KeyDataLength uint // bits of the desired output value
101 | Protected Headers
102 | Other []byte
103 | }
104 |
105 | type suppPubInfo0 struct {
106 | _ struct{} `cbor:",toarray"`
107 | KeyDataLength uint
108 | Protected []byte
109 | }
110 |
111 | type suppPubInfo1 struct {
112 | _ struct{} `cbor:",toarray"`
113 | KeyDataLength uint
114 | Protected []byte
115 | Other []byte
116 | }
117 |
118 | // MarshalCBOR implements the CBOR Marshaler interface for SuppPubInfo.
119 | func (m SuppPubInfo) MarshalCBOR() ([]byte, error) {
120 | protected, err := m.Protected.Bytes()
121 | if err != nil {
122 | return nil, err
123 | }
124 |
125 | if m.Other == nil {
126 | return key.MarshalCBOR(&suppPubInfo0{
127 | KeyDataLength: m.KeyDataLength,
128 | Protected: protected,
129 | })
130 | }
131 |
132 | return key.MarshalCBOR(&suppPubInfo1{
133 | KeyDataLength: m.KeyDataLength,
134 | Protected: protected,
135 | Other: m.Other,
136 | })
137 | }
138 |
139 | // UnmarshalCBOR implements the CBOR Unmarshaler interface for SuppPubInfo.
140 | func (m *SuppPubInfo) UnmarshalCBOR(data []byte) error {
141 | if m == nil {
142 | return errors.New("cose/cose: SuppPubInfo.UnmarshalCBOR: nil SuppPubInfo")
143 | }
144 | if len(data) == 0 {
145 | return errors.New("cose/cose: SuppPubInfo.UnmarshalCBOR: empty data")
146 | }
147 |
148 | var err error
149 | switch data[0] {
150 | case 0x82:
151 | v := &suppPubInfo0{}
152 | if err = key.UnmarshalCBOR(data, v); err != nil {
153 | return err
154 | }
155 | m.KeyDataLength = v.KeyDataLength
156 | if m.Protected, err = HeadersFromBytes(v.Protected); err != nil {
157 | return err
158 | }
159 |
160 | case 0x83:
161 | var v suppPubInfo1
162 | if err = key.UnmarshalCBOR(data, &v); err != nil {
163 | return err
164 | }
165 | m.KeyDataLength = v.KeyDataLength
166 | m.Other = v.Other
167 | if m.Protected, err = HeadersFromBytes(v.Protected); err != nil {
168 | return err
169 | }
170 |
171 | default:
172 | return errors.New("cose/cose: SuppPubInfo.UnmarshalCBOR: invalid data")
173 | }
174 |
175 | return nil
176 | }
177 |
178 | // PartyInfo represents a PartyInfo object.
179 | type PartyInfo struct {
180 | _ struct{} `cbor:",toarray"`
181 | Identity []byte
182 | Nonce []byte
183 | Other []byte
184 | }
185 |
--------------------------------------------------------------------------------
/iana/algorithm.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | // Package iana registers COSE: https://www.iana.org/assignments/cose/cose.xhtml,
5 | // CWT: https://www.iana.org/assignments/cwt/cwt.xhtml,
6 | // and CBOR Tags: https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml.
7 | package iana
8 |
9 | // IANA-registered COSE algorithms.
10 | //
11 | // From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#algorithms
12 | // as of 2022-12-19.
13 | const (
14 | // RSASSA-PKCS1-v1_5 using SHA-1
15 | AlgorithmRS1 = -65535
16 | // WalnutDSA signature
17 | AlgorithmWalnutDSA = -260
18 | // RSASSA-PKCS1-v1_5 using SHA-512
19 | AlgorithmRS512 = -259
20 | // RSASSA-PKCS1-v1_5 using SHA-384
21 | AlgorithmRS384 = -258
22 | // RSASSA-PKCS1-v1_5 using SHA-256
23 | AlgorithmRS256 = -257
24 | // ECDSA using secp256k1 curve and SHA-256
25 | AlgorithmES256K = -47
26 | // HSS/LMS hash-based digital signature
27 | AlgorithmHSS_LMS = -46
28 | // SHAKE-256 512-bit Hash Value
29 | AlgorithmSHAKE256 = -45
30 | // SHA-2 512-bit Hash
31 | AlgorithmSHA_512 = -44
32 | // SHA-2 384-bit Hash
33 | AlgorithmSHA_384 = -43
34 | // RSAES-OAEP w/ SHA-512
35 | AlgorithmRSAES_OAEP_SHA_512 = -42
36 | // RSAES-OAEP w/ SHA-256
37 | AlgorithmRSAES_OAEP_SHA_256 = -41
38 | // RSAES-OAEP w/ SHA-1
39 | AlgorithmRSAES_OAEP_RFC_8017_default = -40
40 | // RSASSA-PSS w/ SHA-512
41 | AlgorithmPS512 = -39
42 | // RSASSA-PSS_SHA-384
43 | AlgorithmPS384 = -38
44 | // RSASSA-PSS w/ SHA-256
45 | AlgorithmPS256 = -37
46 | // ECDSA w/ SHA-512
47 | AlgorithmES512 = -36
48 | // ECDSA w/ SHA-384
49 | AlgorithmES384 = -35
50 | // ECDH SS w/ Concat KDF and AES Key Wrap w/ 256-bit key
51 | AlgorithmECDH_SS_A256KW = -34
52 | // ECDH SS w/ Concat KDF and AES Key Wrap w/ 192-bit key
53 | AlgorithmECDH_SS_A192KW = -33
54 | // ECDH SS w/ Concat KDF and AES Key Wrap w/ 128-bit key
55 | AlgorithmECDH_SS_A128KW = -32
56 | // ECDH ES w/ Concat KDF and AES Key Wrap w/ 256-bit key
57 | AlgorithmECDH_ES_A256KW = -31
58 | // ECDH ES w/ Concat KDF and AES Key Wrap w/ 192-bit key
59 | AlgorithmECDH_ES_A192KW = -30
60 | // ECDH ES w/ Concat KDF and AES Key Wrap w/ 128-bit key
61 | AlgorithmECDH_ES_A128KW = -29
62 | // ECDH SS w/ HKDF - generate key directly
63 | AlgorithmECDH_SS_HKDF_512 = -28
64 | // ECDH SS w/ HKDF - generate key directly
65 | AlgorithmECDH_SS_HKDF_256 = -27
66 | // ECDH ES w/ HKDF - generate key directly
67 | AlgorithmECDH_ES_HKDF_512 = -26
68 | // ECDH ES w/ HKDF - generate key directly
69 | AlgorithmECDH_ES_HKDF_256 = -25
70 | // SHAKE-128 256-bit Hash Value
71 | AlgorithmSHAKE128 = -18
72 | // SHA-2 512-bit Hash truncated to 256-bits
73 | AlgorithmSHA_512_256 = -17
74 | // SHA-2 256-bit Hash
75 | AlgorithmSHA_256 = -16
76 | // SHA-2 256-bit Hash truncated to 64-bits
77 | AlgorithmSHA_256_64 = -15
78 | // SHA-1 Hash
79 | AlgorithmSHA_1 = -14
80 | // Shared secret w/ AES-MAC 256-bit key
81 | AlgorithmDirect_HKDF_AES_256 = -13
82 | // Shared secret w/ AES-MAC 128-bit key
83 | AlgorithmDirect_HKDF_AES_128 = -12
84 | // Shared secret w/ HKDF and SHA-512
85 | AlgorithmDirect_HKDF_SHA_512 = -11
86 | // Shared secret w/ HKDF and SHA-256
87 | AlgorithmDirect_HKDF_SHA_256 = -10
88 | // EdDSA
89 | AlgorithmEdDSA = -8
90 | // ECDSA w/ SHA-256
91 | AlgorithmES256 = -7
92 | // Direct use of CEK
93 | AlgorithmDirect = -6
94 | // AES Key Wrap w/ 256-bit key
95 | AlgorithmA256KW = -5
96 | // AES Key Wrap w/ 192-bit key
97 | AlgorithmA192KW = -4
98 | // AES Key Wrap w/ 128-bit key
99 | AlgorithmA128KW = -3
100 | // Reserved
101 | AlgorithmReserved = 0
102 | // AES-GCM mode w/ 128-bit key, 128-bit tag
103 | AlgorithmA128GCM = 1
104 | // AES-GCM mode w/ 192-bit key, 128-bit tag
105 | AlgorithmA192GCM = 2
106 | // AES-GCM mode w/ 256-bit key, 128-bit tag
107 | AlgorithmA256GCM = 3
108 | // HMAC w/ SHA-256 truncated to 64 bits
109 | AlgorithmHMAC_256_64 = 4
110 | // HMAC w/ SHA-256
111 | AlgorithmHMAC_256_256 = 5
112 | // HMAC w/ SHA-384
113 | AlgorithmHMAC_384_384 = 6
114 | // HMAC w/ SHA-512
115 | AlgorithmHMAC_512_512 = 7
116 | // AES-CCM mode 128-bit key, 64-bit tag, 13-byte nonce
117 | AlgorithmAES_CCM_16_64_128 = 10
118 | // AES-CCM mode 256-bit key, 64-bit tag, 13-byte nonce
119 | AlgorithmAES_CCM_16_64_256 = 11
120 | // AES-CCM mode 128-bit key, 64-bit tag, 7-byte nonce
121 | AlgorithmAES_CCM_64_64_128 = 12
122 | // AES-CCM mode 256-bit key, 64-bit tag, 7-byte nonce
123 | AlgorithmAES_CCM_64_64_256 = 13
124 | // AES-MAC 128-bit key, 64-bit tag
125 | AlgorithmAES_MAC_128_64 = 14
126 | // AES-MAC 256-bit key, 64-bit tag
127 | AlgorithmAES_MAC_256_64 = 15
128 | // ChaCha20/Poly1305 w/ 256-bit key, 128-bit tag
129 | AlgorithmChaCha20Poly1305 = 24
130 | // AES-MAC 128-bit key, 128-bit tag
131 | AlgorithmAES_MAC_128_128 = 25
132 | // AES-MAC 256-bit key, 128-bit tag
133 | AlgorithmAES_MAC_256_128 = 26
134 | // AES-CCM mode 128-bit key, 128-bit tag, 13-byte nonce
135 | AlgorithmAES_CCM_16_128_128 = 30
136 | // AES-CCM mode 256-bit key, 128-bit tag, 13-byte nonce
137 | AlgorithmAES_CCM_16_128_256 = 31
138 | // AES-CCM mode 128-bit key, 128-bit tag, 7-byte nonce
139 | AlgorithmAES_CCM_64_128_128 = 32
140 | // AES-CCM mode 256-bit key, 128-bit tag, 7-byte nonce
141 | AlgorithmAES_CCM_64_128_256 = 33
142 | // For doing IV generation for symmetric algorithms.
143 | AlgorithmIV_GENERATION = 34
144 | )
145 |
--------------------------------------------------------------------------------
/key/registry.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "fmt"
8 |
9 | "github.com/ldclabs/cose/iana"
10 | )
11 |
12 | // SignerFactory is a function that returns a Signer for the given key.
13 | type SignerFactory func(Key) (Signer, error)
14 |
15 | // VerifierFactory is a function that returns a Verifier for the given key.
16 | type VerifierFactory func(Key) (Verifier, error)
17 |
18 | // MACerFactory is a function that returns a MACer for the given key.
19 | type MACerFactory func(Key) (MACer, error)
20 |
21 | // EncryptorFactory is a function that returns a Encryptor for the given key.
22 | type EncryptorFactory func(Key) (Encryptor, error)
23 |
24 | type tripleKey [3]int
25 |
26 | var (
27 | signers = map[tripleKey]SignerFactory{}
28 | verifiers = map[tripleKey]VerifierFactory{}
29 | macers = map[tripleKey]MACerFactory{}
30 | encryptors = map[tripleKey]EncryptorFactory{}
31 | )
32 |
33 | // RegisterSigner registers a SignerFactory for the given key type, algorithm, and curve.
34 | // For example, to register a ed25519 signer factory:
35 | //
36 | // key.RegisterSigner(iana.KeyTypeOKP, iana.AlgorithmEdDSA, iana.EllipticCurveEd25519, ed25519.NewSigner)
37 | func RegisterSigner(kty, alg, crv int, fn SignerFactory) {
38 | tk := tripleKey{kty, alg, crv}
39 | if _, ok := signers[tk]; ok {
40 | panic(fmt.Errorf("cose/key: RegisterSigner: %s is already registered", tk.String()))
41 | }
42 | signers[tk] = fn
43 | }
44 |
45 | // RegisterVerifier registers a VerifierFactory for the given key type, algorithm, and curve.
46 | func RegisterVerifier(kty, alg, crv int, fn VerifierFactory) {
47 | tk := tripleKey{kty, alg, crv}
48 | if _, ok := verifiers[tk]; ok {
49 | panic(fmt.Errorf("cose/key: RegisterVerifier: %s is already registered", tk.String()))
50 | }
51 | verifiers[tk] = fn
52 | }
53 |
54 | // RegisterMACer registers a MACerFactory for the given key type and algorithm.
55 | func RegisterMACer(kty, alg int, fn MACerFactory) {
56 | tk := tripleKey{kty, alg, 0}
57 | if _, ok := macers[tk]; ok {
58 | panic(fmt.Errorf("cose/key: RegisterMACer: %s is already registered", tk.String()))
59 | }
60 | macers[tk] = fn
61 | }
62 |
63 | // RegisterEncryptor registers a EncryptorFactory for the given key type and algorithm.
64 | func RegisterEncryptor(kty, alg int, fn EncryptorFactory) {
65 | tk := tripleKey{kty, alg, 0}
66 | if _, ok := encryptors[tk]; ok {
67 | panic(fmt.Errorf("cose/key: RegisterEncryptor: %s is already registered", tk.String()))
68 | }
69 | encryptors[tk] = fn
70 | }
71 |
72 | // Signer returns a Signer for the given key.
73 | // If the key is nil, or SignerFactory for the given key type, algorithm, and curve not registered,
74 | // an error is returned.
75 | func (k Key) Signer() (Signer, error) {
76 | if k == nil {
77 | return nil, fmt.Errorf("cose/key: Key.Signer: nil key")
78 | }
79 |
80 | tk := k.tripleKey()
81 | fn, ok := signers[tk]
82 | if !ok {
83 | return nil, fmt.Errorf("cose/key: Key.Signer: %s is not registered", tk.String())
84 | }
85 |
86 | return fn(k)
87 | }
88 |
89 | // Verifier returns a Verifier for the given key.
90 | // If the key is nil, or VerifierFactory for the given key type, algorithm, and curve not registered,
91 | // an error is returned.
92 | func (k Key) Verifier() (Verifier, error) {
93 | if k == nil {
94 | return nil, fmt.Errorf("cose/key: Key.Verifier: nil key")
95 | }
96 |
97 | tk := k.tripleKey()
98 | fn, ok := verifiers[tk]
99 | if !ok {
100 | return nil, fmt.Errorf("cose/key: Key.Verifier: %s is not registered", tk.String())
101 | }
102 |
103 | return fn(k)
104 | }
105 |
106 | // MACer returns a MACer for the given key.
107 | // If the key is nil, or MACerFactory for the given key type and algorithm not registered,
108 | // an error is returned.
109 | func (k Key) MACer() (MACer, error) {
110 | if k == nil {
111 | return nil, fmt.Errorf("cose/key: Key.MACer: nil key")
112 | }
113 |
114 | tk := k.tripleKey()
115 | fn, ok := macers[tk]
116 | if !ok {
117 | return nil, fmt.Errorf("cose/key: Key.MACer: %s is not registered", tk.String())
118 | }
119 |
120 | return fn(k)
121 | }
122 |
123 | // Encryptor returns a Encryptor for the given key.
124 | // If the key is nil, or EncryptorFactory for the given key type and algorithm not registered,
125 | // an error is returned.
126 | func (k Key) Encryptor() (Encryptor, error) {
127 | if k == nil {
128 | return nil, fmt.Errorf("cose/key: Key.Encryptor: nil key")
129 | }
130 |
131 | tk := k.tripleKey()
132 | fn, ok := encryptors[tk]
133 | if !ok {
134 | return nil, fmt.Errorf("cose/key: Key.Encryptor: %s is not registered", tk.String())
135 | }
136 |
137 | return fn(k)
138 | }
139 |
140 | func (k Key) tripleKey() tripleKey {
141 | kty := k.Kty()
142 | alg := k.Alg()
143 | crv, _ := k.GetInt(iana.OKPKeyParameterCrv) // or iana.EC2KeyParameterCrv
144 | if alg == iana.AlgorithmReserved {
145 | switch kty {
146 | case iana.KeyTypeOKP:
147 | alg = iana.AlgorithmEdDSA
148 | crv = iana.EllipticCurveEd25519
149 |
150 | case iana.KeyTypeEC2:
151 | alg = iana.AlgorithmES256
152 | crv = iana.EllipticCurveP_256
153 | }
154 | }
155 |
156 | return tripleKey{kty, int(alg), crv}
157 | }
158 |
159 | func (tk tripleKey) String() string {
160 | str := fmt.Sprintf("kty(%d)_alg(%d)", tk[0], tk[1])
161 | if tk[2] != 0 {
162 | str += fmt.Sprintf("_crv(%d)", tk[2])
163 | }
164 | return str
165 | }
166 |
--------------------------------------------------------------------------------
/cwt/claims_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cwt
5 |
6 | import (
7 | "encoding/json"
8 | "testing"
9 |
10 | "github.com/ldclabs/cose/cose"
11 | "github.com/ldclabs/cose/iana"
12 | "github.com/ldclabs/cose/key"
13 | _ "github.com/ldclabs/cose/key/ecdsa"
14 |
15 | "github.com/stretchr/testify/assert"
16 | "github.com/stretchr/testify/require"
17 | )
18 |
19 | func TestClaims(t *testing.T) {
20 | assert := assert.New(t)
21 | require := require.New(t)
22 |
23 | for _, tc := range []struct {
24 | title string
25 | json string
26 | claims Claims
27 | res []byte
28 | }{
29 | {
30 | `Example CWT Claims Set`,
31 | `{"iss":"coap://as.example.com","sub":"erikw","aud":"coap://light.example.com","exp":1444064944,"nbf":1443944944,"iat":1443944944,"cti":"0b71"}`,
32 | Claims{
33 | Issuer: "coap://as.example.com",
34 | Subject: "erikw",
35 | Audience: "coap://light.example.com",
36 | Expiration: 1444064944,
37 | NotBefore: 1443944944,
38 | IssuedAt: 1443944944,
39 | CWTID: key.HexBytesify("0b71"),
40 | },
41 | key.HexBytesify("a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b71"),
42 | },
43 | } {
44 | data, err := key.MarshalCBOR(tc.claims)
45 | require.NoError(err, tc.title)
46 | assert.Equal(tc.res, data, tc.title)
47 |
48 | jsondata, err := json.Marshal(tc.claims)
49 | require.NoError(err, tc.title)
50 | // fmt.Println(string(data))
51 | assert.Equal(tc.json, string(jsondata), tc.title)
52 |
53 | var claims2 Claims
54 | require.NoError(key.UnmarshalCBOR(data, &claims2), tc.title)
55 | assert.Equal(tc.res, claims2.Bytesify(), tc.title)
56 |
57 | var cm ClaimsMap
58 | require.NoError(key.UnmarshalCBOR(data, &cm), tc.title)
59 | assert.Equal(tc.res, cm.Bytesify(), tc.title)
60 | }
61 | }
62 |
63 | func TestClaimsSign1AndVerify(t *testing.T) {
64 | assert := assert.New(t)
65 | require := require.New(t)
66 |
67 | for _, tc := range []struct {
68 | title string
69 | key key.Key
70 | claims Claims
71 | sig []byte
72 | output []byte
73 | }{
74 | {
75 | `Case: Example Signed CWT`,
76 | map[any]any{
77 | iana.KeyParameterKty: iana.KeyTypeEC2,
78 | iana.KeyParameterKid: key.HexBytesify("4173796d6d65747269634543445341323536"),
79 | iana.KeyParameterAlg: iana.AlgorithmES256,
80 | iana.EC2KeyParameterCrv: iana.EllipticCurveP_256,
81 | iana.EC2KeyParameterX: key.HexBytesify("143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f"),
82 | iana.EC2KeyParameterY: key.HexBytesify("60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168db9529971a36e7b9"),
83 | iana.EC2KeyParameterD: key.HexBytesify("6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19"),
84 | },
85 | Claims{
86 | Issuer: "coap://as.example.com",
87 | Subject: "erikw",
88 | Audience: "coap://light.example.com",
89 | Expiration: 1444064944,
90 | NotBefore: 1443944944,
91 | IssuedAt: 1443944944,
92 | CWTID: key.HexBytesify("0b71"),
93 | },
94 | key.HexBytesify("5427c1ff28d23fbad1f29c4c7c6a555e601d6fa29f9179bc3d7438bacaca5acd08c8d4d4f96131680c429a01f85951ecee743a52b9b63632c57209120e1c9e30"),
95 | key.HexBytesify("d28443a10126a104524173796d6d657472696345434453413235365850a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b7158405427c1ff28d23fbad1f29c4c7c6a555e601d6fa29f9179bc3d7438bacaca5acd08c8d4d4f96131680c429a01f85951ecee743a52b9b63632c57209120e1c9e30"),
96 | },
97 | } {
98 | signer, err := tc.key.Signer()
99 | require.NoError(err, tc.title)
100 |
101 | verifier, err := tc.key.Verifier()
102 | require.NoError(err, tc.title)
103 |
104 | s := cose.Sign1Message[Claims]{Payload: tc.claims}
105 | coseData, err := s.SignAndEncode(signer, nil)
106 | require.NoError(err, tc.title)
107 | assert.NotEqual(tc.output, coseData, tc.title)
108 | assert.NotEqual(tc.sig, s.Signature(), tc.title)
109 | assert.NotEqual(tc.output, s.Bytesify(), tc.title)
110 |
111 | s2, err := cose.VerifySign1Message[Claims](verifier, coseData, nil)
112 | require.NoError(err, tc.title)
113 | assert.Equal(tc.claims.Issuer, s2.Payload.Issuer)
114 | assert.Equal(tc.claims.Bytesify(), s2.Payload.Bytesify(), tc.title)
115 | assert.NotEqual(tc.sig, s2.Signature(), tc.title)
116 | assert.NotEqual(tc.output, s2.Bytesify(), tc.title)
117 | assert.Equal(s.Signature(), s2.Signature(), tc.title)
118 | assert.Equal(s.Bytesify(), s2.Bytesify(), tc.title)
119 |
120 | coseData2, err := s2.SignAndEncode(signer, nil)
121 | require.NoError(err, tc.title)
122 | assert.NotEqual(coseData, coseData2, tc.title)
123 |
124 | s3, err := cose.VerifySign1Message[Claims](verifier, tc.output, nil)
125 | require.NoError(err, tc.title)
126 | assert.Equal(tc.claims.Issuer, s3.Payload.Issuer)
127 | assert.Equal(tc.claims.Bytesify(), s3.Payload.Bytesify(), tc.title)
128 | assert.Equal(tc.sig, s3.Signature(), tc.title)
129 | assert.Equal(tc.output, s3.Bytesify(), tc.title)
130 |
131 | s4, err := cose.VerifySign1Message[ClaimsMap](verifier, tc.output, nil)
132 | require.NoError(err, tc.title)
133 |
134 | issuer, err := s4.Payload.GetString(iana.CWTClaimIss)
135 | require.NoError(err, tc.title)
136 | assert.Equal(tc.claims.Issuer, issuer)
137 | assert.Equal(tc.claims.Bytesify(), s4.Payload.Bytesify(), tc.title)
138 | assert.Equal(tc.sig, s4.Signature(), tc.title)
139 | assert.Equal(tc.output, s4.Bytesify(), tc.title)
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/cwt/validator.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cwt
5 |
6 | import (
7 | "fmt"
8 | "math"
9 | "time"
10 |
11 | "github.com/ldclabs/cose/iana"
12 | )
13 |
14 | const (
15 | cwtMaxClockSkewMinutes = 10
16 | )
17 |
18 | // ValidatorOpts defines validation options for CWT validators.
19 | type ValidatorOpts struct {
20 | ExpectedIssuer string
21 | ExpectedAudience string
22 |
23 | AllowMissingExpiration bool
24 | ExpectIssuedInThePast bool
25 |
26 | ClockSkew time.Duration
27 | FixedNow time.Time
28 | }
29 |
30 | // Validator defines how CBOR Web Tokens (CWT) should be validated.
31 | type Validator struct {
32 | opts ValidatorOpts
33 | }
34 |
35 | // NewValidator creates a new CWT Validator.
36 | func NewValidator(opts *ValidatorOpts) (*Validator, error) {
37 | if opts == nil {
38 | return nil, fmt.Errorf("cose/cwt: NewValidator: nil ValidatorOpts")
39 | }
40 |
41 | if opts.ClockSkew.Minutes() > cwtMaxClockSkewMinutes {
42 | return nil, fmt.Errorf("cose/cwt: NewValidator: clock skew too large, expected <= %d minutes, got %f",
43 | cwtMaxClockSkewMinutes, opts.ClockSkew.Minutes())
44 | }
45 | return &Validator{
46 | opts: *opts,
47 | }, nil
48 | }
49 |
50 | // Validate validates a *Claims according to the options provided.
51 | func (v *Validator) Validate(claims *Claims) error {
52 | if claims == nil {
53 | return fmt.Errorf("cose/cwt: Validator.Validate: nil Claims")
54 | }
55 |
56 | now := time.Now()
57 | if !v.opts.FixedNow.IsZero() {
58 | now = v.opts.FixedNow
59 | }
60 |
61 | if claims.Expiration == 0 && !v.opts.AllowMissingExpiration {
62 | return fmt.Errorf("cose/cwt: Validator.Validate: token doesn't have an expiration set")
63 | }
64 |
65 | if claims.Expiration > 0 {
66 | if !toTime(claims.Expiration).After(now.Add(-v.opts.ClockSkew)) {
67 | return fmt.Errorf("cose/cwt: Validator.Validate: token has expired")
68 | }
69 | }
70 |
71 | if claims.NotBefore > 0 {
72 | if t := toTime(claims.NotBefore); t.IsZero() || t.After(now.Add(v.opts.ClockSkew)) {
73 | return fmt.Errorf("cose/cwt: Validator.Validate: token cannot be used yet")
74 | }
75 | }
76 |
77 | if claims.IssuedAt > 0 && v.opts.ExpectIssuedInThePast {
78 | if t := toTime(claims.IssuedAt); t.IsZero() || t.After(now.Add(v.opts.ClockSkew)) {
79 | return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid iat claim in the future")
80 | }
81 | }
82 |
83 | if v.opts.ExpectedIssuer != "" && v.opts.ExpectedIssuer != claims.Issuer {
84 | return fmt.Errorf("cose/cwt: Validator.Validate: issuer mismatch, expected %q, got %q",
85 | v.opts.ExpectedIssuer, claims.Issuer)
86 | }
87 |
88 | if v.opts.ExpectedAudience != "" && v.opts.ExpectedAudience != claims.Audience {
89 | return fmt.Errorf("cose/cwt: Validator.Validate: audience mismatch, expected %q, got %q",
90 | v.opts.ExpectedAudience, claims.Audience)
91 | }
92 | return nil
93 | }
94 |
95 | // ValidateMap validates a ClaimsMap according to the options provided.
96 | func (v *Validator) ValidateMap(claims ClaimsMap) error {
97 | if claims == nil {
98 | return fmt.Errorf("cose/cwt: Validator.Validate: nil ClaimsMap")
99 | }
100 |
101 | now := time.Now()
102 | if !v.opts.FixedNow.IsZero() {
103 | now = v.opts.FixedNow
104 | }
105 |
106 | if !claims.Has(iana.CWTClaimExp) && !v.opts.AllowMissingExpiration {
107 | return fmt.Errorf("cose/cwt: Validator.Validate: token doesn't have an expiration set")
108 | }
109 |
110 | if claims.Has(iana.CWTClaimExp) {
111 | exp, err := claims.GetUint64(iana.CWTClaimExp)
112 | if err != nil {
113 | return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid exp claim, %w", err)
114 | }
115 |
116 | if !toTime(exp).After(now.Add(-v.opts.ClockSkew)) {
117 | return fmt.Errorf("cose/cwt: Validator.Validate: token has expired")
118 | }
119 | }
120 |
121 | if claims.Has(iana.CWTClaimNbf) {
122 | nbf, err := claims.GetUint64(iana.CWTClaimNbf)
123 | if err != nil {
124 | return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid nbf claim, %w", err)
125 | }
126 | if t := toTime(nbf); t.IsZero() || t.After(now.Add(v.opts.ClockSkew)) {
127 | return fmt.Errorf("cose/cwt: Validator.Validate: token cannot be used yet")
128 | }
129 | }
130 |
131 | if claims.Has(iana.CWTClaimIat) {
132 | iat, err := claims.GetUint64(iana.CWTClaimIat)
133 | if err != nil {
134 | return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid iat claim, %w", err)
135 | }
136 | if iat > 0 && v.opts.ExpectIssuedInThePast {
137 | if t := toTime(iat); t.IsZero() || t.After(now.Add(v.opts.ClockSkew)) {
138 | return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid iat claim in the future")
139 | }
140 | }
141 | }
142 |
143 | iss, err := claims.GetString(iana.CWTClaimIss)
144 | if err != nil {
145 | return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid iss claim, %w", err)
146 | }
147 | if v.opts.ExpectedIssuer != "" && v.opts.ExpectedIssuer != iss {
148 | return fmt.Errorf("cose/cwt: Validator.Validate: issuer mismatch, expected %q, got %q",
149 | v.opts.ExpectedIssuer, iss)
150 | }
151 |
152 | aud, err := claims.GetString(iana.CWTClaimAud)
153 | if err != nil {
154 | return fmt.Errorf("cose/cwt: Validator.Validate: token has an invalid aud claim, %w", err)
155 | }
156 | if v.opts.ExpectedAudience != "" && v.opts.ExpectedAudience != aud {
157 | return fmt.Errorf("cose/cwt: Validator.Validate: audience mismatch, expected %q, got %q",
158 | v.opts.ExpectedAudience, aud)
159 | }
160 |
161 | return nil
162 | }
163 |
164 | func toTime(u uint64) time.Time {
165 | if u >= math.MaxInt64 {
166 | return time.Time{}
167 | }
168 |
169 | return time.Unix(int64(u), 0)
170 | }
171 |
--------------------------------------------------------------------------------
/key/key.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | // Package key implements algorithms and key objects for COSE as defined in RFC9052 and RFC9053.
5 | // https://datatracker.ietf.org/doc/html/rfc9052#name-key-objects.
6 | // https://datatracker.ietf.org/doc/html/rfc9053.
7 | package key
8 |
9 | import (
10 | "github.com/ldclabs/cose/iana"
11 | )
12 |
13 | // Key represents a COSE_Key object.
14 | //
15 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-key-objects.
16 | type Key CoseMap
17 |
18 | // Kty returns the key type.
19 | // If the key is nil, it returns KtyReserved.
20 | //
21 | // Reference https://www.iana.org/assignments/cose/cose.xhtml#key-type
22 | func (k Key) Kty() int {
23 | if k == nil {
24 | return iana.KeyTypeReserved
25 | }
26 |
27 | v, _ := k.GetInt(iana.KeyParameterKty)
28 | return v
29 | }
30 |
31 | // Kid returns the key identifier.
32 | // If the key identifier is not present, or the underlying value's Kind is not []byte, it returns nil.
33 | func (k Key) Kid() ByteStr {
34 | v, _ := k.GetBytes(iana.KeyParameterKid)
35 | return v
36 | }
37 |
38 | // SetKid sets the key identifier.
39 | func (k Key) SetKid(kid ByteStr) {
40 | k[iana.KeyParameterKid] = kid
41 | }
42 |
43 | // Alg returns the key algorithm.
44 | // If It is elliptic-curves key and algorithm is not present,
45 | // it will return the algorithm that matched the curve.
46 | //
47 | // Reference https://www.iana.org/assignments/cose/cose.xhtml#algorithms
48 | func (k Key) Alg() Alg {
49 | v, err := k.GetInt(iana.KeyParameterAlg)
50 | if err == nil && v == 0 {
51 | // alg is optional, try lookup it by crv ( or iana.EC2KeyParameterCrv)
52 | if c, err := k.GetInt(iana.OKPKeyParameterCrv); err == nil {
53 | return CrvAlg(c)
54 | }
55 | }
56 | return Alg(v)
57 | }
58 |
59 | // Ops returns the key operations, or nil.
60 | //
61 | // Reference https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters
62 | func (k Key) Ops() Ops {
63 | if v, ok := k[iana.KeyParameterKeyOps]; ok {
64 | switch x := v.(type) {
65 | case Ops:
66 | return x
67 |
68 | case []int:
69 | ops := make(Ops, len(x))
70 | copy(ops, x)
71 | return ops
72 |
73 | case []any:
74 | ops := make(Ops, len(x))
75 | for i, v := range x {
76 | op, err := ToInt(v)
77 | if err != nil {
78 | return nil
79 | }
80 |
81 | ops[i] = op
82 | }
83 | return ops
84 | }
85 | }
86 |
87 | return nil
88 | }
89 |
90 | // SetOps sets the key operations.
91 | // If operations is empty, it will remove the key_ops field.
92 | func (k Key) SetOps(os ...int) {
93 | if len(os) > 0 {
94 | k[iana.KeyParameterKeyOps] = os
95 | } else {
96 | delete(k, iana.KeyParameterKeyOps)
97 | }
98 | }
99 |
100 | // BaseIV returns the base IV to be XORed with Partial IVs.
101 | //
102 | // Reference https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters
103 | func (k Key) BaseIV() ByteStr {
104 | v, _ := k.GetBytes(iana.KeyParameterBaseIV)
105 | return v
106 | }
107 |
108 | // Has returns true if the key has the given parameter.
109 | func (k Key) Has(p any) bool {
110 | return CoseMap(k).Has(p)
111 | }
112 |
113 | // Get returns the value of the given parameter.
114 | func (k Key) Get(p any) any {
115 | return CoseMap(k).Get(p)
116 | }
117 |
118 | // Set sets the parameter. parameter key should be int or string.
119 | func (k Key) Set(p, value any) error {
120 | return CoseMap(k).Set(p, value)
121 | }
122 |
123 | // GetBool returns the value of the given parameter as a bool, or a error.
124 | func (k Key) GetBool(p any) (bool, error) {
125 | return CoseMap(k).GetBool(p)
126 | }
127 |
128 | // GetInt returns the value of the given parameter as a int, or a error.
129 | func (k Key) GetInt(p any) (int, error) {
130 | return CoseMap(k).GetInt(p)
131 | }
132 |
133 | // GetInt64 returns the value of the given parameter as a int64, or a error.
134 | func (k Key) GetInt64(p any) (int64, error) {
135 | return CoseMap(k).GetInt64(p)
136 | }
137 |
138 | // GetUint64 returns the value of the given parameter as a uint64, or a error.
139 | func (k Key) GetUint64(p any) (uint64, error) {
140 | return CoseMap(k).GetUint64(p)
141 | }
142 |
143 | // GetBytes returns the value of the given parameter as a slice of bytes, or a error.
144 | func (k Key) GetBytes(p any) ([]byte, error) {
145 | return CoseMap(k).GetBytes(p)
146 | }
147 |
148 | // GetString returns the value of the given parameter as a string, or a error.
149 | func (k Key) GetString(p any) (string, error) {
150 | return CoseMap(k).GetString(p)
151 | }
152 |
153 | // MarshalCBOR implements the CBOR Marshaler interface for Key.
154 | func (k Key) MarshalCBOR() ([]byte, error) {
155 | return CoseMap(k).MarshalCBOR()
156 | }
157 |
158 | // UnmarshalCBOR implements the CBOR Unmarshaler interface for Key.
159 | func (k *Key) UnmarshalCBOR(data []byte) error {
160 | return (*CoseMap)(k).UnmarshalCBOR(data)
161 | }
162 |
163 | // MarshalText implements encoding/text interface for Key.
164 | func (k Key) MarshalText() ([]byte, error) {
165 | return CoseMap(k).MarshalText()
166 | }
167 |
168 | // UnmarshalText implements encoding/text interface for Key.
169 | func (k *Key) UnmarshalText(text []byte) error {
170 | return (*CoseMap)(k).UnmarshalText(text)
171 | }
172 |
173 | // MarshalJSON implements encoding/json interface for Key.
174 | func (k Key) MarshalJSON() ([]byte, error) {
175 | return CoseMap(k).MarshalJSON()
176 | }
177 |
178 | // UnmarshalJSON implements encoding/json interface for Key.
179 | func (k *Key) UnmarshalJSON(text []byte) error {
180 | return (*CoseMap)(k).UnmarshalJSON(text)
181 | }
182 |
183 | // Bytesify returns a CBOR-encoded byte slice.
184 | // It returns nil if MarshalCBOR failed.
185 | func (k Key) Bytesify() []byte {
186 | return CoseMap(k).Bytesify()
187 | }
188 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | We as members, contributors, and leaders pledge to make participation in our
7 | community a harassment-free experience for everyone, regardless of age, body
8 | size, visible or invisible disability, ethnicity, sex characteristics, gender
9 | identity and expression, level of experience, education, socio-economic status,
10 | nationality, personal appearance, race, caste, color, religion, or sexual
11 | identity and orientation.
12 |
13 | We pledge to act and interact in ways that contribute to an open, welcoming,
14 | diverse, inclusive, and healthy community.
15 |
16 | ## Our Standards
17 |
18 | Examples of behavior that contributes to a positive environment for our
19 | community include:
20 |
21 | * Demonstrating empathy and kindness toward other people
22 | * Being respectful of differing opinions, viewpoints, and experiences
23 | * Giving and gracefully accepting constructive feedback
24 | * Accepting responsibility and apologizing to those affected by our mistakes,
25 | and learning from the experience
26 | * Focusing on what is best not just for us as individuals, but for the overall
27 | community
28 |
29 | Examples of unacceptable behavior include:
30 |
31 | * The use of sexualized language or imagery, and sexual attention or advances of
32 | any kind
33 | * Trolling, insulting or derogatory comments, and personal or political attacks
34 | * Public or private harassment
35 | * Publishing others' private information, such as a physical or email address,
36 | without their explicit permission
37 | * Other conduct which could reasonably be considered inappropriate in a
38 | professional setting
39 |
40 | ## Enforcement Responsibilities
41 |
42 | Community leaders are responsible for clarifying and enforcing our standards of
43 | acceptable behavior and will take appropriate and fair corrective action in
44 | response to any behavior that they deem inappropriate, threatening, offensive,
45 | or harmful.
46 |
47 | Community leaders have the right and responsibility to remove, edit, or reject
48 | comments, commits, code, wiki edits, issues, and other contributions that are
49 | not aligned to this Code of Conduct, and will communicate reasons for moderation
50 | decisions when appropriate.
51 |
52 | ## Scope
53 |
54 | This Code of Conduct applies within all community spaces, and also applies when
55 | an individual is officially representing the community in public spaces.
56 | Examples of representing our community include using an official e-mail address,
57 | posting via an official social media account, or acting as an appointed
58 | representative at an online or offline event.
59 |
60 | ## Enforcement
61 |
62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
63 | reported to the community leaders responsible for enforcement at
64 | txr1883@gmail.com.
65 | All complaints will be reviewed and investigated promptly and fairly.
66 |
67 | All community leaders are obligated to respect the privacy and security of the
68 | reporter of any incident.
69 |
70 | ## Enforcement Guidelines
71 |
72 | Community leaders will follow these Community Impact Guidelines in determining
73 | the consequences for any action they deem in violation of this Code of Conduct:
74 |
75 | ### 1. Correction
76 |
77 | **Community Impact**: Use of inappropriate language or other behavior deemed
78 | unprofessional or unwelcome in the community.
79 |
80 | **Consequence**: A private, written warning from community leaders, providing
81 | clarity around the nature of the violation and an explanation of why the
82 | behavior was inappropriate. A public apology may be requested.
83 |
84 | ### 2. Warning
85 |
86 | **Community Impact**: A violation through a single incident or series of
87 | actions.
88 |
89 | **Consequence**: A warning with consequences for continued behavior. No
90 | interaction with the people involved, including unsolicited interaction with
91 | those enforcing the Code of Conduct, for a specified period of time. This
92 | includes avoiding interactions in community spaces as well as external channels
93 | like social media. Violating these terms may lead to a temporary or permanent
94 | ban.
95 |
96 | ### 3. Temporary Ban
97 |
98 | **Community Impact**: A serious violation of community standards, including
99 | sustained inappropriate behavior.
100 |
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 |
107 | ### 4. Permanent Ban
108 |
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 |
113 | **Consequence**: A permanent ban from any sort of public interaction within the
114 | community.
115 |
116 | ## Attribution
117 |
118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119 | version 2.1, available at
120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121 |
122 | Community Impact Guidelines were inspired by
123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
127 | [https://www.contributor-covenant.org/translations][translations].
128 |
129 | [homepage]: https://www.contributor-covenant.org
130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
131 | [Mozilla CoC]: https://github.com/mozilla/diversity
132 | [FAQ]: https://www.contributor-covenant.org/faq
133 | [translations]: https://www.contributor-covenant.org/translations
134 |
--------------------------------------------------------------------------------
/key/hmac/hmac.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | // Package hmac implements message authentication code algorithm HMAC for COSE as defined in RFC9053.
5 | // https://datatracker.ietf.org/doc/html/rfc9053#name-hash-based-message-authenti.
6 | package hmac
7 |
8 | import (
9 | "crypto/hmac"
10 | "fmt"
11 | "hash"
12 |
13 | "github.com/ldclabs/cose/iana"
14 | "github.com/ldclabs/cose/key"
15 | )
16 |
17 | // GenerateKey generates a new Key with given algorithm for HMAC.
18 | func GenerateKey(alg int) (key.Key, error) {
19 | if alg == iana.AlgorithmReserved {
20 | alg = iana.AlgorithmHMAC_256_64
21 | }
22 |
23 | keySize, _ := getKeySize(key.Alg(alg))
24 | if keySize == 0 {
25 | return nil, fmt.Errorf(`cose/key/hmac: GenerateKey: algorithm mismatch %d`, alg)
26 | }
27 |
28 | k := key.GetRandomBytes(uint16(keySize))
29 | return map[any]any{
30 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
31 | iana.KeyParameterKid: key.SumKid(k), // default kid, can be set to other value.
32 | iana.KeyParameterAlg: alg,
33 | iana.SymmetricKeyParameterK: k, // REQUIRED
34 | }, nil
35 | }
36 |
37 | // KeyFrom returns a Key with given algorithm and bytes for HMAC.
38 | func KeyFrom(alg int, k []byte) (key.Key, error) {
39 | keySize, _ := getKeySize(key.Alg(alg))
40 | if keySize == 0 {
41 | return nil, fmt.Errorf(`cose/key/hmac: KeyFrom: algorithm mismatch %d`, alg)
42 | }
43 | if keySize != len(k) {
44 | return nil, fmt.Errorf(`cose/key/hmac: KeyFrom: invalid key size, expected %d, got %d`,
45 | keySize, len(k))
46 | }
47 |
48 | return map[any]any{
49 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
50 | iana.KeyParameterKid: key.SumKid(k), // default kid, can be set to other value.
51 | iana.KeyParameterAlg: alg,
52 | iana.SymmetricKeyParameterK: append(make([]byte, 0, len(k)), k...), // REQUIRED
53 | }, nil
54 | }
55 |
56 | // CheckKey checks whether the given Key is a valid HMAC key.
57 | func CheckKey(k key.Key) error {
58 | if k.Kty() != iana.KeyTypeSymmetric {
59 | return fmt.Errorf(`cose/key/hmac: CheckKey: invalid key type, expected "Symmetric":4, got %d`, k.Kty())
60 | }
61 |
62 | for p := range k {
63 | switch p {
64 | case iana.KeyParameterKty, iana.KeyParameterKid, iana.SymmetricKeyParameterK:
65 | // continue
66 |
67 | case iana.KeyParameterAlg: // optional
68 | switch k.Alg() {
69 | case iana.AlgorithmHMAC_256_64, iana.AlgorithmHMAC_256_256, iana.AlgorithmHMAC_384_384, iana.AlgorithmHMAC_512_512:
70 | // continue
71 | default:
72 | return fmt.Errorf(`cose/key/hmac: CheckKey: algorithm mismatch %d`, k.Alg())
73 | }
74 |
75 | case iana.KeyParameterKeyOps: // optional
76 | for _, op := range k.Ops() {
77 | switch op {
78 | case iana.KeyOperationMacCreate, iana.KeyOperationMacVerify:
79 | // continue
80 | default:
81 | return fmt.Errorf(`cose/key/hmac: CheckKey: invalid parameter key_ops %d`, op)
82 | }
83 | }
84 |
85 | default:
86 | return fmt.Errorf(`cose/key/hmac: CheckKey: redundant parameter %d`, p)
87 | }
88 | }
89 |
90 | // REQUIRED
91 | kb, err := k.GetBytes(iana.SymmetricKeyParameterK)
92 | if err != nil {
93 | return fmt.Errorf(`cose/key/hmac: CheckKey: invalid parameter k, %v`, err)
94 | }
95 | keySize, _ := getKeySize(k.Alg())
96 | if keySize == 0 {
97 | return fmt.Errorf(`cose/key/hmac: CheckKey: algorithm mismatch %d`, k.Alg())
98 | }
99 |
100 | if len(kb) != keySize {
101 | return fmt.Errorf(`cose/key/hmac: CheckKey: invalid key size, expected %d, got %d`,
102 | keySize, len(kb))
103 | }
104 |
105 | // RECOMMENDED
106 | if k.Has(iana.KeyParameterKid) {
107 | if x, err := k.GetBytes(iana.KeyParameterKid); err != nil || len(x) == 0 {
108 | return fmt.Errorf(`cose/key/hmac: CheckKey: invalid parameter kid`)
109 | }
110 | }
111 | return nil
112 | }
113 |
114 | type hMAC struct {
115 | key key.Key
116 | tagSize int
117 | hash func() hash.Hash
118 | }
119 |
120 | // New creates a key.MACer for the given HMAC key.
121 | func New(k key.Key) (key.MACer, error) {
122 | if err := CheckKey(k); err != nil {
123 | return nil, err
124 | }
125 |
126 | h := k.Alg().HashFunc()
127 | _, tagSize := getKeySize(k.Alg())
128 |
129 | return &hMAC{key: k, tagSize: tagSize, hash: h.New}, nil
130 | }
131 |
132 | // MACCreate implements the key.MACer interface.
133 | // MACCreate computes message authentication code (MAC) for the given data.
134 | func (h *hMAC) MACCreate(data []byte) ([]byte, error) {
135 | if !h.key.Ops().EmptyOrHas(iana.KeyOperationMacCreate) {
136 | return nil, fmt.Errorf("cose/key/hmac: MACCreate: invalid key_ops")
137 | }
138 |
139 | return h.create(data), nil
140 | }
141 |
142 | // MACVerify implements the key.MACer interface.
143 | // MACVerify verifies whether the given MAC is a correct message authentication code (MAC) the given data.
144 | func (h *hMAC) MACVerify(data, mac []byte) error {
145 | if !h.key.Ops().EmptyOrHas(iana.KeyOperationMacVerify) {
146 | return fmt.Errorf("cose/key/hmac: MACVerify: invalid key_ops")
147 | }
148 |
149 | expectedMAC := h.create(data)
150 | if hmac.Equal(expectedMAC, mac) {
151 | return nil
152 | }
153 | return fmt.Errorf("cose/key/hmac: VerifyMAC: invalid MAC")
154 | }
155 |
156 | func (h *hMAC) create(data []byte) []byte {
157 | cek, _ := h.key.GetBytes(iana.SymmetricKeyParameterK)
158 | mac := hmac.New(h.hash, cek)
159 | mac.Write(data) // err should never happen
160 | tag := mac.Sum(nil)
161 | return tag[:h.tagSize]
162 | }
163 |
164 | // Key implements the key.MACer interface.
165 | // Key returns the key in MACer.
166 | func (h *hMAC) Key() key.Key {
167 | return h.key
168 | }
169 |
170 | func getKeySize(alg key.Alg) (keySize, tagSize int) {
171 | switch alg {
172 | case iana.AlgorithmHMAC_256_64:
173 | return 32, 8
174 | case iana.AlgorithmHMAC_256_256:
175 | return 32, 32
176 | case iana.AlgorithmHMAC_384_384:
177 | return 48, 48
178 | case iana.AlgorithmHMAC_512_512:
179 | return 64, 64
180 | default:
181 | return 0, 0
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/key/cosemap_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "fmt"
8 | "math"
9 | "testing"
10 |
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func TestCoseMap(t *testing.T) {
16 | t.Run("common", func(t *testing.T) {
17 | assert := assert.New(t)
18 | require := require.New(t)
19 |
20 | var im *CoseMap
21 | assert.ErrorContains(im.UnmarshalCBOR([]byte{0xa0}), "nil CoseMap")
22 |
23 | type Str string
24 | m1 := CoseMap{
25 | 1: int(1),
26 | 2: int64(2),
27 | 3: int32(3),
28 | -1: int(-1),
29 | -2: int64(-2),
30 | -3: int32(-3),
31 | 0: math.MaxInt64,
32 | 10: []byte{1, 2, 3, 4},
33 | 11: ByteStr{1, 2, 3, 4},
34 | 12: [4]byte{1, 2, 3, 4},
35 | 13: "hello",
36 | 14: []string{"hello"},
37 | 15: Str("hello"),
38 | "hello": "hello",
39 | }
40 |
41 | data := MustMarshalCBOR(m1)
42 | assert.NoError(ValidCBOR(data))
43 | fmt.Printf("%x\n", data)
44 |
45 | var m2 CoseMap
46 | assert.NoError(UnmarshalCBOR(data, &m2))
47 |
48 | mx := CoseMap{}
49 | assert.NoError(UnmarshalCBOR(MustMarshalCBOR(CoseMap{-11: m1}), &mx))
50 | mx[-12] = m2
51 |
52 | m3, err := mx.GetMap(-11)
53 | assert.NoError(err)
54 | m4, err := mx.GetMap(-12)
55 | assert.NoError(err)
56 |
57 | for i, m := range []CoseMap{m1, m2, m3, m4} {
58 | smallInt, err := m.GetInt(1)
59 | assert.NoError(err)
60 | assert.Equal(1, smallInt, fmt.Sprintf("case %d", i))
61 |
62 | smallInt, err = m.GetInt(-1)
63 | assert.NoError(err)
64 | assert.Equal(-1, smallInt)
65 |
66 | smallInt, err = m.GetInt(0)
67 | assert.Error(err)
68 | assert.Equal(0, smallInt)
69 |
70 | smallInt, err = m.GetInt(-10)
71 | assert.NoError(err)
72 | assert.Equal(0, smallInt)
73 |
74 | smallInt, err = m.GetInt(10)
75 | assert.Error(err)
76 | assert.Equal(0, smallInt)
77 |
78 | vInt, err := m.GetInt64(1)
79 | assert.NoError(err)
80 | assert.Equal(int64(1), vInt)
81 |
82 | vInt, err = m.GetInt64(-1)
83 | assert.NoError(err)
84 | assert.Equal(int64(-1), vInt)
85 |
86 | vInt, err = m.GetInt64(0)
87 | assert.NoError(err)
88 | assert.Equal(int64(math.MaxInt64), vInt)
89 |
90 | vInt, err = m.GetInt64(10)
91 | assert.Error(err)
92 | assert.Equal(int64(0), vInt)
93 |
94 | vInt, err = m.GetInt64(-10)
95 | assert.NoError(err)
96 | assert.Equal(int64(0), vInt)
97 |
98 | vUint, err := m.GetUint64(1)
99 | assert.NoError(err)
100 | assert.Equal(uint64(1), vUint)
101 |
102 | vUint, err = m.GetUint64(-1)
103 | assert.Error(err)
104 | assert.Equal(uint64(0), vUint)
105 |
106 | vUint, err = m.GetUint64(-10)
107 | assert.NoError(err)
108 | assert.Equal(uint64(0), vUint)
109 |
110 | vUint, err = m.GetUint64(0)
111 | assert.NoError(err)
112 | assert.Equal(uint64(math.MaxInt64), vUint)
113 |
114 | vUint, err = m.GetUint64(10)
115 | assert.Error(err)
116 | assert.Equal(uint64(0), vUint)
117 |
118 | vb, err := m.GetBytes(1)
119 | assert.Error(err)
120 | assert.Nil(vb)
121 |
122 | vb, err = m.GetBytes(-1)
123 | assert.Error(err)
124 | assert.Nil(vb)
125 |
126 | vb, err = m.GetBytes(-10)
127 | assert.NoError(err)
128 | assert.Nil(vb)
129 |
130 | vb, err = m.GetBytes(10)
131 | assert.NoError(err)
132 | assert.Equal([]byte{1, 2, 3, 4}, vb)
133 |
134 | vb, err = m.GetBytes(11)
135 | assert.NoError(err)
136 | assert.Equal([]byte{1, 2, 3, 4}, vb)
137 |
138 | // vb, err = m.GetBytes(12)
139 | // assert.NoError(err)
140 | // assert.Equal([]byte{1, 2, 3, 4}, vb)
141 |
142 | vb, err = m.GetBytes(13)
143 | assert.Error(err)
144 | assert.Nil(vb)
145 |
146 | vb, err = m.GetBytes(14)
147 | assert.Error(err)
148 | assert.Nil(vb)
149 |
150 | vs, err := m.GetString(1)
151 | assert.Error(err)
152 | assert.Equal("", vs)
153 |
154 | vs, err = m.GetString(-1)
155 | assert.Error(err)
156 | assert.Equal("", vs)
157 |
158 | vs, err = m.GetString(-10)
159 | assert.NoError(err)
160 | assert.Equal("", vs)
161 |
162 | vs, err = m.GetString(13)
163 | assert.NoError(err)
164 | assert.Equal("hello", vs)
165 |
166 | vs, err = m.GetString(14)
167 | assert.Error(err)
168 | assert.Equal("", vs)
169 |
170 | vs, err = m.GetString(15)
171 | assert.NoError(err)
172 | assert.Equal("hello", vs)
173 |
174 | assert.Equal("hello", m.Get("hello"))
175 |
176 | data, err := MarshalCBOR(m)
177 | require.NoError(err)
178 | // CBOR Diagnostic:
179 | // {0: 9223372036854775807, 1: 1, 2: 2, 3: 3, 10: h'01020304', 11: h'01020304', 12: h'01020304', 13: "hello", 14: ["hello"], 15: "hello", -1: -1, -2: -2, -3: -3, "hello": "hello"}
180 | assert.Equal(`ae001b7fffffffffffffff0101020203030a44010203040b44010203040c44010203040d6568656c6c6f0e816568656c6c6f0f6568656c6c6f2020212122226568656c6c6f6568656c6c6f`, ByteStr(data).String())
181 | }
182 | })
183 |
184 | t.Run("GetMap", func(t *testing.T) {
185 | assert := assert.New(t)
186 | require := require.New(t)
187 |
188 | var m1 CoseMap
189 | vm, err := m1.GetMap(1)
190 | require.Nil(err)
191 | require.Nil(vm)
192 |
193 | m1 = CoseMap{
194 | 1: CoseMap{},
195 | }
196 |
197 | vm, err = m1.GetMap(1)
198 | require.NoError(err)
199 | assert.Equal(CoseMap{}, vm)
200 |
201 | m1 = CoseMap{
202 | 1: map[int]any{},
203 | }
204 | vm, err = m1.GetMap(1)
205 | require.NoError(err)
206 | assert.Equal(CoseMap{}, vm)
207 |
208 | m1 = CoseMap{
209 | 1: map[uint]any{},
210 | }
211 | vm, err = m1.GetMap(1)
212 | require.NoError(err)
213 | assert.Equal(CoseMap{}, vm)
214 |
215 | m1 = CoseMap{
216 | 1: map[any]any{
217 | int64(-1): CoseMap{},
218 | },
219 | }
220 | vm, err = m1.GetMap(1)
221 | require.NoError(err)
222 |
223 | vm, err = vm.GetMap(-1)
224 | require.NoError(err)
225 | assert.Equal(CoseMap{}, vm)
226 |
227 | m1 = CoseMap{
228 | 1: map[any]any{
229 | 1: CoseMap{},
230 | "2": CoseMap{},
231 | },
232 | }
233 | _, err = m1.GetMap(1)
234 | require.NoError(err)
235 | })
236 | }
237 |
--------------------------------------------------------------------------------
/iana/key.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package iana
5 |
6 | // IANA-registered COSE common key parameters.
7 | //
8 | // From IANA registry
9 | // as of 2022-12-19.
10 | const (
11 | // Reserved value.
12 | KeyParameterReserved = 0
13 | // Identification of the key type
14 | //
15 | // Associated value of type tstr / int
16 | KeyParameterKty = 1
17 | // Key identification value - match to kid in message
18 | //
19 | // Associated value of type bstr
20 | KeyParameterKid = 2
21 | // Key usage restriction to this algorithm
22 | //
23 | // Associated value of type tstr / int
24 | KeyParameterAlg = 3
25 | // Restrict set of permissible operations
26 | //
27 | // Associated value of type [+ (tstr / int)]
28 | KeyParameterKeyOps = 4
29 | // Base IV to be XORed with Partial IVs
30 | //
31 | // Associated value of type bstr
32 | KeyParameterBaseIV = 5
33 | )
34 |
35 | // IANA-registered COSE key types.
36 | //
37 | // From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type
38 | // as of 2022-12-19.
39 | const (
40 | // This value is reserved
41 | KeyTypeReserved = 0
42 | // Octet Key Pair
43 | KeyTypeOKP = 1
44 | // Elliptic Curve Keys w/ x- and y-coordinate pair
45 | KeyTypeEC2 = 2
46 | // RSA Key
47 | KeyTypeRSA = 3
48 | // Symmetric Keys
49 | KeyTypeSymmetric = 4
50 | // Public key for HSS/LMS hash-based digital signature
51 | KeyTypeHSS_LMS = 5
52 | // WalnutDSA public key
53 | KeyTypeWalnutDSA = 6
54 | )
55 |
56 | // IANA-registered COSE key parameters for keys of type [KeyType::OKP].
57 | //
58 | // From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters
59 | // as of 2022-12-19.
60 | const (
61 | // EC identifier - Taken from the "COSE Elliptic Curves" registry
62 | //
63 | // Associated value of type tstr / int
64 | OKPKeyParameterCrv = -1
65 | // x-coordinate
66 | //
67 | // Associated value of type bstr
68 | OKPKeyParameterX = -2
69 | // Private key
70 | //
71 | // Associated value of type bstr
72 | OKPKeyParameterD = -4
73 | )
74 |
75 | // IANA-registered COSE key parameters for keys of type [KeyType::EC2].
76 | //
77 | // From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters
78 | // as of 2022-12-19.
79 | const (
80 | // EC identifier - Taken from the "COSE Elliptic Curves" registry
81 | //
82 | // Associated value of type tstr / int
83 | EC2KeyParameterCrv = -1
84 | // Public Key
85 | //
86 | // Associated value of type bstr
87 | EC2KeyParameterX = -2
88 | // y-coordinate
89 | //
90 | // Associated value of type bstr / bool
91 | EC2KeyParameterY = -3
92 | // Private key
93 | //
94 | // Associated value of type bstr
95 | EC2KeyParameterD = -4
96 | )
97 |
98 | // IANA-registered COSE key parameters for keys of type [KeyType::RSA].
99 | //
100 | // From IANA registry
101 | // as of 2022-12-19.
102 | const (
103 | // The RSA modulus n
104 | //
105 | // Associated value of type bstr
106 | RSAKeyParameterN = -1
107 | // The RSA public exponent e
108 | //
109 | // Associated value of type bstr
110 | RSAKeyParameterE = -2
111 | // The RSA private exponent d
112 | //
113 | // Associated value of type bstr
114 | RSAKeyParameterD = -3
115 | // The prime factor p of n
116 | //
117 | // Associated value of type bstr
118 | RSAKeyParameterP = -4
119 | // The prime factor q of n
120 | //
121 | // Associated value of type bstr
122 | RSAKeyParameterQ = -5
123 | // dP is d mod (p - 1)
124 | //
125 | // Associated value of type bstr
126 | RSAKeyParameterDP = -6
127 | // dQ is d mod (q - 1)
128 | //
129 | // Associated value of type bstr
130 | RSAKeyParameterDQ = -7
131 | // qInv is the CRT coefficient q^(-1) mod p
132 | //
133 | // Associated value of type bstr
134 | RSAKeyParameterQInv = -8
135 | // Other prime infos, an array
136 | //
137 | // Associated value of type array
138 | RSAKeyParameterOther = -9
139 | // a prime factor r_i of n, where i >= 3
140 | //
141 | // Associated value of type bstr
142 | RSAKeyParameterRI = -10
143 | // d_i = d mod (r_i - 1)
144 | //
145 | // Associated value of type bstr
146 | RSAKeyParameterDI = -11
147 | // The CRT coefficient t_i = (r_1 * r_2 * ... * r_(i-1))^(-1) mod r_i
148 | //
149 | // Associated value of type bstr
150 | RSAKeyParameterTI = -12
151 | )
152 |
153 | // IANA-registered COSE key parameters for keys of type [KeyType::Symmetric].
154 | //
155 | // From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters
156 | // as of 2022-12-19.
157 | const (
158 | // Key Value
159 | //
160 | // Associated value of type bstr
161 | SymmetricKeyParameterK = -1
162 | )
163 |
164 | // IANA-registered COSE key parameters for keys of type [KeyType::HSS_LMS].
165 | //
166 | // From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters
167 | // as of 2022-12-19.
168 | const (
169 | // Public key for HSS/LMS hash-based digital signature
170 | //
171 | // Associated value of type bstr
172 | HSS_LMSKeyParameterPub = -1
173 | )
174 |
175 | // IANA-registered COSE key parameters for keys of type [KeyType::WalnutDSA].
176 | //
177 | // From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters
178 | // as of 2022-12-19.
179 | const (
180 | // Group and Matrix (NxN) size
181 | //
182 | // Associated value of type uint
183 | WalnutDSAKeyParameterN = -1
184 | // Finite field F_q
185 | //
186 | // Associated value of type uint
187 | WalnutDSAKeyParameterQ = -2
188 | // List of T-values, enties in F_q
189 | //
190 | // Associated value of type array of uint
191 | WalnutDSAKeyParameterTValues = -3
192 | // NxN Matrix of enties in F_q in column-major form
193 | //
194 | // Associated value of type array of array of uint
195 | WalnutDSAKeyParameterMatrix1 = -4
196 | // Permutation associated with matrix 1
197 | //
198 | // Associated value of type array of uint
199 | WalnutDSAKeyParameterPermutation1 = -5
200 | // NxN Matrix of enties in F_q in column-major form
201 | //
202 | // Associated value of type array of array of uint
203 | WalnutDSAKeyParameterMatrix2 = -6
204 | )
205 |
--------------------------------------------------------------------------------
/key/hkdf/hkdf_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package hkdf
5 |
6 | import (
7 | "fmt"
8 | "testing"
9 |
10 | "github.com/ldclabs/cose/key"
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func TestHKDF256(t *testing.T) {
16 | assert := assert.New(t)
17 |
18 | // https://github.com/cose-wg/Examples/tree/master/hkdf-hmac-sha-examples
19 | for i, tc := range []struct {
20 | secret []byte
21 | salt []byte
22 | context []byte
23 | keySize int
24 | key []byte
25 | }{
26 | {
27 | key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6"),
28 | nil,
29 | key.HexBytesify("840183F6F6F683F6F6F682188044A1013818"),
30 | 16,
31 | key.HexBytesify("56074D506729CA40C4B4FE50C6439893"),
32 | },
33 | {
34 | key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6"),
35 | nil,
36 | key.HexBytesify("840383F6F6F683F6F6F68219010044A1013818"),
37 | 32,
38 | key.HexBytesify("29CAA7326B683A73C98777707866D8838A3ADC3E3F46C180C54C5AAF01F1CC0C"),
39 | },
40 | {
41 | key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6"),
42 | nil,
43 | key.HexBytesify("840783F6F6F683F6F6F68219020044A1013818"),
44 | 64,
45 | key.HexBytesify("69220077533E89BDA8DA04814ACCB4703E8C9B009033C8F6A7E65DBB3BCA621B2CF279C6842998CB2B4D2BBAD2E6652824F424D7B7004CC2D6A7384086CF5FF8"),
46 | },
47 | {
48 | key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6"),
49 | nil,
50 | nil,
51 | 16,
52 | key.HexBytesify("0A9E2D1F080FDF6686C7DDE0DA3F113C"),
53 | },
54 | } {
55 | testmsg := fmt.Sprintf("test case %d", i)
56 | k, err := HKDF256(tc.secret, tc.salt, tc.context, tc.keySize)
57 | require.NoError(t, err, testmsg)
58 | assert.Equal(tc.key, k, testmsg)
59 | }
60 |
61 | _, err := HKDF256(key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6"), nil, nil, 256*32)
62 | assert.ErrorContains(err, "entropy limit reached")
63 | }
64 |
65 | func TestHKDF512(t *testing.T) {
66 | assert := assert.New(t)
67 |
68 | // https://github.com/cose-wg/Examples/tree/master/hkdf-hmac-sha-examples
69 | for i, tc := range []struct {
70 | secret []byte
71 | salt []byte
72 | context []byte
73 | keySize int
74 | key []byte
75 | }{
76 | {
77 | key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6"),
78 | nil,
79 | key.HexBytesify("840183F6F6F683F6F6F682188044A1013819"),
80 | 16,
81 | key.HexBytesify("7EC6DB8FF17E392A6CB51579F8443976"),
82 | },
83 | {
84 | key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6"),
85 | nil,
86 | key.HexBytesify("840383F6F6F683F6F6F68219010044A1013819"),
87 | 32,
88 | key.HexBytesify("4684AD00BE06914F7B74EE11F70E448D9192EE740182A674A665D7B4692A3EEB"),
89 | },
90 | {
91 | key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6"),
92 | nil,
93 | key.HexBytesify("840783F6F6F683F6F6F68219020044A1013819"),
94 | 64,
95 | key.HexBytesify("ECEAACB6A84FC9FAD2BB2E2C9520A036675BD6894CE41E826E0A5BB98D22403163739A28A2FDFED93675BCC8E46F40EDBEA98D15834F01418A43382D54510DCB"),
96 | },
97 | {
98 | key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6"),
99 | nil,
100 | nil,
101 | 16,
102 | key.HexBytesify("C42FFE41AA6D378EB0BEFE47841D2E28"),
103 | },
104 | } {
105 | testmsg := fmt.Sprintf("test case %d", i)
106 | k, err := HKDF512(tc.secret, tc.salt, tc.context, tc.keySize)
107 | require.NoError(t, err, testmsg)
108 | assert.Equal(tc.key, k, testmsg)
109 | }
110 |
111 | _, err := HKDF512(key.HexBytesify("4B31712E096E5F20B4ECF9790FD8CC7C8B7E2C8AD90BDA81CB224F62C0E7B9A6"), nil, nil, 256*64)
112 | assert.ErrorContains(err, "entropy limit reached")
113 | }
114 |
115 | func TestHKDFAES(t *testing.T) {
116 | assert := assert.New(t)
117 |
118 | // https://github.com/cose-wg/Examples/tree/master/hkdf-aes-examples
119 | for i, tc := range []struct {
120 | secret []byte
121 | context []byte
122 | keySize int
123 | key []byte
124 | }{
125 | {
126 | key.Base64Bytesify("hJtXIZ2uSN5kbQfbtTNWbg"),
127 | key.HexBytesify("840A83F6F6F683F6F6F682188043A1012B"),
128 | 16,
129 | key.HexBytesify("F0CCBAF836D73DA63ED8508EF966EEC9"),
130 | },
131 | {
132 | key.Base64Bytesify("hJtXIZ2uSN5kbQfbtTNWbg"),
133 | key.HexBytesify("840B83F6F6F683F6F6F68219010043A1012B"),
134 | 32,
135 | key.HexBytesify("9F881DC284490E4C2133EBB6946EB6E2172B5B66A9D0E0862B1A7812165CED7F"),
136 | },
137 | {
138 | key.Base64Bytesify("hJtXIZ2uSN5kbQfbtTNWbg"),
139 | key.HexBytesify("840583F6F6F683F6F6F68219010043A1012B"),
140 | 32,
141 | key.HexBytesify("C0DFE3BA00D222CC9FE1C90AA0EF88E7CDB1C67C6C1BE20C5746A909C23F5A6C"),
142 | },
143 | {
144 | key.Base64Bytesify("hJtXIZ2uSN5kbQfbtTNWbg"),
145 | key.HexBytesify("840783F6F6F683F6F6F68219020043A1012B"),
146 | 64,
147 | key.HexBytesify("78FCC6C1395B8CFD3CBB893CCE3483A75B5D829DA2453B99C12E816186F7B95E65FC77C0C9C94495C22215CC6CCC1993893B224E5448B6310D3EC5CD6E1B1E49"),
148 | },
149 | {
150 | key.Base64Bytesify("hJtXIZ2uSN5kbQfbtTNWbg"),
151 | key.HexBytesify("840A834653656E646572F6F68349526563697069656E74F6F682188043A1012B"),
152 | 16,
153 | key.HexBytesify("7CC520F4248B71FB43DECA848CAAB874"),
154 | },
155 | {
156 | key.Base64Bytesify("hJtXIZ2uSN5kbQfbtTNWbg"),
157 | key.HexBytesify("840A83F64453313031F683F64452313032F682188043A1012B"),
158 | 16,
159 | key.HexBytesify("671BBD43EF98AA4B5D265FE93633EED3"),
160 | },
161 | {
162 | key.Base64Bytesify("hJtXIZ2uSN5kbQfbtTNWbg"),
163 | nil,
164 | 16,
165 | key.HexBytesify("E76D66F01225020639F8DBD2EF3990AA"),
166 | },
167 | } {
168 | testmsg := fmt.Sprintf("test case %d", i)
169 | k, err := HKDFAES(tc.secret, tc.context, tc.keySize)
170 | require.NoError(t, err, testmsg)
171 | assert.Equal(tc.key, k, testmsg)
172 | }
173 |
174 | _, err := HKDFAES([]byte{1, 2, 3, 4}, nil, 256*16)
175 | assert.ErrorContains(err, "crypto/aes: invalid key size 4")
176 |
177 | _, err = HKDFAES(key.Base64Bytesify("hJtXIZ2uSN5kbQfbtTNWbg"), nil, 256*16)
178 | assert.ErrorContains(err, "entropy limit reached")
179 | }
180 |
--------------------------------------------------------------------------------
/key/chacha20poly1305/chacha20poly1305.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | // Package chacha20poly1305 implements content encryption algorithm ChaCha20/Poly1305 for COSE as defined in RFC9053.
5 | // https://datatracker.ietf.org/doc/html/rfc9053#name-chacha20-and-poly1305.
6 | package chacha20poly1305
7 |
8 | import (
9 | "fmt"
10 |
11 | gochacha20poly1305 "golang.org/x/crypto/chacha20poly1305"
12 |
13 | "github.com/ldclabs/cose/iana"
14 | "github.com/ldclabs/cose/key"
15 | )
16 |
17 | // GenerateKey generates a new Key with given algorithm for ChaCha20/Poly1305.
18 | func GenerateKey() (key.Key, error) {
19 | k := key.GetRandomBytes(uint16(keySize))
20 | return map[any]any{
21 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
22 | iana.KeyParameterKid: key.SumKid(k), // default kid, can be set to other value.
23 | iana.KeyParameterAlg: iana.AlgorithmChaCha20Poly1305,
24 | iana.SymmetricKeyParameterK: k, // REQUIRED
25 | }, nil
26 | }
27 |
28 | // KeyFrom returns a Key with given algorithm and bytes for ChaCha20/Poly1305.
29 | func KeyFrom(k []byte) (key.Key, error) {
30 | if keySize != len(k) {
31 | return nil, fmt.Errorf(`cose/key/chacha20poly1305: KeyFrom: invalid key size, expected %d, got %d`,
32 | keySize, len(k))
33 | }
34 |
35 | return map[any]any{
36 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
37 | iana.KeyParameterKid: key.SumKid(k), // default kid, can be set to other value.
38 | iana.KeyParameterAlg: iana.AlgorithmChaCha20Poly1305,
39 | iana.SymmetricKeyParameterK: append(make([]byte, 0, len(k)), k...), // REQUIRED
40 | }, nil
41 | }
42 |
43 | // CheckKey checks whether the given key is a valid ChaCha20/Poly1305 key.
44 | func CheckKey(k key.Key) error {
45 | if k.Kty() != iana.KeyTypeSymmetric {
46 | return fmt.Errorf(`cose/key/chacha20poly1305: CheckKey: invalid key type, expected "Symmetric":4, got %d`,
47 | k.Kty())
48 | }
49 |
50 | for p := range k {
51 | switch p {
52 | case iana.KeyParameterKty, iana.KeyParameterKid, iana.SymmetricKeyParameterK, iana.KeyParameterBaseIV:
53 | // continue
54 |
55 | case iana.KeyParameterAlg: // optional
56 | switch k.Alg() {
57 | case iana.AlgorithmChaCha20Poly1305:
58 | // continue
59 | default:
60 | return fmt.Errorf(`cose/key/chacha20poly1305: CheckKey: algorithm mismatch %d`, k.Alg())
61 | }
62 |
63 | case iana.KeyParameterKeyOps: // optional
64 | for _, op := range k.Ops() {
65 | switch op {
66 | case iana.KeyOperationEncrypt, iana.KeyOperationDecrypt:
67 | // continue
68 | default:
69 | return fmt.Errorf(`cose/key/chacha20poly1305: CheckKey: invalid parameter key_ops %d`, op)
70 | }
71 | }
72 |
73 | default:
74 | return fmt.Errorf(`cose/key/chacha20poly1305: CheckKey: redundant parameter %d`, p)
75 | }
76 | }
77 |
78 | // REQUIRED
79 | kb, err := k.GetBytes(iana.SymmetricKeyParameterK)
80 | if err != nil {
81 | return fmt.Errorf(`cose/key/chacha20poly1305: CheckKey: invalid parameter k, %w`, err)
82 | }
83 |
84 | keySize := getKeySize(k.Alg())
85 | if keySize == 0 {
86 | return fmt.Errorf(`cose/key/chacha20poly1305: CheckKey: algorithm mismatch %d`, k.Alg())
87 | }
88 | if len(kb) != keySize {
89 | return fmt.Errorf(`cose/key/chacha20poly1305: CheckKey: invalid key size, expected %d, got %d`,
90 | keySize, len(kb))
91 | }
92 |
93 | // RECOMMENDED
94 | if k.Has(iana.KeyParameterKid) {
95 | if kid, err := k.GetBytes(iana.KeyParameterKid); err != nil || len(kid) == 0 {
96 | return fmt.Errorf(`cose/key/chacha20poly1305: CheckKey: invalid parameter kid`)
97 | }
98 | }
99 | return nil
100 | }
101 |
102 | type chacha struct {
103 | key key.Key
104 | }
105 |
106 | // New creates a key.Encryptor for the given ChaCha20/Poly1305 key.
107 | func New(k key.Key) (key.Encryptor, error) {
108 | if err := CheckKey(k); err != nil {
109 | return nil, err
110 | }
111 |
112 | return &chacha{key: k}, nil
113 | }
114 |
115 | // Encrypt implements the key.Encryptor interface.
116 | // Encrypt encrypts a plaintext with the given iv and additional data.
117 | // It returns the ciphertext or error.
118 | func (h *chacha) Encrypt(iv, plaintext, additionalData []byte) ([]byte, error) {
119 | if !h.key.Ops().EmptyOrHas(iana.KeyOperationEncrypt) {
120 | return nil, fmt.Errorf("cose/key/chacha20poly1305: Encryptor.Encrypt: invalid key_ops")
121 | }
122 |
123 | if len(iv) != nonceSize {
124 | return nil, fmt.Errorf("cose/key/chacha20poly1305: Encryptor.Encrypt: invalid nonce size, expected 12, got %d",
125 | len(iv))
126 | }
127 | cek, _ := h.key.GetBytes(iana.SymmetricKeyParameterK)
128 | aead, err := gochacha20poly1305.New(cek)
129 | if err != nil {
130 | return nil, err
131 | }
132 | ciphertext := aead.Seal(nil, iv, plaintext, additionalData)
133 | return ciphertext, nil
134 | }
135 |
136 | // Decrypt implements the key.Encryptor interface.
137 | // Decrypt decrypts a ciphertext with the given iv and additional data.
138 | // It returns the corresponding plaintext or error.
139 | func (h *chacha) Decrypt(iv, ciphertext, additionalData []byte) ([]byte, error) {
140 | if !h.key.Ops().EmptyOrHas(iana.KeyOperationDecrypt) {
141 | return nil, fmt.Errorf("cose/key/chacha20poly1305: Encryptor.Decrypt: invalid key_ops")
142 | }
143 |
144 | if len(iv) != nonceSize {
145 | return nil, fmt.Errorf("cose/key/chacha20poly1305: Encryptor.Decrypt: invalid nonce size, expected 12, got %d",
146 | len(iv))
147 | }
148 |
149 | cek, _ := h.key.GetBytes(iana.SymmetricKeyParameterK)
150 | aead, err := gochacha20poly1305.New(cek)
151 | if err != nil {
152 | return nil, err
153 | }
154 | return aead.Open(nil, iv, ciphertext, additionalData)
155 | }
156 |
157 | // NonceSize implements the key.Encryptor interface.
158 | // NonceSize returns the size of the nonce for encrypting and decrypting.
159 | // It is: 12 bytes.
160 | func (h *chacha) NonceSize() int {
161 | return nonceSize
162 | }
163 |
164 | // Key implements the key.Encryptor interface.
165 | // Key returns the key in Encryptor.
166 | func (h *chacha) Key() key.Key {
167 | return h.key
168 | }
169 |
170 | const (
171 | keySize = 32
172 | nonceSize = 12
173 | )
174 |
175 | func getKeySize(alg key.Alg) int {
176 | switch alg {
177 | case iana.AlgorithmChaCha20Poly1305:
178 | return keySize
179 | default:
180 | return 0
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/key/aesgcm/aes_gcm.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | // Package aesgcm implements content encryption algorithm AES-GCM for COSE as defined in RFC9053.
5 | // https://datatracker.ietf.org/doc/html/rfc9053#name-aes-gcm.
6 | package aesgcm
7 |
8 | import (
9 | "crypto/aes"
10 | "crypto/cipher"
11 | "fmt"
12 |
13 | "github.com/ldclabs/cose/iana"
14 | "github.com/ldclabs/cose/key"
15 | )
16 |
17 | // GenerateKey generates a new Key with given algorithm for AES-GCM.
18 | func GenerateKey(alg int) (key.Key, error) {
19 | if alg == iana.AlgorithmReserved {
20 | alg = iana.AlgorithmA128GCM
21 | }
22 |
23 | keySize := getKeySize(key.Alg(alg))
24 | if keySize == 0 {
25 | return nil, fmt.Errorf(`cose/key/aesgcm: GenerateKey: algorithm mismatch %d`, alg)
26 | }
27 |
28 | k := key.GetRandomBytes(uint16(keySize))
29 | return map[any]any{
30 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
31 | iana.KeyParameterKid: key.SumKid(k), // default kid, can be set to other value.
32 | iana.KeyParameterAlg: alg,
33 | iana.SymmetricKeyParameterK: k, // REQUIRED
34 | }, nil
35 | }
36 |
37 | // KeyFrom returns a Key with given algorithm and bytes for AES-GCM.
38 | func KeyFrom(alg int, k []byte) (key.Key, error) {
39 | keySize := getKeySize(key.Alg(alg))
40 | if keySize == 0 {
41 | return nil, fmt.Errorf(`cose/key/aesgcm: KeyFrom: algorithm mismatch %d`, alg)
42 | }
43 | if keySize != len(k) {
44 | return nil, fmt.Errorf(`cose/key/aesgcm: KeyFrom: invalid key size, expected %d, got %d`,
45 | keySize, len(k))
46 | }
47 |
48 | return map[any]any{
49 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
50 | iana.KeyParameterKid: key.SumKid(k), // default kid, can be set to other value.
51 | iana.KeyParameterAlg: alg,
52 | iana.SymmetricKeyParameterK: append(make([]byte, 0, len(k)), k...), // REQUIRED
53 | }, nil
54 | }
55 |
56 | // CheckKey checks whether the given key is a valid AES-GCM key.
57 | func CheckKey(k key.Key) error {
58 | if k.Kty() != iana.KeyTypeSymmetric {
59 | return fmt.Errorf(`cose/key/aesgcm: CheckKey: invalid key type, expected "Symmetric":4, got %d`,
60 | k.Kty())
61 | }
62 |
63 | for p := range k {
64 | switch p {
65 | case iana.KeyParameterKty, iana.KeyParameterKid, iana.SymmetricKeyParameterK, iana.KeyParameterBaseIV:
66 | // continue
67 |
68 | case iana.KeyParameterAlg: // optional
69 | switch k.Alg() {
70 | case iana.AlgorithmA128GCM, iana.AlgorithmA192GCM, iana.AlgorithmA256GCM:
71 | // continue
72 | default:
73 | return fmt.Errorf(`cose/key/aesgcm: CheckKey: algorithm mismatch %d`, k.Alg())
74 | }
75 |
76 | case iana.KeyParameterKeyOps: // optional
77 | for _, op := range k.Ops() {
78 | switch op {
79 | case iana.KeyOperationEncrypt, iana.KeyOperationDecrypt:
80 | // continue
81 | default:
82 | return fmt.Errorf(`cose/key/aesgcm: CheckKey: invalid parameter key_ops %d`, op)
83 | }
84 | }
85 |
86 | default:
87 | return fmt.Errorf(`cose/key/aesgcm: CheckKey: redundant parameter %d`, p)
88 | }
89 | }
90 |
91 | // REQUIRED
92 | kb, err := k.GetBytes(iana.SymmetricKeyParameterK)
93 | if err != nil {
94 | return fmt.Errorf(`cose/key/aesgcm: CheckKey: invalid parameter k, %w`, err)
95 | }
96 | keySize := getKeySize(k.Alg())
97 | if keySize == 0 {
98 | return fmt.Errorf(`cose/key/aesgcm: CheckKey: algorithm mismatch %d`, k.Alg())
99 | }
100 |
101 | if len(kb) != keySize {
102 | return fmt.Errorf(`cose/key/aesgcm: CheckKey: invalid key size, expected %d, got %d`,
103 | keySize, len(kb))
104 | }
105 | // RECOMMENDED
106 | if k.Has(iana.KeyParameterKid) {
107 | if kid, err := k.GetBytes(iana.KeyParameterKid); err != nil || len(kid) == 0 {
108 | return fmt.Errorf(`cose/key/aesgcm: CheckKey: invalid parameter kid`)
109 | }
110 | }
111 | return nil
112 | }
113 |
114 | type aesGCM struct {
115 | key key.Key
116 | block cipher.Block
117 | }
118 |
119 | // New creates a key.Encryptor for the given AES-GCM key.
120 | func New(k key.Key) (key.Encryptor, error) {
121 | if err := CheckKey(k); err != nil {
122 | return nil, err
123 | }
124 |
125 | cek, _ := k.GetBytes(iana.SymmetricKeyParameterK)
126 | block, _ := aes.NewCipher(cek) // err should never happen
127 | return &aesGCM{key: k, block: block}, nil
128 | }
129 |
130 | // Encrypt implements the key.Encryptor interface.
131 | // Encrypt encrypts a plaintext with the given iv and additional data.
132 | // It returns the ciphertext or error.
133 | func (h *aesGCM) Encrypt(iv, plaintext, additionalData []byte) ([]byte, error) {
134 | if !h.key.Ops().EmptyOrHas(iana.KeyOperationEncrypt) {
135 | return nil, fmt.Errorf("cose/key/aesgcm: Encryptor.Encrypt: invalid key_ops")
136 | }
137 |
138 | if len(iv) != nonceSize {
139 | return nil, fmt.Errorf("cose/key/aesgcm: Encryptor.Encrypt: invalid nonce size, expected 12, got %d",
140 | len(iv))
141 | }
142 | aead, _ := cipher.NewGCM(h.block) // err should never happen
143 | ciphertext := aead.Seal(nil, iv, plaintext, additionalData)
144 | return ciphertext, nil
145 | }
146 |
147 | // Decrypt implements the key.Encryptor interface.
148 | // Decrypt decrypts a ciphertext with the given iv and additional data.
149 | // It returns the corresponding plaintext or error.
150 | func (h *aesGCM) Decrypt(iv, ciphertext, additionalData []byte) ([]byte, error) {
151 | if !h.key.Ops().EmptyOrHas(iana.KeyOperationDecrypt) {
152 | return nil, fmt.Errorf("cose/key/aesgcm: Encryptor.Decrypt: invalid key_ops")
153 | }
154 |
155 | if len(iv) != nonceSize {
156 | return nil, fmt.Errorf("cose/key/aesgcm: Encryptor.Decrypt: invalid nonce size, expected 12, got %d",
157 | len(iv))
158 | }
159 |
160 | aead, _ := cipher.NewGCM(h.block) // err should never happen
161 | return aead.Open(nil, iv, ciphertext, additionalData)
162 | }
163 |
164 | // NonceSize implements the key.Encryptor interface.
165 | // NonceSize returns the size of the nonce for encrypting and decrypting.
166 | // It is: 12 bytes.
167 | func (h *aesGCM) NonceSize() int {
168 | return nonceSize
169 | }
170 |
171 | // Key implements the key.Encryptor interface.
172 | // Key returns the key in Encryptor.
173 | func (h *aesGCM) Key() key.Key {
174 | return h.key
175 | }
176 |
177 | const (
178 | nonceSize = 12
179 | )
180 |
181 | func getKeySize(alg key.Alg) (keySize int) {
182 | switch alg {
183 | case iana.AlgorithmA128GCM:
184 | return 16
185 | case iana.AlgorithmA192GCM:
186 | return 24
187 | case iana.AlgorithmA256GCM:
188 | return 32
189 | default:
190 | return 0
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/cose/recipient_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose
5 |
6 | import (
7 | "testing"
8 |
9 | "github.com/ldclabs/cose/iana"
10 | "github.com/ldclabs/cose/key"
11 | "github.com/stretchr/testify/assert"
12 | "github.com/stretchr/testify/require"
13 | )
14 |
15 | func TestRecipient(t *testing.T) {
16 | t.Run("common case", func(t *testing.T) {
17 | assert := assert.New(t)
18 |
19 | r1 := &Recipient{}
20 | data, err := r1.MarshalCBOR()
21 | require.NoError(t, err)
22 | assert.Nil(r1.Protected)
23 | assert.Nil(r1.Unprotected)
24 |
25 | r2 := &Recipient{}
26 | assert.NoError(r2.UnmarshalCBOR(data))
27 | assert.Equal(r1.Bytesify(), r2.Bytesify())
28 | assert.Equal(Headers{}, r2.Protected)
29 | assert.Equal(Headers{}, r2.Unprotected)
30 |
31 | r1 = &Recipient{}
32 | data, err = key.MarshalCBOR(r1)
33 | require.NoError(t, err)
34 | assert.Nil(r1.Protected)
35 | assert.Nil(r1.Unprotected)
36 |
37 | r2 = &Recipient{}
38 | assert.NoError(key.UnmarshalCBOR(data, r2))
39 | assert.Equal(r1.Bytesify(), r2.Bytesify())
40 | assert.Equal(Headers{}, r2.Protected)
41 | assert.Equal(Headers{}, r2.Unprotected)
42 |
43 | r1 = &Recipient{
44 | Protected: Headers{},
45 | Unprotected: Headers{
46 | iana.HeaderParameterAlg: iana.AlgorithmDirect,
47 | iana.HeaderParameterKid: []byte("our-secret"),
48 | },
49 | Ciphertext: []byte{},
50 | }
51 | data, err = key.MarshalCBOR(r1)
52 | require.NoError(t, err)
53 |
54 | r2 = &Recipient{}
55 | assert.NoError(key.UnmarshalCBOR(data, r2))
56 | assert.Equal(r1.Bytesify(), r2.Bytesify())
57 |
58 | r2 = &Recipient{
59 | Protected: Headers{
60 | iana.HeaderParameterAlg: iana.AlgorithmECDH_SS_HKDF_256,
61 | },
62 | Unprotected: Headers{
63 | iana.HeaderParameterKid: []byte("our-secret"),
64 | },
65 | Ciphertext: nil,
66 | }
67 | data = r2.Bytesify()
68 | assert.NotEqual(r1.Bytesify(), data)
69 | assert.Equal(byte(0xf6), data[len(data)-1])
70 |
71 | assert.Nil(r1.Recipients())
72 | assert.ErrorContains(r1.AddRecipient(nil), "nil Recipient")
73 | assert.ErrorContains(r1.AddRecipient(r1), "should not add itself")
74 | assert.NoError(r1.AddRecipient(r2))
75 | assert.Equal("Rec_Recipient", r2.context)
76 | assert.Same(r2, r1.Recipients()[0])
77 | assert.ErrorContains(r1.AddRecipient(r2), `should not have "Rec_Recipient" context`)
78 | assert.ErrorContains(r2.AddRecipient(r1), `should not have nested recipients`)
79 |
80 | var r3 Recipient
81 | assert.NoError(key.UnmarshalCBOR(key.MustMarshalCBOR(r1), &r3))
82 | assert.Equal(r1.Bytesify(), r3.Bytesify())
83 | assert.Equal(r2.Bytesify(), r3.Recipients()[0].Bytesify())
84 |
85 | r2 = &Recipient{
86 | Protected: Headers{
87 | iana.HeaderParameterAlg: func() {},
88 | },
89 | Unprotected: Headers{
90 | iana.HeaderParameterKid: []byte("our-secret"),
91 | },
92 | Ciphertext: nil,
93 | }
94 | _, err = r2.MarshalCBOR()
95 | assert.ErrorContains(err, "cbor: unsupported type")
96 | })
97 |
98 | t.Run("Recipient.UnmarshalCBOR", func(t *testing.T) {
99 | assert := assert.New(t)
100 |
101 | var r4 *Recipient
102 | assert.ErrorContains(r4.UnmarshalCBOR([]byte{}), "nil Recipient")
103 |
104 | r4 = &Recipient{}
105 | assert.ErrorContains(r4.UnmarshalCBOR([]byte{}), "empty data")
106 | assert.ErrorContains(r4.UnmarshalCBOR([]byte{0x85}), "invalid data")
107 |
108 | r := &Recipient{
109 | Protected: Headers{},
110 | Unprotected: Headers{
111 | iana.HeaderParameterKid: []byte("our-secret"),
112 | },
113 | Ciphertext: []byte{1, 2, 3, 4},
114 | }
115 | data := key.MustMarshalCBOR(r)
116 | assert.NoError(r4.UnmarshalCBOR(data))
117 | assert.Equal(r.Ciphertext, r4.Ciphertext)
118 | assert.Equal(r.Bytesify(), r4.Bytesify())
119 |
120 | r4 = &Recipient{}
121 | r = &Recipient{
122 | Protected: Headers{
123 | iana.HeaderParameterAlg: iana.AlgorithmECDH_SS_HKDF_256,
124 | },
125 | Unprotected: Headers{
126 | iana.HeaderParameterKid: []byte("our-secret"),
127 | },
128 | Ciphertext: []byte{1, 2, 3, 4},
129 | }
130 | data = key.MustMarshalCBOR(r)
131 | datae := make([]byte, len(data))
132 | copy(datae, data)
133 | assert.Equal(byte(0x01), datae[3])
134 | datae[3] = 0x40
135 | assert.Error(r4.UnmarshalCBOR(datae))
136 |
137 | datae = make([]byte, len(data))
138 | copy(datae, data)
139 | assert.Equal(byte(0x04), datae[7])
140 | datae[7] = 0x40
141 | assert.Error(r4.UnmarshalCBOR(datae))
142 | assert.NoError(r4.UnmarshalCBOR(data))
143 | assert.Equal(r.Ciphertext, r4.Ciphertext)
144 | assert.Equal(r.Bytesify(), r4.Bytesify())
145 |
146 | r4 = &Recipient{}
147 | data = key.MustMarshalCBOR(r)
148 | data[0] = 0x84
149 | data = append(data, 0xf6)
150 | assert.ErrorContains(r4.UnmarshalCBOR(data), "no recipients")
151 |
152 | data[len(data)-1] = 0x80
153 | assert.ErrorContains(r4.UnmarshalCBOR(data), "no recipients")
154 |
155 | data[len(data)-1] = 0x81
156 | data = append(data, 0xf6)
157 | assert.ErrorContains(r4.UnmarshalCBOR(data), "nil Recipient")
158 |
159 | data = key.MustMarshalCBOR(r)
160 | data[0] = 0x84
161 | data = append(data, 0x81)
162 | data = append(data, r.Bytesify()...)
163 | assert.NoError(r4.UnmarshalCBOR(data))
164 | assert.Equal(r.Ciphertext, r4.Ciphertext)
165 | assert.Equal(r.Ciphertext, r4.Recipients()[0].Ciphertext)
166 | assert.Equal(r.Bytesify(), r4.Recipients()[0].Bytesify())
167 |
168 | data = key.MustMarshalCBOR(r)
169 | data[0] = 0x84
170 | data = append(data, 0x81)
171 | data = append(data, r4.Bytesify()...)
172 |
173 | r4 = &Recipient{}
174 | assert.ErrorContains(r4.UnmarshalCBOR(data), "should not have nested recipients")
175 |
176 | r = &Recipient{
177 | Protected: Headers{
178 | iana.HeaderParameterAlg: iana.AlgorithmECDH_SS_HKDF_256,
179 | },
180 | Unprotected: Headers{
181 | iana.HeaderParameterKid: []byte("our-secret"),
182 | },
183 | Ciphertext: []byte{1, 2, 3, 4},
184 | }
185 | r.AddRecipient(&Recipient{
186 | Protected: Headers{},
187 | Unprotected: Headers{
188 | iana.HeaderParameterKid: []byte("our-secret"),
189 | },
190 | })
191 | data = key.MustMarshalCBOR(r)
192 | datae = make([]byte, len(data))
193 | copy(datae, data)
194 | assert.Equal(byte(0x01), datae[3])
195 | datae[3] = 0x40
196 | assert.Error(r4.UnmarshalCBOR(datae))
197 |
198 | datae = make([]byte, len(data))
199 | copy(datae, data)
200 | assert.Equal(byte(0x04), datae[7])
201 | datae[7] = 0x40
202 | assert.Error(r4.UnmarshalCBOR(datae))
203 | assert.NoError(r4.UnmarshalCBOR(data))
204 | assert.Equal(r.Ciphertext, r4.Ciphertext)
205 | assert.Equal(r.Bytesify(), r4.Bytesify())
206 | })
207 | }
208 |
--------------------------------------------------------------------------------
/key/aesmac/aes_mac.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | // Package aesmac implements message authentication code algorithm AES-CBC-MAC for COSE as defined in RFC9053.
5 | // https://datatracker.ietf.org/doc/html/rfc9053#name-hash-based-message-authenti.
6 | package aesmac
7 |
8 | import (
9 | "crypto/aes"
10 | "crypto/cipher"
11 | "crypto/subtle"
12 | "fmt"
13 |
14 | "github.com/ldclabs/cose/iana"
15 | "github.com/ldclabs/cose/key"
16 | )
17 |
18 | // GenerateKey generates a new Key with given algorithm for AES-CBC-MAC.
19 | func GenerateKey(alg int) (key.Key, error) {
20 | if alg == iana.AlgorithmReserved {
21 | alg = iana.AlgorithmAES_MAC_128_64
22 | }
23 |
24 | keySize, _ := getKeySize(key.Alg(alg))
25 | if keySize == 0 {
26 | return nil, fmt.Errorf(`cose/key/aesmac: GenerateKey: algorithm mismatch %d`, alg)
27 | }
28 |
29 | k := key.GetRandomBytes(uint16(keySize))
30 | return map[any]any{
31 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
32 | iana.KeyParameterKid: key.SumKid(k), // default kid, can be set to other value.
33 | iana.KeyParameterAlg: alg,
34 | iana.SymmetricKeyParameterK: k, // REQUIRED
35 | }, nil
36 | }
37 |
38 | // KeyFrom returns a Key with given algorithm and bytes for AES-CBC-MAC.
39 | func KeyFrom(alg int, k []byte) (key.Key, error) {
40 | keySize, _ := getKeySize(key.Alg(alg))
41 | if keySize == 0 {
42 | return nil, fmt.Errorf(`cose/key/aesmac: KeyFrom: algorithm mismatch %d`, alg)
43 | }
44 | if keySize != len(k) {
45 | return nil, fmt.Errorf(`cose/key/aesmac: KeyFrom: invalid key size, expected %d, got %d`, keySize, len(k))
46 | }
47 |
48 | return map[any]any{
49 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
50 | iana.KeyParameterKid: key.SumKid(k), // default kid, can be set to other value.
51 | iana.KeyParameterAlg: alg,
52 | iana.SymmetricKeyParameterK: append(make([]byte, 0, len(k)), k...), // REQUIRED
53 | }, nil
54 | }
55 |
56 | // CheckKey checks whether the given key is a valid AES-CBC-MAC key.
57 | func CheckKey(k key.Key) error {
58 | if k.Kty() != iana.KeyTypeSymmetric {
59 | return fmt.Errorf(`cose/key/aesmac: CheckKey: invalid key type, expected "Symmetric":4, got %d`, k.Kty())
60 | }
61 |
62 | for p := range k {
63 | switch p {
64 | case iana.KeyParameterKty, iana.KeyParameterKid, iana.SymmetricKeyParameterK:
65 | // continue
66 |
67 | case iana.KeyParameterAlg: // optional
68 | switch k.Alg() {
69 | case iana.AlgorithmAES_MAC_128_64, iana.AlgorithmAES_MAC_256_64,
70 | iana.AlgorithmAES_MAC_128_128, iana.AlgorithmAES_MAC_256_128:
71 | // continue
72 | default:
73 | return fmt.Errorf(`cose/key/aesmac: CheckKey: algorithm mismatch %d`, k.Alg())
74 | }
75 |
76 | case iana.KeyParameterKeyOps: // optional
77 | for _, op := range k.Ops() {
78 | switch op {
79 | case iana.KeyOperationMacCreate, iana.KeyOperationMacVerify:
80 | // continue
81 | default:
82 | return fmt.Errorf(`cose/key/aesmac: CheckKey: invalid parameter key_ops %d`, op)
83 | }
84 | }
85 |
86 | default:
87 | return fmt.Errorf(`cose/key/aesmac: CheckKey: redundant parameter %d`, p)
88 | }
89 | }
90 |
91 | // REQUIRED
92 | kb, err := k.GetBytes(iana.SymmetricKeyParameterK)
93 | if err != nil {
94 | return fmt.Errorf(`cose/key/aesmac: CheckKey: invalid parameter k, %w`, err)
95 | }
96 |
97 | keySize, _ := getKeySize(k.Alg())
98 | if keySize == 0 {
99 | return fmt.Errorf(`cose/key/hmac: CheckKey: algorithm mismatch %d`, k.Alg())
100 | }
101 |
102 | if len(kb) != keySize {
103 | return fmt.Errorf(`cose/key/aesmac: CheckKey: invalid key size, expected %d, got %d`,
104 | keySize, len(kb))
105 | }
106 |
107 | // RECOMMENDED
108 | if k.Has(iana.KeyParameterKid) {
109 | if kid, err := k.GetBytes(iana.KeyParameterKid); err != nil || len(kid) == 0 {
110 | return fmt.Errorf(`cose/key/aesmac: CheckKey: invalid parameter kid`)
111 | }
112 | }
113 | return nil
114 | }
115 |
116 | type aesMAC struct {
117 | key key.Key
118 | block cipher.Block
119 | tagSize int
120 | }
121 |
122 | // New creates a key.MACer for the given AES-CBC-MAC key.
123 | func New(k key.Key) (key.MACer, error) {
124 | if err := CheckKey(k); err != nil {
125 | return nil, err
126 | }
127 |
128 | cek, _ := k.GetBytes(iana.SymmetricKeyParameterK)
129 | _, tagSize := getKeySize(k.Alg())
130 | block, _ := aes.NewCipher(cek) // err should never happen
131 | return &aesMAC{key: k, tagSize: tagSize, block: block}, nil
132 | }
133 |
134 | // MACCreate implements the key.MACer interface.
135 | // MACCreate computes message authentication code (MAC) for the given data.
136 | func (h *aesMAC) MACCreate(data []byte) ([]byte, error) {
137 | if !h.key.Ops().EmptyOrHas(iana.KeyOperationMacCreate) {
138 | return nil, fmt.Errorf("cose/key/aesmac: MACer.MACCreate: invalid key_ops")
139 | }
140 |
141 | return h.create(data), nil
142 | }
143 |
144 | // MACVerify implements the key.MACer interface.
145 | // MACVerify verifies whether the given MAC is a correct message authentication code (MAC) the given data.
146 | func (h *aesMAC) MACVerify(data, mac []byte) error {
147 | if !h.key.Ops().EmptyOrHas(iana.KeyOperationMacVerify) {
148 | return fmt.Errorf("cose/key/aesmac: MACer.MACVerify: invalid key_ops")
149 | }
150 |
151 | expectedMAC := h.create(data)
152 | if subtle.ConstantTimeCompare(expectedMAC, mac) == 1 {
153 | return nil
154 | }
155 | return fmt.Errorf("cose/key/aesmac: MACer.MACVerify: invalid MAC")
156 | }
157 |
158 | // the IV is fixed to all zeros
159 | // Reference https://datatracker.ietf.org/doc/html/rfc9053#section-3.2
160 | var fixedIV = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
161 |
162 | func (h *aesMAC) create(plaintext []byte) []byte {
163 | x := len(plaintext) % aes.BlockSize
164 | if x > 0 {
165 | x = aes.BlockSize - x
166 | }
167 |
168 | ciphertext := make([]byte, len(plaintext)+x)
169 | copy(ciphertext, plaintext)
170 | mode := cipher.NewCBCEncrypter(h.block, fixedIV)
171 | mode.CryptBlocks(ciphertext, ciphertext)
172 |
173 | sum := ciphertext[len(ciphertext)-aes.BlockSize:]
174 | tag := make([]byte, h.tagSize)
175 | copy(tag, sum)
176 | return tag
177 | }
178 |
179 | // Key implements the key.MACer interface.
180 | // Key returns the key in MACer.
181 | func (h *aesMAC) Key() key.Key {
182 | return h.key
183 | }
184 |
185 | func getKeySize(alg key.Alg) (keySize, tagSize int) {
186 | switch alg {
187 | case iana.AlgorithmAES_MAC_128_64:
188 | return 16, 8
189 | case iana.AlgorithmAES_MAC_256_64:
190 | return 32, 8
191 | case iana.AlgorithmAES_MAC_128_128:
192 | return 16, 16
193 | case iana.AlgorithmAES_MAC_256_128:
194 | return 32, 16
195 | default:
196 | return 0, 0
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/key/registry_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package key
5 |
6 | import (
7 | "fmt"
8 | "testing"
9 |
10 | "github.com/ldclabs/cose/iana"
11 | "github.com/stretchr/testify/assert"
12 | )
13 |
14 | func TestTripleKey(t *testing.T) {
15 | assert := assert.New(t)
16 |
17 | for i, tc := range []struct {
18 | key Key
19 | tripleKey tripleKey
20 | str string
21 | }{
22 | {
23 | Key{},
24 | tripleKey{0, 0, 0},
25 | "kty(0)_alg(0)",
26 | },
27 | {
28 | Key{
29 | iana.KeyParameterKty: iana.KeyTypeOKP,
30 | iana.KeyParameterAlg: iana.AlgorithmEdDSA,
31 | iana.OKPKeyParameterCrv: iana.EllipticCurveX25519,
32 | },
33 | tripleKey{1, -8, 4},
34 | "kty(1)_alg(-8)_crv(4)",
35 | },
36 | {
37 | Key{
38 | iana.KeyParameterKty: iana.KeyTypeOKP,
39 | iana.KeyParameterAlg: iana.AlgorithmEdDSA,
40 | iana.OKPKeyParameterCrv: iana.EllipticCurveX448,
41 | },
42 | tripleKey{1, -8, 5},
43 | "kty(1)_alg(-8)_crv(5)",
44 | },
45 | {
46 | Key{
47 | iana.KeyParameterKty: iana.KeyTypeOKP,
48 | iana.KeyParameterAlg: iana.AlgorithmEdDSA,
49 | iana.OKPKeyParameterCrv: iana.EllipticCurveEd25519,
50 | },
51 | tripleKey{1, -8, 6},
52 | "kty(1)_alg(-8)_crv(6)",
53 | },
54 | {
55 | Key{
56 | iana.KeyParameterKty: iana.KeyTypeOKP,
57 | iana.KeyParameterAlg: iana.AlgorithmEdDSA,
58 | iana.OKPKeyParameterCrv: iana.EllipticCurveEd448,
59 | },
60 | tripleKey{1, -8, 7},
61 | "kty(1)_alg(-8)_crv(7)",
62 | },
63 | {
64 | Key{
65 | iana.KeyParameterKty: iana.KeyTypeOKP,
66 | iana.OKPKeyParameterCrv: iana.EllipticCurveEd448,
67 | },
68 | tripleKey{1, -8, 7},
69 | "kty(1)_alg(-8)_crv(7)",
70 | },
71 | {
72 | Key{
73 | iana.KeyParameterKty: iana.KeyTypeOKP,
74 | },
75 | tripleKey{1, -8, 6},
76 | "kty(1)_alg(-8)_crv(6)",
77 | },
78 | {
79 | Key{
80 | iana.KeyParameterKty: iana.KeyTypeEC2,
81 | iana.KeyParameterAlg: iana.AlgorithmES256,
82 | iana.OKPKeyParameterCrv: iana.EllipticCurveP_256,
83 | },
84 | tripleKey{2, -7, 1},
85 | "kty(2)_alg(-7)_crv(1)",
86 | },
87 | {
88 | Key{
89 | iana.KeyParameterKty: iana.KeyTypeEC2,
90 | iana.KeyParameterAlg: iana.AlgorithmES512,
91 | iana.OKPKeyParameterCrv: iana.EllipticCurveP_521,
92 | },
93 | tripleKey{2, -36, 3},
94 | "kty(2)_alg(-36)_crv(3)",
95 | },
96 | {
97 | Key{
98 | iana.KeyParameterKty: iana.KeyTypeEC2,
99 | iana.OKPKeyParameterCrv: iana.EllipticCurveP_521,
100 | },
101 | tripleKey{2, -36, 3},
102 | "kty(2)_alg(-36)_crv(3)",
103 | },
104 | {
105 | Key{
106 | iana.KeyParameterKty: iana.KeyTypeEC2,
107 | },
108 | tripleKey{2, -7, 1},
109 | "kty(2)_alg(-7)_crv(1)",
110 | },
111 | {
112 | Key{
113 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
114 | iana.KeyParameterAlg: iana.AlgorithmA128GCM,
115 | },
116 | tripleKey{4, 1, 0},
117 | "kty(4)_alg(1)",
118 | },
119 | } {
120 | tk := tc.key.tripleKey()
121 | assert.Equal(tc.tripleKey, tk, fmt.Sprintf("test case %d", i))
122 | assert.Equal(tc.str, tk.String(), fmt.Sprintf("test case %d", i))
123 | }
124 | }
125 |
126 | func TestRegister(t *testing.T) {
127 | t.Run("RegisterSigner", func(t *testing.T) {
128 | assert := assert.New(t)
129 |
130 | var k Key
131 | _, err := k.Signer()
132 | assert.ErrorContains(err, "nil key")
133 |
134 | k = Key{
135 | iana.KeyParameterKty: iana.KeyTypeOKP,
136 | iana.KeyParameterAlg: -999,
137 | iana.OKPKeyParameterCrv: -999,
138 | }
139 | _, err = k.Signer()
140 | assert.ErrorContains(err, "kty(1)_alg(-999)_crv(-999) is not registered")
141 |
142 | fn := func(Key) (Signer, error) { return nil, nil }
143 | RegisterSigner(iana.KeyTypeOKP, -999, -999, fn)
144 | assert.Panics(func() {
145 | RegisterSigner(iana.KeyTypeOKP, -999, -999, fn)
146 | }, "already registered")
147 |
148 | _, err = k.Signer()
149 | assert.NoError(err)
150 |
151 | delete(signers, k.tripleKey())
152 | _, err = k.Signer()
153 | assert.ErrorContains(err, "kty(1)_alg(-999)_crv(-999) is not registered")
154 | })
155 |
156 | t.Run("RegisterVerifier", func(t *testing.T) {
157 | assert := assert.New(t)
158 |
159 | var k Key
160 | _, err := k.Verifier()
161 | assert.ErrorContains(err, "nil key")
162 |
163 | k = Key{
164 | iana.KeyParameterKty: iana.KeyTypeOKP,
165 | iana.KeyParameterAlg: -999,
166 | iana.OKPKeyParameterCrv: -999,
167 | }
168 | _, err = k.Verifier()
169 | assert.ErrorContains(err, "kty(1)_alg(-999)_crv(-999) is not registered")
170 |
171 | fn := func(Key) (Verifier, error) { return nil, nil }
172 | RegisterVerifier(iana.KeyTypeOKP, -999, -999, fn)
173 | assert.Panics(func() {
174 | RegisterVerifier(iana.KeyTypeOKP, -999, -999, fn)
175 | }, "already registered")
176 |
177 | _, err = k.Verifier()
178 | assert.NoError(err)
179 |
180 | delete(verifiers, k.tripleKey())
181 | _, err = k.Verifier()
182 | assert.ErrorContains(err, "kty(1)_alg(-999)_crv(-999) is not registered")
183 | })
184 |
185 | t.Run("RegisterMACer", func(t *testing.T) {
186 | assert := assert.New(t)
187 |
188 | var k Key
189 | _, err := k.MACer()
190 | assert.ErrorContains(err, "nil key")
191 |
192 | k = Key{
193 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
194 | iana.KeyParameterAlg: -999,
195 | }
196 | _, err = k.MACer()
197 | assert.ErrorContains(err, "kty(4)_alg(-999) is not registered")
198 |
199 | fn := func(Key) (MACer, error) { return nil, nil }
200 | RegisterMACer(iana.KeyTypeSymmetric, -999, fn)
201 | assert.Panics(func() {
202 | RegisterMACer(iana.KeyTypeSymmetric, -999, fn)
203 | }, "already registered")
204 |
205 | _, err = k.MACer()
206 | assert.NoError(err)
207 |
208 | delete(macers, k.tripleKey())
209 | _, err = k.MACer()
210 | assert.ErrorContains(err, "kty(4)_alg(-999) is not registered")
211 | })
212 |
213 | t.Run("RegisterEncryptor", func(t *testing.T) {
214 | assert := assert.New(t)
215 |
216 | var k Key
217 | _, err := k.Encryptor()
218 | assert.ErrorContains(err, "nil key")
219 |
220 | k = Key{
221 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
222 | iana.KeyParameterAlg: -999,
223 | }
224 | _, err = k.Encryptor()
225 | assert.ErrorContains(err, "kty(4)_alg(-999) is not registered")
226 |
227 | fn := func(Key) (Encryptor, error) { return nil, nil }
228 | RegisterEncryptor(iana.KeyTypeSymmetric, -999, fn)
229 | assert.Panics(func() {
230 | RegisterEncryptor(iana.KeyTypeSymmetric, -999, fn)
231 | }, "already registered")
232 |
233 | _, err = k.Encryptor()
234 | assert.NoError(err)
235 |
236 | delete(encryptors, k.tripleKey())
237 | _, err = k.Encryptor()
238 | assert.ErrorContains(err, "kty(4)_alg(-999) is not registered")
239 | })
240 | }
241 |
--------------------------------------------------------------------------------
/cose/mac0.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose
5 |
6 | import (
7 | "bytes"
8 | "errors"
9 | "fmt"
10 |
11 | "github.com/fxamacker/cbor/v2"
12 | "github.com/ldclabs/cose/iana"
13 | "github.com/ldclabs/cose/key"
14 | )
15 |
16 | // Mac0Message represents a COSE_Mac0 object.
17 | //
18 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-signing-with-one-signer.
19 | type Mac0Message[T any] struct {
20 | // protected header parameters: iana.HeaderParameterAlg, iana.HeaderParameterCrit.
21 | Protected Headers
22 | // Other header parameters.
23 | Unprotected Headers
24 | // If payload is []byte or cbor.RawMessage,
25 | // it will not be encoded/decoded by key.MarshalCBOR/key.UnmarshalCBOR.
26 | Payload T
27 |
28 | mm *mac0Message
29 | toMac []byte
30 | }
31 |
32 | // VerifyMac0Message verifies and decodes a COSE_Mac0 object with a MACer and returns a *Mac0Message.
33 | // `externalData` should be the same as the one used when computing.
34 | func VerifyMac0Message[T any](macer key.MACer, coseData, externalData []byte) (*Mac0Message[T], error) {
35 | m := &Mac0Message[T]{}
36 | if err := m.UnmarshalCBOR(coseData); err != nil {
37 | return nil, err
38 | }
39 | if err := m.Verify(macer, externalData); err != nil {
40 | return nil, err
41 | }
42 | return m, nil
43 | }
44 |
45 | // ComputeAndEncode computes and encodes a COSE_Mac0 object with a MACer.
46 | // `externalData` can be nil. https://datatracker.ietf.org/doc/html/rfc9052#name-externally-supplied-data.
47 | func (m *Mac0Message[T]) ComputeAndEncode(macer key.MACer, externalData []byte) ([]byte, error) {
48 | if err := m.Compute(macer, externalData); err != nil {
49 | return nil, err
50 | }
51 | return m.MarshalCBOR()
52 | }
53 |
54 | // Compute computes a COSE_Mac0 object' MAC with a MACer.
55 | // `externalData` can be nil. https://datatracker.ietf.org/doc/html/rfc9052#name-externally-supplied-data.
56 | func (m *Mac0Message[T]) Compute(macer key.MACer, externalData []byte) error {
57 | if m.Protected == nil {
58 | m.Protected = Headers{}
59 |
60 | if alg := macer.Key().Alg(); alg != iana.AlgorithmReserved {
61 | m.Protected[iana.HeaderParameterAlg] = alg
62 | }
63 | } else if m.Protected.Has(iana.HeaderParameterAlg) {
64 | alg, _ := m.Protected.GetInt(iana.HeaderParameterAlg)
65 | if alg != int(macer.Key().Alg()) {
66 | return fmt.Errorf("cose/cose: Mac0Message.Compute: macer'alg mismatch, expected %d, got %d",
67 | alg, macer.Key().Alg())
68 | }
69 | }
70 |
71 | if m.Unprotected == nil {
72 | m.Unprotected = Headers{}
73 |
74 | if kid := macer.Key().Kid(); len(kid) > 0 {
75 | m.Unprotected[iana.HeaderParameterKid] = kid
76 | }
77 | }
78 |
79 | mm := &mac0Message{
80 | Protected: []byte{},
81 | Unprotected: m.Unprotected,
82 | }
83 |
84 | var err error
85 | if mm.Protected, err = m.Protected.Bytes(); err != nil {
86 | return err
87 | }
88 |
89 | switch v := any(m.Payload).(type) {
90 | case []byte:
91 | mm.Payload = v
92 | case cbor.RawMessage:
93 | mm.Payload = v
94 | default:
95 | mm.Payload, err = key.MarshalCBOR(m.Payload)
96 | if err != nil {
97 | return err
98 | }
99 | }
100 |
101 | m.toMac = mm.toMac(externalData)
102 |
103 | if mm.Tag, err = macer.MACCreate(m.toMac); err == nil {
104 | m.mm = mm
105 | }
106 | return err
107 | }
108 |
109 | // Verify verifies a COSE_Mac0 object' MAC with a MACer.
110 | // It should call `Mac0Message.UnmarshalCBOR` before calling this method.
111 | // `externalData` should be the same as the one used when computing.
112 | func (m *Mac0Message[T]) Verify(macer key.MACer, externalData []byte) error {
113 | if m.mm == nil || m.mm.Tag == nil {
114 | return errors.New("cose/cose: Mac0Message.Verify: should call Mac0Message.UnmarshalCBOR")
115 | }
116 |
117 | if m.Protected.Has(iana.HeaderParameterAlg) {
118 | alg, _ := m.Protected.GetInt(iana.HeaderParameterAlg)
119 | if alg != int(macer.Key().Alg()) {
120 | return fmt.Errorf("cose/cose: Mac0Message.Verify: macer'alg mismatch, expected %d, got %d",
121 | alg, macer.Key().Alg())
122 | }
123 | }
124 |
125 | m.toMac = m.mm.toMac(externalData)
126 | return macer.MACVerify(m.toMac, m.mm.Tag)
127 | }
128 |
129 | // mac0Message represents a COSE_Mac0 structure to encode and decode.
130 | type mac0Message struct {
131 | _ struct{} `cbor:",toarray"`
132 | Protected []byte
133 | Unprotected Headers
134 | Payload []byte // can be nil
135 | Tag []byte
136 | }
137 |
138 | func (mm *mac0Message) toMac(external_aad []byte) []byte {
139 | if external_aad == nil {
140 | external_aad = []byte{}
141 | }
142 | // MAC_structure https://datatracker.ietf.org/doc/html/rfc9052#name-how-to-compute-and-verify-a
143 | return key.MustMarshalCBOR([]any{
144 | "MAC0", // context
145 | mm.Protected, // body_protected
146 | external_aad, // external_aad
147 | mm.Payload, // payload
148 | })
149 | }
150 |
151 | // MarshalCBOR implements the CBOR Marshaler interface for Mac0Message.
152 | // It should call `Mac0Message.WithSign` before calling this method.
153 | func (m *Mac0Message[T]) MarshalCBOR() ([]byte, error) {
154 | if m.mm == nil || m.mm.Tag == nil {
155 | return nil, errors.New("cose/cose: Mac0Message.MarshalCBOR: should call Mac0Message.Compute")
156 | }
157 |
158 | return key.MarshalCBOR(cbor.Tag{
159 | Number: iana.CBORTagCOSEMac0,
160 | Content: m.mm,
161 | })
162 | }
163 |
164 | // UnmarshalCBOR implements the CBOR Unmarshaler interface for Mac0Message.
165 | func (m *Mac0Message[T]) UnmarshalCBOR(data []byte) error {
166 | if m == nil {
167 | return errors.New("cose/cose: Mac0Message.UnmarshalCBOR: nil Mac0Message")
168 | }
169 |
170 | if bytes.HasPrefix(data, cwtPrefix) {
171 | data = data[2:]
172 | }
173 |
174 | // support untagged message
175 | if bytes.HasPrefix(data, mac0MessagePrefix) {
176 | data = data[1:]
177 | }
178 |
179 | var err error
180 | mm := &mac0Message{}
181 | if err = key.UnmarshalCBOR(data, mm); err != nil {
182 | return err
183 | }
184 |
185 | if m.Protected, err = HeadersFromBytes(mm.Protected); err != nil {
186 | return err
187 | }
188 |
189 | if len(mm.Payload) > 0 {
190 | switch any(m.Payload).(type) {
191 | case []byte:
192 | m.Payload = any(mm.Payload).(T)
193 | case cbor.RawMessage:
194 | m.Payload = any(cbor.RawMessage(mm.Payload)).(T)
195 | default:
196 | if err := key.UnmarshalCBOR(mm.Payload, &m.Payload); err != nil {
197 | return err
198 | }
199 | }
200 | }
201 |
202 | m.Unprotected = mm.Unprotected
203 | m.mm = mm
204 | return nil
205 | }
206 |
207 | // Bytesify returns a CBOR-encoded byte slice.
208 | // It returns nil if MarshalCBOR failed.
209 | func (m *Mac0Message[T]) Bytesify() []byte {
210 | b, _ := m.MarshalCBOR()
211 | return b
212 | }
213 |
214 | // Tag returns the MAC tag of the Mac0Message.
215 | // If the MAC is not computed, it returns nil.
216 | func (m *Mac0Message[T]) Tag() []byte {
217 | if m.mm == nil {
218 | return nil
219 | }
220 | return m.mm.Tag
221 | }
222 |
--------------------------------------------------------------------------------
/cose/sign1.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cose
5 |
6 | import (
7 | "bytes"
8 | "errors"
9 | "fmt"
10 |
11 | "github.com/fxamacker/cbor/v2"
12 |
13 | "github.com/ldclabs/cose/iana"
14 | "github.com/ldclabs/cose/key"
15 | )
16 |
17 | // Sign1Message represents a COSE_Sign1 object.
18 | //
19 | // Reference https://datatracker.ietf.org/doc/html/rfc9052#name-signing-with-one-signer
20 | type Sign1Message[T any] struct {
21 | // protected header parameters: iana.HeaderParameterAlg, iana.HeaderParameterCrit.
22 | Protected Headers
23 | // Other header parameters.
24 | Unprotected Headers
25 | // If payload is []byte or cbor.RawMessage,
26 | // it will not be encoded/decoded by key.MarshalCBOR/key.UnmarshalCBOR.
27 | Payload T
28 |
29 | mm *sign1Message
30 | toSign []byte
31 | }
32 |
33 | // VerifySign1Message verifies and decodes a COSE_Sign1 message with a Verifier and returns a *Sign1Message.
34 | // `externalData` should be the same as the one used when signing.
35 | func VerifySign1Message[T any](verifier key.Verifier, coseData, externalData []byte) (*Sign1Message[T], error) {
36 | m := &Sign1Message[T]{}
37 | if err := m.UnmarshalCBOR(coseData); err != nil {
38 | return nil, err
39 | }
40 | if err := m.Verify(verifier, externalData); err != nil {
41 | return nil, err
42 | }
43 | return m, nil
44 | }
45 |
46 | // SignAndEncode signs and encodes a COSE_Sign1 message with a Signer.
47 | // `externalData` can be nil. https://datatracker.ietf.org/doc/html/rfc9052#name-externally-supplied-data
48 | func (m *Sign1Message[T]) SignAndEncode(signer key.Signer, externalData []byte) ([]byte, error) {
49 | if err := m.WithSign(signer, externalData); err != nil {
50 | return nil, err
51 | }
52 | return m.MarshalCBOR()
53 | }
54 |
55 | // WithSign signs a COSE_Sign1 message with a Signer.
56 | // `externalData` can be nil. https://datatracker.ietf.org/doc/html/rfc9052#name-externally-supplied-data
57 | func (m *Sign1Message[T]) WithSign(signer key.Signer, externalData []byte) error {
58 | if m.Protected == nil {
59 | m.Protected = Headers{}
60 |
61 | if alg := signer.Key().Alg(); alg != iana.AlgorithmReserved {
62 | m.Protected[iana.HeaderParameterAlg] = alg
63 | }
64 | } else if m.Protected.Has(iana.HeaderParameterAlg) {
65 | alg, _ := m.Protected.GetInt(iana.HeaderParameterAlg)
66 | if alg != int(signer.Key().Alg()) {
67 | return fmt.Errorf("cose/cose: Sign1Message.WithSign: signer'alg mismatch, expected %d, got %d",
68 | alg, signer.Key().Alg())
69 | }
70 | }
71 |
72 | if m.Unprotected == nil {
73 | m.Unprotected = Headers{}
74 |
75 | if kid := signer.Key().Kid(); len(kid) > 0 {
76 | m.Unprotected[iana.HeaderParameterKid] = kid
77 | }
78 | }
79 |
80 | mm := &sign1Message{
81 | Unprotected: m.Unprotected,
82 | }
83 |
84 | var err error
85 | if mm.Protected, err = m.Protected.Bytes(); err != nil {
86 | return err
87 | }
88 |
89 | switch v := any(m.Payload).(type) {
90 | case []byte:
91 | mm.Payload = v
92 | case cbor.RawMessage:
93 | mm.Payload = v
94 | default:
95 | if mm.Payload, err = key.MarshalCBOR(m.Payload); err != nil {
96 | return err
97 | }
98 | }
99 |
100 | m.toSign = mm.toSign(externalData)
101 | if mm.Signature, err = signer.Sign(m.toSign); err == nil {
102 | m.mm = mm
103 | }
104 | return err
105 | }
106 |
107 | // Verify verifies a COSE_Sign1 message with a Verifier.
108 | // It should call `Sign1Message.UnmarshalCBOR` before calling this method.
109 | // `externalData` should be the same as the one used when signing.
110 | func (m *Sign1Message[T]) Verify(verifier key.Verifier, externalData []byte) error {
111 | if m.mm == nil || m.mm.Signature == nil {
112 | return errors.New("cose/cose: Sign1Message.Verify: should call Sign1Message.UnmarshalCBOR")
113 | }
114 |
115 | if m.Protected.Has(iana.HeaderParameterAlg) {
116 | alg, _ := m.Protected.GetInt(iana.HeaderParameterAlg)
117 | if alg != int(verifier.Key().Alg()) {
118 | return fmt.Errorf("cose/cose: Sign1Message.Verify: verifier'alg mismatch, expected %d, got %d",
119 | alg, verifier.Key().Alg())
120 | }
121 | }
122 |
123 | m.toSign = m.mm.toSign(externalData)
124 | return verifier.Verify(m.toSign, m.mm.Signature)
125 | }
126 |
127 | // sign1Message represents a COSE_Sign1 structure to encode and decode.
128 | type sign1Message struct {
129 | _ struct{} `cbor:",toarray"`
130 | Protected []byte
131 | Unprotected Headers
132 | Payload []byte // can be nil
133 | Signature []byte
134 | }
135 |
136 | func (mm *sign1Message) toSign(external_aad []byte) []byte {
137 | if external_aad == nil {
138 | external_aad = []byte{}
139 | }
140 | // Sig_structure https://datatracker.ietf.org/doc/html/rfc9052#name-signing-and-verification-pr
141 | return key.MustMarshalCBOR([]any{
142 | "Signature1", // context
143 | mm.Protected, // body_protected
144 | external_aad, // external_aad
145 | mm.Payload, // payload
146 | })
147 | }
148 |
149 | // MarshalCBOR implements the CBOR Marshaler interface for Sign1Message.
150 | // It should call `Sign1Message.WithSign` before calling this method.
151 | func (m *Sign1Message[T]) MarshalCBOR() ([]byte, error) {
152 | if m.mm == nil || m.mm.Signature == nil {
153 | return nil, errors.New("cose/cose: Sign1Message.MarshalCBOR: should call Sign1Message.WithSign")
154 | }
155 |
156 | return key.MarshalCBOR(cbor.Tag{
157 | Number: iana.CBORTagCOSESign1,
158 | Content: m.mm,
159 | })
160 | }
161 |
162 | // UnmarshalCBOR implements the CBOR Unmarshaler interface for Sign1Message.
163 | func (m *Sign1Message[T]) UnmarshalCBOR(data []byte) error {
164 | if m == nil {
165 | return errors.New("cose/cose: Sign1Message.UnmarshalCBOR: nil Sign1Message")
166 | }
167 |
168 | if bytes.HasPrefix(data, cwtPrefix) {
169 | data = data[2:]
170 | }
171 |
172 | if bytes.HasPrefix(data, sign1MessagePrefix) {
173 | data = data[1:]
174 | }
175 |
176 | var err error
177 | mm := &sign1Message{}
178 | if err = key.UnmarshalCBOR(data, mm); err != nil {
179 | return err
180 | }
181 |
182 | if m.Protected, err = HeadersFromBytes(mm.Protected); err != nil {
183 | return err
184 | }
185 |
186 | if len(mm.Payload) > 0 {
187 | switch any(m.Payload).(type) {
188 | case []byte:
189 | m.Payload = any(mm.Payload).(T)
190 | case cbor.RawMessage:
191 | m.Payload = any(cbor.RawMessage(mm.Payload)).(T)
192 | default:
193 | if err := key.UnmarshalCBOR(mm.Payload, &m.Payload); err != nil {
194 | return err
195 | }
196 | }
197 | }
198 |
199 | m.Unprotected = mm.Unprotected
200 | m.mm = mm
201 | return nil
202 | }
203 |
204 | // Bytesify returns a CBOR-encoded byte slice.
205 | // It returns nil if MarshalCBOR failed.
206 | func (m *Sign1Message[T]) Bytesify() []byte {
207 | b, _ := m.MarshalCBOR()
208 | return b
209 | }
210 |
211 | // Signature returns the signature of the Sign1Message.
212 | // If the Sign1Message is not signed, it returns nil.
213 | func (m *Sign1Message[T]) Signature() []byte {
214 | if m.mm == nil {
215 | return nil
216 | }
217 | return m.mm.Signature
218 | }
219 |
--------------------------------------------------------------------------------
/key/aesccm/aes_ccm.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | // Package aesccm implements content encryption algorithm AES-CCM for COSE as defined in RFC9053.
5 | // https://datatracker.ietf.org/doc/html/rfc9053#name-aes-ccm.
6 | package aesccm
7 |
8 | import (
9 | "crypto/aes"
10 | "crypto/cipher"
11 | "fmt"
12 |
13 | "github.com/ldclabs/cose/iana"
14 | "github.com/ldclabs/cose/key"
15 | )
16 |
17 | // GenerateKey generates a new Key with given algorithm for AES-CCM.
18 | func GenerateKey(alg int) (key.Key, error) {
19 | if alg == iana.AlgorithmReserved {
20 | alg = iana.AlgorithmAES_CCM_16_64_128
21 | }
22 |
23 | keySize, _, _ := getKeySize(key.Alg(alg))
24 | if keySize == 0 {
25 | return nil, fmt.Errorf(`cose/key/aesccm: GenerateKey: algorithm mismatch %d`, alg)
26 | }
27 |
28 | k := key.GetRandomBytes(uint16(keySize))
29 | return map[any]any{
30 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
31 | iana.KeyParameterKid: key.SumKid(k), // default kid, can be set to other value.
32 | iana.KeyParameterAlg: alg,
33 | iana.SymmetricKeyParameterK: k, // REQUIRED
34 | }, nil
35 | }
36 |
37 | // KeyFrom returns a Key with given algorithm and bytes for AES-CCM.
38 | func KeyFrom(alg int, k []byte) (key.Key, error) {
39 | keySize, _, _ := getKeySize(key.Alg(alg))
40 | if keySize == 0 {
41 | return nil, fmt.Errorf(`cose/key/aesccm: KeyFrom: algorithm mismatch %d`, alg)
42 | }
43 |
44 | if keySize != len(k) {
45 | return nil, fmt.Errorf(`cose/key/aesccm: KeyFrom: invalid key size, expected %d, got %d`,
46 | keySize, len(k))
47 | }
48 |
49 | return map[any]any{
50 | iana.KeyParameterKty: iana.KeyTypeSymmetric,
51 | iana.KeyParameterKid: key.SumKid(k), // default kid, can be set to other value.
52 | iana.KeyParameterAlg: alg,
53 | iana.SymmetricKeyParameterK: append(make([]byte, 0, len(k)), k...), // REQUIRED
54 | }, nil
55 | }
56 |
57 | // CheckKey checks whether the given key is a valid AES-CCM key.
58 | func CheckKey(k key.Key) error {
59 | if k.Kty() != iana.KeyTypeSymmetric {
60 | return fmt.Errorf(`cose/key/aesccm: CheckKey: invalid key type, expected "Symmetric":4, got %d`,
61 | k.Kty())
62 | }
63 |
64 | for p := range k {
65 | switch p {
66 | case iana.KeyParameterKty, iana.KeyParameterKid, iana.SymmetricKeyParameterK, iana.KeyParameterBaseIV:
67 | // continue
68 |
69 | case iana.KeyParameterAlg: // optional
70 | switch k.Alg() {
71 | case iana.AlgorithmAES_CCM_16_64_128, iana.AlgorithmAES_CCM_16_64_256, iana.AlgorithmAES_CCM_64_64_128,
72 | iana.AlgorithmAES_CCM_64_64_256, iana.AlgorithmAES_CCM_16_128_128, iana.AlgorithmAES_CCM_16_128_256,
73 | iana.AlgorithmAES_CCM_64_128_128, iana.AlgorithmAES_CCM_64_128_256:
74 | // continue
75 | default:
76 | return fmt.Errorf(`cose/key/aesccm: CheckKey: algorithm mismatch %d`, k.Alg())
77 | }
78 |
79 | case iana.KeyParameterKeyOps: // optional
80 | for _, op := range k.Ops() {
81 | switch op {
82 | case iana.KeyOperationEncrypt, iana.KeyOperationDecrypt:
83 | // continue
84 | default:
85 | return fmt.Errorf(`cose/key/aesccm: CheckKey: invalid parameter key_ops %d`, op)
86 | }
87 | }
88 |
89 | default:
90 | return fmt.Errorf(`cose/key/aesccm: CheckKey: redundant parameter %d`, p)
91 | }
92 | }
93 |
94 | // REQUIRED
95 | kb, err := k.GetBytes(iana.SymmetricKeyParameterK)
96 | if err != nil {
97 | return fmt.Errorf(`cose/key/aesccm: CheckKey: invalid parameter k, %w`, err)
98 | }
99 |
100 | keySize, _, _ := getKeySize(k.Alg())
101 | if keySize == 0 {
102 | return fmt.Errorf(`cose/key/aesccm: CheckKey: algorithm mismatch %d`, k.Alg())
103 | }
104 |
105 | if len(kb) != keySize {
106 | return fmt.Errorf(`cose/key/aesccm: CheckKey: invalid key size, expected %d, got %d`,
107 | keySize, len(kb))
108 | }
109 | // RECOMMENDED
110 | if k.Has(iana.KeyParameterKid) {
111 | if kid, err := k.GetBytes(iana.KeyParameterKid); err != nil || len(kid) == 0 {
112 | return fmt.Errorf(`cose/key/aesccm: CheckKey: invalid parameter kid`)
113 | }
114 | }
115 | return nil
116 | }
117 |
118 | type aesCCM struct {
119 | key key.Key
120 | block cipher.Block
121 | ivSize int
122 | }
123 |
124 | // New creates a key.Encryptor for the given AES-CCM key.
125 | func New(k key.Key) (key.Encryptor, error) {
126 | if err := CheckKey(k); err != nil {
127 | return nil, err
128 | }
129 |
130 | cek, _ := k.GetBytes(iana.SymmetricKeyParameterK)
131 | block, _ := aes.NewCipher(cek) // err should never happen
132 | _, _, nonceSize := getKeySize(k.Alg())
133 | return &aesCCM{key: k, block: block, ivSize: nonceSize}, nil
134 | }
135 |
136 | // Encrypt implements the key.Encryptor interface.
137 | // Encrypt encrypts a plaintext with the given iv and additional data.
138 | // It returns the ciphertext or error.
139 | func (h *aesCCM) Encrypt(iv, plaintext, additionalData []byte) ([]byte, error) {
140 | if !h.key.Ops().EmptyOrHas(iana.KeyOperationEncrypt) {
141 | return nil, fmt.Errorf("cose/key/aesccm: Encryptor.Encrypt: invalid key_ops")
142 | }
143 |
144 | _, tagSize, nonceSize := getKeySize(h.key.Alg())
145 | if len(iv) != nonceSize {
146 | return nil, fmt.Errorf("cose/key/aesccm: Encryptor.Encrypt: invalid nonce size, expected %d, got %d",
147 | nonceSize, len(iv))
148 | }
149 | aead, _ := NewCCM(h.block, tagSize, nonceSize) // err should never happen
150 | ciphertext := aead.Seal(nil, iv, plaintext, additionalData)
151 | return ciphertext, nil
152 | }
153 |
154 | // Decrypt implements the key.Encryptor interface.
155 | // Decrypt decrypts a ciphertext with the given iv and additional data.
156 | // It returns the corresponding plaintext or error.
157 | func (h *aesCCM) Decrypt(iv, ciphertext, additionalData []byte) ([]byte, error) {
158 | if !h.key.Ops().EmptyOrHas(iana.KeyOperationDecrypt) {
159 | return nil, fmt.Errorf("cose/key/aesccm: Encryptor.Decrypt: invalid key_ops")
160 | }
161 | _, tagSize, nonceSize := getKeySize(h.key.Alg())
162 | if len(iv) != nonceSize {
163 | return nil, fmt.Errorf("cose/key/aesccm: Encryptor.Decrypt: invalid nonce size, expected %d, got %d",
164 | nonceSize, len(iv))
165 | }
166 |
167 | aead, _ := NewCCM(h.block, tagSize, nonceSize) // err should never happen
168 | return aead.Open(nil, iv, ciphertext, additionalData)
169 | }
170 |
171 | // NonceSize implements the key.Encryptor interface.
172 | // NonceSize returns the size of the nonce for encrypting and decrypting.
173 | // It is: 7 bytes or 13 bytes.
174 | func (h *aesCCM) NonceSize() int {
175 | return h.ivSize
176 | }
177 |
178 | // Key implements the key.Encryptor interface.
179 | // Key returns the key in Encryptor.
180 | func (h *aesCCM) Key() key.Key {
181 | return h.key
182 | }
183 |
184 | func getKeySize(alg key.Alg) (keySize, tagSize, nonceSize int) {
185 | switch alg {
186 | case iana.AlgorithmAES_CCM_16_64_128:
187 | return 16, 8, 13
188 | case iana.AlgorithmAES_CCM_16_64_256:
189 | return 32, 8, 13
190 | case iana.AlgorithmAES_CCM_64_64_128:
191 | return 16, 8, 7
192 | case iana.AlgorithmAES_CCM_64_64_256:
193 | return 32, 8, 7
194 | case iana.AlgorithmAES_CCM_16_128_128:
195 | return 16, 16, 13
196 | case iana.AlgorithmAES_CCM_16_128_256:
197 | return 32, 16, 13
198 | case iana.AlgorithmAES_CCM_64_128_128:
199 | return 16, 16, 7
200 | case iana.AlgorithmAES_CCM_64_128_256:
201 | return 32, 16, 7
202 | default:
203 | return 0, 0, 0
204 | }
205 | }
206 |
--------------------------------------------------------------------------------
/cwt/validator_test.go:
--------------------------------------------------------------------------------
1 | // (c) 2022-present, LDC Labs, Inc. All rights reserved.
2 | // See the file LICENSE for licensing terms.
3 |
4 | package cwt
5 |
6 | import (
7 | "math"
8 | "testing"
9 | "time"
10 |
11 | "github.com/ldclabs/cose/iana"
12 | _ "github.com/ldclabs/cose/key/ecdsa"
13 |
14 | "github.com/stretchr/testify/assert"
15 | "github.com/stretchr/testify/require"
16 | )
17 |
18 | func TestValidator(t *testing.T) {
19 |
20 | t.Run("NewValidator", func(t *testing.T) {
21 | assert := assert.New(t)
22 |
23 | va, err := NewValidator(nil)
24 | assert.ErrorContains(err, "nil ValidatorOpts")
25 | assert.Nil(va)
26 |
27 | va, err = NewValidator(&ValidatorOpts{ClockSkew: time.Minute * 11})
28 | assert.ErrorContains(err, "clock skew too large")
29 | assert.Nil(va)
30 |
31 | va, err = NewValidator(&ValidatorOpts{})
32 | require.NoError(t, err)
33 | assert.ErrorContains(va.Validate(nil), "nil Claims")
34 | assert.ErrorContains(va.ValidateMap(nil), "nil ClaimsMap")
35 | })
36 |
37 | t.Run("Expiration", func(t *testing.T) {
38 | assert := assert.New(t)
39 |
40 | va, err := NewValidator(&ValidatorOpts{})
41 | require.NoError(t, err)
42 | assert.ErrorContains(va.Validate(&Claims{}), "token doesn't have an expiration set")
43 | assert.ErrorContains(va.ValidateMap(ClaimsMap{}), "token doesn't have an expiration set")
44 |
45 | va, err = NewValidator(&ValidatorOpts{
46 | AllowMissingExpiration: true,
47 | })
48 | require.NoError(t, err)
49 | assert.NoError(va.Validate(&Claims{}))
50 | assert.NoError(va.ValidateMap(ClaimsMap{}))
51 | assert.ErrorContains(va.Validate(&Claims{
52 | Expiration: 123,
53 | }), "token has expired")
54 | assert.ErrorContains(va.ValidateMap(ClaimsMap{
55 | iana.CWTClaimExp: "123",
56 | }), "token has an invalid exp claim")
57 | assert.ErrorContains(va.ValidateMap(ClaimsMap{
58 | iana.CWTClaimExp: 123,
59 | }), "token has expired")
60 |
61 | fixedNow := time.Unix(3600, 0)
62 | va, err = NewValidator(&ValidatorOpts{
63 | ClockSkew: time.Minute,
64 | FixedNow: fixedNow,
65 | })
66 | require.NoError(t, err)
67 | assert.ErrorContains(va.Validate(&Claims{
68 | Expiration: uint64(fixedNow.Unix()) - 60,
69 | }), "token has expired")
70 | assert.ErrorContains(va.ValidateMap(ClaimsMap{
71 | iana.CWTClaimExp: uint64(fixedNow.Unix()) - 60,
72 | }), "token has expired")
73 |
74 | assert.NoError(va.Validate(&Claims{
75 | Expiration: uint64(fixedNow.Unix()) - 1,
76 | }))
77 | assert.NoError(va.ValidateMap(ClaimsMap{
78 | iana.CWTClaimExp: uint64(fixedNow.Unix()) - 1,
79 | }))
80 | assert.ErrorContains(va.Validate(&Claims{
81 | Expiration: math.MaxInt64,
82 | }), "token has expired")
83 | assert.ErrorContains(va.ValidateMap(ClaimsMap{
84 | iana.CWTClaimExp: math.MaxInt64,
85 | }), "token has expired")
86 | })
87 |
88 | t.Run("NotBefore", func(t *testing.T) {
89 | assert := assert.New(t)
90 |
91 | fixedNow := time.Unix(3600, 0)
92 | va, err := NewValidator(&ValidatorOpts{
93 | AllowMissingExpiration: true,
94 | ClockSkew: time.Minute,
95 | FixedNow: fixedNow,
96 | })
97 | require.NoError(t, err)
98 | assert.ErrorContains(va.Validate(&Claims{
99 | NotBefore: uint64(fixedNow.Unix()) + 61,
100 | }), "token cannot be used yet")
101 | assert.ErrorContains(va.ValidateMap(ClaimsMap{
102 | iana.CWTClaimNbf: "123",
103 | }), "token has an invalid nbf claim")
104 | assert.ErrorContains(va.ValidateMap(ClaimsMap{
105 | iana.CWTClaimNbf: uint64(fixedNow.Unix()) + 61,
106 | }), "token cannot be used yet")
107 |
108 | assert.NoError(va.Validate(&Claims{
109 | NotBefore: uint64(fixedNow.Unix()) + 1,
110 | }))
111 | assert.NoError(va.ValidateMap(ClaimsMap{
112 | iana.CWTClaimNbf: uint64(fixedNow.Unix()) + 1,
113 | }))
114 | })
115 |
116 | t.Run("IssuedAt", func(t *testing.T) {
117 | assert := assert.New(t)
118 |
119 | fixedNow := time.Unix(3600, 0)
120 | va, err := NewValidator(&ValidatorOpts{
121 | AllowMissingExpiration: true,
122 | ExpectIssuedInThePast: true,
123 | ClockSkew: time.Minute,
124 | FixedNow: fixedNow,
125 | })
126 | require.NoError(t, err)
127 | assert.ErrorContains(va.Validate(&Claims{
128 | IssuedAt: uint64(fixedNow.Unix()) + 61,
129 | }), "token has an invalid iat claim in the future")
130 | assert.ErrorContains(va.ValidateMap(ClaimsMap{
131 | iana.CWTClaimIat: "123",
132 | }), "token has an invalid iat claim")
133 | assert.ErrorContains(va.ValidateMap(ClaimsMap{
134 | iana.CWTClaimIat: uint64(fixedNow.Unix()) + 61,
135 | }), "token has an invalid iat claim in the future")
136 |
137 | assert.NoError(va.Validate(&Claims{
138 | IssuedAt: uint64(fixedNow.Unix()) + 1,
139 | }))
140 | assert.NoError(va.ValidateMap(ClaimsMap{
141 | iana.CWTClaimIat: uint64(fixedNow.Unix()) + 1,
142 | }))
143 | })
144 |
145 | t.Run("ExpectedIssuer", func(t *testing.T) {
146 | assert := assert.New(t)
147 |
148 | va, err := NewValidator(&ValidatorOpts{
149 | AllowMissingExpiration: true,
150 | })
151 | require.NoError(t, err)
152 | assert.NoError(va.Validate(&Claims{}))
153 | assert.NoError(va.ValidateMap(ClaimsMap{}))
154 |
155 | va, err = NewValidator(&ValidatorOpts{
156 | AllowMissingExpiration: true,
157 | ExpectedIssuer: "ldclabs",
158 | })
159 | require.NoError(t, err)
160 |
161 | assert.ErrorContains(va.Validate(&Claims{}),
162 | `issuer mismatch, expected "ldclabs", got ""`)
163 | assert.ErrorContains(va.ValidateMap(ClaimsMap{
164 | iana.CWTClaimIss: 123,
165 | }), "token has an invalid iss claim")
166 | assert.ErrorContains(va.ValidateMap(ClaimsMap{}),
167 | `issuer mismatch, expected "ldclabs", got ""`)
168 |
169 | assert.ErrorContains(va.Validate(&Claims{Issuer: "alice"}),
170 | `issuer mismatch, expected "ldclabs", got "alice"`)
171 | assert.ErrorContains(va.ValidateMap(ClaimsMap{iana.CWTClaimIss: 123}),
172 | "token has an invalid iss claim")
173 | assert.ErrorContains(va.ValidateMap(ClaimsMap{iana.CWTClaimIss: "alice"}),
174 | `issuer mismatch, expected "ldclabs", got "alice"`)
175 |
176 | assert.NoError(va.Validate(&Claims{Issuer: "ldclabs"}))
177 | assert.NoError(va.ValidateMap(ClaimsMap{iana.CWTClaimIss: "ldclabs"}))
178 | })
179 |
180 | t.Run("ExpectedAudience", func(t *testing.T) {
181 | assert := assert.New(t)
182 |
183 | va, err := NewValidator(&ValidatorOpts{
184 | AllowMissingExpiration: true,
185 | })
186 | require.NoError(t, err)
187 | assert.NoError(va.Validate(&Claims{}))
188 | assert.NoError(va.ValidateMap(ClaimsMap{}))
189 |
190 | va, err = NewValidator(&ValidatorOpts{
191 | AllowMissingExpiration: true,
192 | ExpectedAudience: "ldclabs",
193 | })
194 | require.NoError(t, err)
195 |
196 | assert.ErrorContains(va.Validate(&Claims{}),
197 | `audience mismatch, expected "ldclabs", got ""`)
198 | assert.ErrorContains(va.ValidateMap(ClaimsMap{
199 | iana.CWTClaimAud: 123,
200 | }), "token has an invalid aud claim")
201 | assert.ErrorContains(va.ValidateMap(ClaimsMap{}),
202 | `audience mismatch, expected "ldclabs", got ""`)
203 |
204 | assert.ErrorContains(va.Validate(&Claims{Audience: "alice"}),
205 | `audience mismatch, expected "ldclabs", got "alice"`)
206 | assert.ErrorContains(va.ValidateMap(ClaimsMap{iana.CWTClaimAud: 123}),
207 | "token has an invalid aud claim")
208 | assert.ErrorContains(va.ValidateMap(ClaimsMap{iana.CWTClaimAud: "alice"}),
209 | `audience mismatch, expected "ldclabs", got "alice"`)
210 |
211 | assert.NoError(va.Validate(&Claims{Audience: "ldclabs"}))
212 | assert.NoError(va.ValidateMap(ClaimsMap{iana.CWTClaimAud: "ldclabs"}))
213 | })
214 | }
215 |
--------------------------------------------------------------------------------