├── .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 | --------------------------------------------------------------------------------