├── .github ├── actions │ ├── install-openssl │ │ └── action.yml │ └── install-ruby │ │ └── action.yml └── workflows │ └── build.yml ├── .gitignore ├── .gitmodules ├── .rspec ├── .rubocop.yml ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── SECURITY.md ├── bin ├── console └── setup ├── cose.gemspec ├── gemfiles ├── openssl_2_1.gemfile ├── openssl_2_2.gemfile ├── openssl_3_0.gemfile ├── openssl_3_1.gemfile ├── openssl_3_2.gemfile ├── openssl_3_3.gemfile └── openssl_default.gemfile ├── lib ├── cose.rb └── cose │ ├── algorithm.rb │ ├── algorithm │ ├── base.rb │ ├── ecdsa.rb │ ├── eddsa.rb │ ├── hmac.rb │ ├── rsa_pss.rb │ └── signature_algorithm.rb │ ├── encrypt.rb │ ├── encrypt0.rb │ ├── error.rb │ ├── key.rb │ ├── key │ ├── base.rb │ ├── curve.rb │ ├── curve_key.rb │ ├── ec2.rb │ ├── okp.rb │ ├── rsa.rb │ └── symmetric.rb │ ├── mac.rb │ ├── mac0.rb │ ├── recipient.rb │ ├── security_message.rb │ ├── security_message │ └── headers.rb │ ├── sign.rb │ ├── sign1.rb │ ├── signature.rb │ └── version.rb └── spec ├── cose ├── encrypt0_spec.rb ├── encrypt_spec.rb ├── key │ ├── ec2_spec.rb │ ├── okp_spec.rb │ ├── rsa_spec.rb │ └── symmetric_spec.rb ├── key_spec.rb ├── mac0_spec.rb ├── mac_spec.rb ├── recipient_spec.rb ├── sign1_spec.rb ├── sign_spec.rb └── signature_spec.rb ├── cose_spec.rb └── spec_helper.rb /.github/actions/install-openssl/action.yml: -------------------------------------------------------------------------------- 1 | name: Install OpenSSL 2 | inputs: 3 | version: 4 | description: 'The version of OpenSSL to install' 5 | required: true 6 | os: 7 | description: 'The operating system to install OpenSSL on' 8 | required: true 9 | runs: 10 | using: 'composite' 11 | steps: 12 | - name: Cache OpenSSL library 13 | id: cache-openssl 14 | uses: actions/cache@v4 15 | with: 16 | path: ~/openssl 17 | key: openssl-${{ inputs.version }}-${{ inputs.os }} 18 | 19 | - name: Compile OpenSSL library 20 | if: steps.cache-openssl.outputs.cache-hit != 'true' 21 | shell: bash 22 | run: | 23 | mkdir -p tmp/build-openssl && cd tmp/build-openssl 24 | case ${{ inputs.version }} in 25 | 1.1.*) 26 | OPENSSL_COMMIT=OpenSSL_ 27 | OPENSSL_COMMIT+=$(echo ${{ inputs.version }} | sed -e 's/\./_/g') 28 | git clone -b $OPENSSL_COMMIT --depth 1 https://github.com/openssl/openssl.git . 29 | echo "Git commit: $(git rev-parse HEAD)" 30 | ./Configure --prefix=$HOME/openssl --libdir=lib linux-x86_64 31 | make depend && make -j4 && make install_sw 32 | ;; 33 | *) 34 | echo "Don't know how to build OpenSSL ${{ inputs.version }}" 35 | ;; 36 | esac 37 | -------------------------------------------------------------------------------- /.github/actions/install-ruby/action.yml: -------------------------------------------------------------------------------- 1 | name: Install Ruby 2 | inputs: 3 | version: 4 | description: 'The version of Ruby to install' 5 | required: true 6 | os: 7 | description: 'The operating system to install Ruby on' 8 | required: true 9 | runs: 10 | using: 'composite' 11 | steps: 12 | - name: Cache Ruby 13 | id: ruby-cache 14 | uses: actions/cache@v4 15 | with: 16 | path: ~/rubies/ruby-${{ inputs.version }} 17 | key: ruby-${{ inputs.version }}-${{ inputs.os }}-openssl-1.1.1w 18 | 19 | - name: Install Ruby 20 | if: steps.ruby-cache.outputs.cache-hit != 'true' 21 | shell: bash 22 | run: | 23 | latest_patch=$(curl -s https://cache.ruby-lang.org/pub/ruby/${{ inputs.version }}/ \ 24 | | grep -oP "ruby-${{ inputs.version }}\.\d+\.tar\.xz" \ 25 | | grep -oP "\d+(?=\.tar\.xz)" \ 26 | | sort -V | tail -n 1) 27 | wget https://cache.ruby-lang.org/pub/ruby/${{ inputs.version }}/ruby-${{ inputs.version }}.${latest_patch}.tar.xz 28 | tar -xJvf ruby-${{ inputs.version }}.${latest_patch}.tar.xz 29 | cd ruby-${{ inputs.version }}.${latest_patch} 30 | ./configure --prefix=$HOME/rubies/ruby-${{ inputs.version }} --with-openssl-dir=$HOME/openssl 31 | make 32 | make install 33 | - name: Update PATH 34 | shell: bash 35 | run: | 36 | echo "~/rubies/ruby-${{ inputs.version }}/bin" >> $GITHUB_PATH 37 | - name: Install Bundler 38 | shell: bash 39 | run: | 40 | case ${{ inputs.version }} in 41 | 2.7* | 3.*) 42 | echo "Skipping Bundler installation for Ruby ${{ inputs.version }}" 43 | ;; 44 | 2.4* | 2.5* | 2.6*) 45 | gem install bundler -v '~> 2.3.0' 46 | ;; 47 | *) 48 | echo "Don't know how to install Bundler for Ruby ${{ inputs.version }}" 49 | ;; 50 | esac 51 | - name: Cache Bundler Install 52 | id: bundler-cache 53 | uses: actions/cache@v4 54 | env: 55 | GEMFILE: ${{ env.BUNDLE_GEMFILE || 'Gemfile' }} 56 | with: 57 | path: ./vendor/bundle 58 | key: bundler-ruby-${{ inputs.version }}-${{ inputs.os }}-${{ hashFiles(env.Gemfile, 'tpm-key_attestation.gemspec') }} 59 | 60 | - name: Install dependencies 61 | shell: bash 62 | run: | 63 | bundle config set --local path ../vendor/bundle 64 | bundle install 65 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake 6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby 7 | 8 | name: build 9 | 10 | on: push 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Check out repository code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Ruby 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: '3.4' 24 | bundler-cache: true 25 | 26 | - name: Lint code for consistent style 27 | run: bundle exec rubocop -f github 28 | test: 29 | runs-on: ubuntu-24.04 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | ruby: 34 | - 3.4 35 | - 3.3 36 | - 3.2 37 | - 3.1 38 | - '3.0' 39 | - 2.7 40 | - 2.6 41 | - 2.5 42 | - 2.4 43 | gemfile: 44 | - openssl_3_3 45 | - openssl_3_2 46 | - openssl_3_1 47 | - openssl_3_0 48 | - openssl_2_2 49 | - openssl_2_1 50 | - openssl_default 51 | exclude: 52 | - ruby: '2.4' 53 | gemfile: openssl_3_0 54 | - ruby: '2.4' 55 | gemfile: openssl_3_1 56 | - ruby: '2.4' 57 | gemfile: openssl_3_2 58 | - ruby: '2.4' 59 | gemfile: openssl_3_3 60 | - ruby: '2.5' 61 | gemfile: openssl_3_0 62 | - ruby: '2.5' 63 | gemfile: openssl_3_1 64 | - ruby: '2.5' 65 | gemfile: openssl_3_2 66 | - ruby: '2.5' 67 | gemfile: openssl_3_3 68 | - ruby: '2.6' 69 | gemfile: openssl_3_2 70 | - ruby: '2.6' 71 | gemfile: openssl_3_3 72 | 73 | env: 74 | BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile 75 | steps: 76 | - uses: actions/checkout@v2 77 | 78 | - run: rm Gemfile.lock 79 | 80 | - name: Install OpenSSL 81 | if: matrix.ruby != '2.4' 82 | uses: ./.github/actions/install-openssl 83 | with: 84 | version: "1.1.1w" 85 | os: ${{ runner.os }} 86 | 87 | - name: Manually set up Ruby 88 | if: matrix.ruby != '2.4' 89 | uses: ./.github/actions/install-ruby 90 | with: 91 | version: ${{ matrix.ruby }} 92 | os: ${{ runner.os }} 93 | 94 | - name: Set up Ruby 95 | if: matrix.ruby == '2.4' 96 | uses: ruby/setup-ruby@v1 97 | with: 98 | ruby-version: ${{ matrix.ruby }} 99 | bundler-cache: true 100 | 101 | - run: bundle exec rspec 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | 10 | # rspec failure tracking 11 | .rspec_status 12 | 13 | /gemfiles/*.gemfile.lock 14 | 15 | .byebug_history 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/fixtures/cose-wg-examples"] 2 | path = spec/fixtures/cose-wg-examples 3 | url = https://github.com/cose-wg/Examples 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --order random 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-performance 3 | 4 | inherit_mode: 5 | merge: 6 | - Exclude 7 | 8 | AllCops: 9 | TargetRubyVersion: 2.4 10 | DisabledByDefault: true 11 | Exclude: 12 | - "gemfiles/**/*" 13 | 14 | Bundler: 15 | Enabled: true 16 | 17 | Gemspec: 18 | Enabled: true 19 | 20 | Layout: 21 | Enabled: true 22 | 23 | Layout/LineLength: 24 | Max: 120 25 | 26 | Lint: 27 | Enabled: true 28 | 29 | Naming: 30 | Enabled: true 31 | 32 | Performance: 33 | Enabled: true 34 | 35 | Security: 36 | Enabled: true 37 | 38 | Style/FrozenStringLiteralComment: 39 | Enabled: true 40 | 41 | Style/HashSyntax: 42 | Enabled: true 43 | 44 | Style/RedundantFreeze: 45 | Enabled: true 46 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | appraise "openssl_2_2" do 4 | gem "openssl", "~> 2.2.0" 5 | end 6 | 7 | appraise "openssl_2_1" do 8 | gem "openssl", "~> 2.1.0" 9 | end 10 | 11 | appraise "openssl_3_0" do 12 | gem "openssl", "~> 3.0.0" 13 | end 14 | 15 | appraise "openssl_3_1" do 16 | gem "openssl", "~> 3.1.0" 17 | end 18 | 19 | appraise "openssl_3_2" do 20 | gem "openssl", "~> 3.2.0" 21 | end 22 | 23 | appraise "openssl_3_3" do 24 | gem "openssl", "~> 3.3.0" 25 | end 26 | 27 | appraise "openssl_default" do 28 | end 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v1.3.1](https://github.com/cedarcode/cose-ruby/compare/v1.3.0...v1.3.1/) - 2024-08-12 4 | 5 | - Handling COSE EC keys encoded without leading 0 bytes in coordinates (#64). Credits to @waltercacau. 6 | 7 | ## [v1.3.0] - 2022-10-28 8 | 9 | - Add support for EdDSA (#55). Credits to @bdewater. 10 | 11 | ## [v1.2.1] - 2022-07-03 12 | 13 | - Support OpenSSL ~>3.0.0. Credits to @ClearlyClaire <3 14 | 15 | ## [v1.2.0] - 2020-07-10 16 | 17 | ### Added 18 | 19 | - Support ES256K signature algorithm 20 | 21 | ## [v1.1.0] - 2020-07-09 22 | 23 | ### Dependencies 24 | 25 | - Update `openssl-signature_algorithm` runtime dependency from `~> 0.4.0` to `~> 1.0`. 26 | 27 | ## [v1.0.0] - 2020-03-29 28 | 29 | ### Added 30 | 31 | - Signature verification validates key `alg` is compatible with the signature algorithm 32 | 33 | NOTE: No breaking changes. Moving out of `v0.x` to express the intention to keep the public API stable. 34 | 35 | ## [v0.11.0] - 2020-01-30 36 | 37 | ### Added 38 | 39 | - Let others easily support more signature algorithms by making `COSE::Algorithm::SignatureAlgorithm` smarter 40 | 41 | ## [v0.10.0] - 2019-12-19 42 | 43 | ### Added 44 | 45 | - Works on ruby 2.7 without throwing any warnings 46 | - Simpler way to rescue key deserialization error, now possible to: 47 | ```rb 48 | begin 49 | COSE::Key.deserialize(cbor) 50 | rescue COSE::KeyDeserializationError 51 | # handle error 52 | end 53 | ``` 54 | 55 | ## [v0.9.0] - 2019-08-31 56 | 57 | ### Added 58 | 59 | - `COSE::Sign1#verify` 60 | - `COSE::Sign#verify` 61 | - `COSE::Mac0#verify` 62 | - `COSE::Mac#verify` 63 | 64 | ## [v0.8.0] - 2019-08-17 65 | 66 | ### Added 67 | 68 | - Support `COSE::Key` instantiation based on an `OpenSSL::PKey` object with `COSE::Key.from_pkey` 69 | - Provide writer methods for `COSE::Key` Common Parameters (`#base_iv=`, `#key_ops=`, `#alg=` and `#kid=`) 70 | 71 | ## [v0.7.0] - 2019-05-02 72 | 73 | ### Fixed 74 | 75 | - `require "cose"` now correctly requires all features 76 | 77 | ## [v0.6.1] - 2019-04-06 78 | 79 | ### Fixed 80 | 81 | - Fix COSE::Key::RSA#to_pkey for a public key 82 | 83 | ## [v0.6.0] - 2019-04-03 84 | 85 | ### Added 86 | 87 | - Support Key Common Parameters (`#base_iv`, `key_ops`, `#alg` and `#kid`) 88 | - Support OKP Key 89 | - Support RSA private key serialization 90 | - Works with ruby 2.3 91 | 92 | ### Changed 93 | 94 | - Key type-specific parameters names better match RFC 95 | 96 | ## [v0.5.0] - 2019-03-25 97 | 98 | ### Added 99 | 100 | - `COSE::Key.serialize(openssl_pkey)` serializes an `OpenSSL::PKey::PKey` object into CBOR data. Supports RSA keys plus 101 | EC keys from curves prime256v1, secp384r1 and secp521r1. 102 | - `COSE::Key::EC2#to_pkey` converts to an `OpenSSL::PKey::EC` object 103 | - `COSE::Key::RSA#to_pkey` converts to an `OpenSSL::PKey::RSA` object 104 | 105 | ## [v0.4.1] - 2019-03-12 106 | 107 | ### Fixed 108 | 109 | - Fix `uninitialized constant COSE::Key::Base::LABEL_KTY` when requiring only particular key 110 | 111 | ## [v0.4.0] - 2019-03-12 112 | 113 | ### Added 114 | 115 | - RSA public key deserialization 116 | - Key type-agnostic deserialization 117 | 118 | ### Changed 119 | 120 | - Keys `.from_cbor` methods changed to `.deserialize` 121 | 122 | ## [v0.3.0] - 2019-03-09 123 | 124 | ### Added 125 | 126 | - Support deserialization of security messages: 127 | - COSE_Sign 128 | - COSE_Sign1 129 | - COSE_Mac 130 | - COSE_Mac0 131 | - COSE_Encrypt 132 | - COSE_Encrypt0 133 | - Works with ruby 2.6 134 | 135 | ## [v0.2.0] - 2019-03-04 136 | 137 | ### Added 138 | 139 | - Symmetric key object 140 | - EC2 key suppors D coordinate 141 | - Works with ruby 2.4 142 | 143 | ## [v0.1.0] - 2018-06-08 144 | 145 | ### Added 146 | 147 | - EC2 key object 148 | - Works with ruby 2.5 149 | 150 | [v1.3.0]: https://github.com/cedarcode/cose-ruby/compare/v1.2.1...v1.3.0/ 151 | [v1.2.1]: https://github.com/cedarcode/cose-ruby/compare/v1.2.0...v1.2.1/ 152 | [v1.2.0]: https://github.com/cedarcode/cose-ruby/compare/v1.1.0...v1.2.0/ 153 | [v1.1.0]: https://github.com/cedarcode/cose-ruby/compare/v1.0.0...v1.1.0/ 154 | [v1.0.0]: https://github.com/cedarcode/cose-ruby/compare/v0.11.0...v1.0.0/ 155 | [v0.11.0]: https://github.com/cedarcode/cose-ruby/compare/v0.10.0...v0.11.0/ 156 | [v0.10.0]: https://github.com/cedarcode/cose-ruby/compare/v0.9.0...v0.10.0/ 157 | [v0.9.0]: https://github.com/cedarcode/cose-ruby/compare/v0.8.0...v0.9.0/ 158 | [v0.8.0]: https://github.com/cedarcode/cose-ruby/compare/v0.7.0...v0.8.0/ 159 | [v0.7.0]: https://github.com/cedarcode/cose-ruby/compare/v0.6.1...v0.7.0/ 160 | [v0.6.1]: https://github.com/cedarcode/cose-ruby/compare/v0.6.0...v0.6.1/ 161 | [v0.6.0]: https://github.com/cedarcode/cose-ruby/compare/v0.5.0...v0.6.0/ 162 | [v0.5.0]: https://github.com/cedarcode/cose-ruby/compare/v0.4.1...v0.5.0/ 163 | [v0.4.1]: https://github.com/cedarcode/cose-ruby/compare/v0.4.0...v0.4.1/ 164 | [v0.4.0]: https://github.com/cedarcode/cose-ruby/compare/v0.3.0...v0.4.0/ 165 | [v0.3.0]: https://github.com/cedarcode/cose-ruby/compare/v0.2.0...v0.3.0/ 166 | [v0.2.0]: https://github.com/cedarcode/cose-ruby/compare/v0.1.0...v0.2.0/ 167 | [v0.1.0]: https://github.com/cedarcode/cose-ruby/compare/5725d9b5db978f19a21bd59182f092d31a118eff...v0.1.0/ 168 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } 6 | 7 | # Specify your gem's dependencies in cose.gemspec 8 | gemspec 9 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | cose (1.3.1) 5 | cbor (~> 0.5.9) 6 | openssl-signature_algorithm (~> 1.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | appraisal (2.2.0) 12 | bundler 13 | rake 14 | thor (>= 0.14.0) 15 | ast (2.4.2) 16 | base64 (0.2.0) 17 | byebug (11.1.3) 18 | cbor (0.5.9.8) 19 | diff-lcs (1.6.0) 20 | json (2.10.1) 21 | language_server-protocol (3.17.0.4) 22 | lint_roller (1.1.0) 23 | openssl (3.3.0) 24 | openssl-signature_algorithm (1.3.0) 25 | openssl (> 2.0) 26 | parallel (1.26.3) 27 | parser (3.3.7.1) 28 | ast (~> 2.4.1) 29 | racc 30 | racc (1.8.1) 31 | rainbow (3.1.1) 32 | rake (13.2.1) 33 | regexp_parser (2.10.0) 34 | rspec (3.13.0) 35 | rspec-core (~> 3.13.0) 36 | rspec-expectations (~> 3.13.0) 37 | rspec-mocks (~> 3.13.0) 38 | rspec-core (3.13.3) 39 | rspec-support (~> 3.13.0) 40 | rspec-expectations (3.13.3) 41 | diff-lcs (>= 1.2.0, < 2.0) 42 | rspec-support (~> 3.13.0) 43 | rspec-mocks (3.13.2) 44 | diff-lcs (>= 1.2.0, < 2.0) 45 | rspec-support (~> 3.13.0) 46 | rspec-support (3.13.2) 47 | rubocop (1.72.2) 48 | json (~> 2.3) 49 | language_server-protocol (~> 3.17.0.2) 50 | lint_roller (~> 1.1.0) 51 | parallel (~> 1.10) 52 | parser (>= 3.3.0.2) 53 | rainbow (>= 2.2.2, < 4.0) 54 | regexp_parser (>= 2.9.3, < 3.0) 55 | rubocop-ast (>= 1.38.0, < 2.0) 56 | ruby-progressbar (~> 1.7) 57 | unicode-display_width (>= 2.4.0, < 4.0) 58 | rubocop-ast (1.38.0) 59 | parser (>= 3.3.1.0) 60 | rubocop-performance (1.24.0) 61 | lint_roller (~> 1.1) 62 | rubocop (>= 1.72.1, < 2.0) 63 | rubocop-ast (>= 1.38.0, < 2.0) 64 | ruby-progressbar (1.13.0) 65 | thor (1.3.2) 66 | unicode-display_width (3.1.4) 67 | unicode-emoji (~> 4.0, >= 4.0.4) 68 | unicode-emoji (4.0.4) 69 | 70 | PLATFORMS 71 | arm64-darwin-24 72 | ruby 73 | 74 | DEPENDENCIES 75 | appraisal (~> 2.2.0) 76 | base64 (~> 0.2) 77 | bundler (>= 1.17, < 3) 78 | byebug (~> 11.0) 79 | cose! 80 | rake (~> 13.0) 81 | rspec (~> 3.8) 82 | rubocop (~> 1) 83 | rubocop-performance (~> 1.4) 84 | 85 | BUNDLED WITH 86 | 2.6.3 87 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Gonzalo 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cose-ruby 2 | 3 | Ruby implementation of RFC [8152](https://tools.ietf.org/html/rfc8152) CBOR Object Signing and Encryption (COSE) 4 | 5 | [![Gem](https://img.shields.io/gem/v/cose.svg?style=flat-square&color=informational)](https://rubygems.org/gems/cose) 6 | [![Actions Build](https://github.com/cedarcode/cose-ruby/workflows/build/badge.svg)](https://github.com/cedarcode/cose-ruby/actions) 7 | 8 | ## Installation 9 | 10 | Add this line to your application's Gemfile: 11 | 12 | ```ruby 13 | gem 'cose' 14 | ``` 15 | 16 | And then execute: 17 | 18 | $ bundle 19 | 20 | Or install it yourself as: 21 | 22 | $ gem install cose 23 | 24 | ## Usage 25 | 26 | ### Key Objects 27 | 28 | #### Deserialization (from CBOR to Ruby objects) 29 | 30 | ```ruby 31 | cbor_data = "..." 32 | 33 | key = COSE::Key.deserialize(cbor_data) 34 | ``` 35 | 36 | Once you have a `COSE::Key` instance you can either access key parameters directly and/or convert it to an 37 | `OpenSSL::PKey::PKey` instance (if supported for the key type) for operating with it 38 | (encrypting/decrypting, signing/verifying, etc). 39 | 40 | ```ruby 41 | # Convert to an OpenSSL::PKey::PKey 42 | if key.respond_to?(:to_pkey) 43 | openssl_pkey = key.to_pkey 44 | end 45 | 46 | # Access COSE key parameters 47 | case key 48 | when COSE::Key::OKP 49 | key.crv 50 | key.x 51 | key.d 52 | when COSE::Key::EC2 53 | key.crv 54 | key.x 55 | key.y 56 | key.d 57 | when COSE::Key::RSA 58 | key.n 59 | key.e 60 | key.d 61 | key.p 62 | key.q 63 | key.dp 64 | key.dq 65 | key.qinv 66 | when COSE::Key::Symmetric 67 | key.k 68 | end 69 | ``` 70 | 71 | If you already know which COSE key type is encoded in the CBOR data, then: 72 | 73 | ```ruby 74 | okp_key_cbor = "..." 75 | 76 | cose_okp_key = COSE::Key::OKP.deserialize(okp_key_cbor) 77 | 78 | cose_okp_key.crv 79 | cose_okp_key.x 80 | cose_okp_key.d 81 | ``` 82 | 83 | ```ruby 84 | ec2_key_cbor = "..." 85 | 86 | cose_ec2_key = COSE::Key::EC2.deserialize(ec2_key_cbor) 87 | 88 | cose_ec2_key.crv 89 | cose_ec2_key.x 90 | cose_ec2_key.y 91 | cose_ec2_key.d 92 | 93 | # or 94 | 95 | ec_pkey = cose_ec2_key.to_pkey # Instance of an OpenSSL::PKey::EC 96 | ``` 97 | 98 | ```ruby 99 | symmetric_key_cbor = "..." 100 | 101 | cose_symmetric_key = COSE::Key::Symmetric.deserialize(symmetric_key_cbor) 102 | 103 | cose_symmetric_key.k 104 | ``` 105 | 106 | ```ruby 107 | rsa_key_cbor = "..." 108 | 109 | cose_rsa_key = COSE::Key::RSA.deserialize(rsa_key_cbor) 110 | 111 | cose_rsa_key.n 112 | cose_rsa_key.e 113 | cose_rsa_key.d 114 | cose_rsa_key.p 115 | cose_rsa_key.q 116 | cose_rsa_key.dp 117 | cose_rsa_key.dq 118 | cose_rsa_key.qinv 119 | 120 | # or 121 | 122 | rsa_pkey = cose_rsa_key.to_pkey # Instance of an OpenSSL::PKey::RSA 123 | ``` 124 | 125 | #### Serialization (from Ruby objects to CBOR) 126 | 127 | ```ruby 128 | ec_pkey = OpenSSL::PKey::EC.new("prime256v1").generate_key 129 | 130 | cose_ec2_key_cbor = COSE::Key.serialize(ec_pkey) 131 | ``` 132 | 133 | ```ruby 134 | rsa_pkey = OpenSSL::PKey::RSA.new(2048) 135 | 136 | cose_rsa_key_cbor = COSE::Key.serialize(rsa_pkey) 137 | ``` 138 | 139 | ### Signing Objects 140 | 141 | #### COSE_Sign 142 | 143 | ```ruby 144 | cbor_data = "..." 145 | 146 | sign = COSE::Sign.deserialize(cbor_data) 147 | 148 | # Verify by doing (key should be a COSE::Key): 149 | sign.verify(key) 150 | 151 | # or, if externally supplied authenticated data exists: 152 | sign.verify(key, external_aad) 153 | 154 | # Then access payload 155 | sign.payload 156 | ``` 157 | 158 | #### COSE_Sign1 159 | 160 | ```ruby 161 | cbor_data = "..." 162 | 163 | sign1 = COSE::Sign1.deserialize(cbor_data) 164 | 165 | # Verify by doing (key should be a COSE::Key): 166 | sign1.verify(key) 167 | 168 | # or, if externally supplied authenticated data exists: 169 | sign1.verify(key, external_aad) 170 | 171 | # Then access payload 172 | sign1.payload 173 | ``` 174 | 175 | ### MAC Objects 176 | 177 | #### COSE_Mac 178 | 179 | ```ruby 180 | cbor_data = "..." 181 | 182 | mac = COSE::Mac.deserialize(cbor_data) 183 | 184 | # Verify by doing (key should be a COSE::Key::Symmetric): 185 | mac.verify(key) 186 | 187 | # or, if externally supplied authenticated data exists: 188 | mac.verify(key, external_aad) 189 | 190 | # Then access payload 191 | mac.payload 192 | ``` 193 | 194 | #### COSE_Mac0 195 | 196 | ```ruby 197 | cbor_data = "..." 198 | 199 | mac0 = COSE::Mac0.deserialize(cbor_data) 200 | 201 | # Verify by doing (key should be a COSE::Key::Symmetric): 202 | mac0.verify(key) 203 | 204 | # or, if externally supplied authenticated data exists: 205 | mac0.verify(key, external_aad) 206 | 207 | # Then access payload 208 | mac0.payload 209 | ``` 210 | 211 | ### Encryption Objects 212 | 213 | #### COSE_Encrypt 214 | 215 | ```ruby 216 | cbor_data = "..." 217 | 218 | encrypt = COSE::Encrypt.deserialize(cbor_data) 219 | 220 | encrypt.protected_headers 221 | encrypt.unprotected_headers 222 | encrypt.ciphertext 223 | 224 | encrypt.recipients.each do |recipient| 225 | recipient.protected_headers 226 | recipient.unprotected_headers 227 | recipient.ciphertext 228 | 229 | if recipient.recipients 230 | recipient.recipients.each do |recipient| 231 | recipient.protected_headers 232 | recipient.unprotected_headers 233 | recipient.ciphertext 234 | end 235 | end 236 | end 237 | ``` 238 | 239 | #### COSE_Encrypt0 240 | 241 | ```ruby 242 | cbor_data = "..." 243 | 244 | encrypt0 = COSE::Encrypt0.deserialize(cbor_data) 245 | 246 | encrypt0.protected_headers 247 | encrypt0.unprotected_headers 248 | encrypt0.ciphertext 249 | ``` 250 | 251 | ## Development 252 | 253 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 254 | 255 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 256 | 257 | ## Contributing 258 | 259 | Bug reports and pull requests are welcome on GitHub at https://github.com/cedarcode/cose-ruby. 260 | 261 | ## License 262 | 263 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 264 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | require "rubocop/rake_task" 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | RuboCop::RakeTask.new 9 | 10 | task default: [:rubocop, :spec] 11 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 0.9.z | :white_check_mark: | 8 | | 0.8.z | :white_check_mark: | 9 | | < 0.8 | :x: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | If you have discovered a security bug, please send an email to security@cedarcode.com 14 | instead of posting to the GitHub issue tracker. 15 | 16 | Thank you! 17 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "bundler/setup" 5 | require "cose" 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require "irb" 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | git submodule update --init --recursive 7 | 8 | bundle install 9 | 10 | # Do any other automated setup that you need to do here 11 | -------------------------------------------------------------------------------- /cose.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("../lib", __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "cose/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "cose" 9 | spec.version = COSE::VERSION 10 | spec.authors = ["Gonzalo Rodriguez", "Braulio Martinez"] 11 | spec.email = ["gonzalo@cedarcode.com", "braulio@cedarcode.com"] 12 | 13 | spec.summary = "Ruby implementation of RFC 8152 CBOR Object Signing and Encryption (COSE)" 14 | spec.homepage = "https://github.com/cedarcode/cose-ruby" 15 | spec.license = "MIT" 16 | 17 | spec.metadata = { 18 | "bug_tracker_uri" => "https://github.com/cedarcode/cose-ruby/issues", 19 | "changelog_uri" => "https://github.com/cedarcode/cose-ruby/blob/master/CHANGELOG.md", 20 | "source_code_uri" => "https://github.com/cedarcode/cose-ruby" 21 | } 22 | 23 | # Specify which files should be added to the gem when it is released. 24 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 25 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 26 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 27 | end 28 | spec.bindir = "exe" 29 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 30 | spec.require_paths = ["lib"] 31 | 32 | spec.required_ruby_version = ">= 2.4" 33 | 34 | spec.add_dependency "cbor", "~> 0.5.9" 35 | spec.add_dependency "openssl-signature_algorithm", "~> 1.0" 36 | 37 | spec.add_development_dependency "appraisal", "~> 2.2.0" 38 | spec.add_development_dependency "base64", "~> 0.2" 39 | spec.add_development_dependency "bundler", ">= 1.17", "< 3" 40 | spec.add_development_dependency "byebug", "~> 11.0" 41 | spec.add_development_dependency "rake", "~> 13.0" 42 | spec.add_development_dependency "rspec", "~> 3.8" 43 | spec.add_development_dependency "rubocop", "~> 1" 44 | spec.add_development_dependency "rubocop-performance", "~> 1.4" 45 | end 46 | -------------------------------------------------------------------------------- /gemfiles/openssl_2_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "openssl", "~> 2.1.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/openssl_2_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "openssl", "~> 2.2.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/openssl_3_0.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "openssl", "~> 3.0.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/openssl_3_1.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "openssl", "~> 3.1.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/openssl_3_2.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "openssl", "~> 3.2.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/openssl_3_3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "openssl", "~> 3.3.0" 6 | 7 | gemspec path: "../" 8 | -------------------------------------------------------------------------------- /gemfiles/openssl_default.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gemspec path: "../" 6 | -------------------------------------------------------------------------------- /lib/cose.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/encrypt" 4 | require "cose/encrypt0" 5 | require "cose/error" 6 | require "cose/key" 7 | require "cose/mac" 8 | require "cose/mac0" 9 | require "cose/sign" 10 | require "cose/sign1" 11 | require "cose/version" 12 | -------------------------------------------------------------------------------- /lib/cose/algorithm.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/algorithm/ecdsa" 4 | require "cose/algorithm/eddsa" 5 | require "cose/algorithm/hmac" 6 | require "cose/algorithm/rsa_pss" 7 | 8 | module COSE 9 | module Algorithm 10 | @registered_by_id = {} 11 | @registered_by_name = {} 12 | 13 | def self.register(algorithm) 14 | @registered_by_id[algorithm.id] = algorithm 15 | @registered_by_name[algorithm.name] = algorithm 16 | end 17 | 18 | def self.find(id_or_name) 19 | by_id(id_or_name) || by_name(id_or_name) 20 | end 21 | 22 | def self.by_id(id) 23 | @registered_by_id[id] 24 | end 25 | 26 | def self.by_name(name) 27 | @registered_by_name[name] 28 | end 29 | 30 | register(ECDSA.new(-7, "ES256", hash_function: "SHA256", curve_name: "P-256")) 31 | register(ECDSA.new(-35, "ES384", hash_function: "SHA384", curve_name: "P-384")) 32 | register(ECDSA.new(-36, "ES512", hash_function: "SHA512", curve_name: "P-521")) 33 | register(ECDSA.new(-47, "ES256K", hash_function: "SHA256", curve_name: "secp256k1")) 34 | register(EdDSA.new(-8, "EdDSA")) 35 | register(RSAPSS.new(-37, "PS256", hash_function: "SHA256", salt_length: 32)) 36 | register(RSAPSS.new(-38, "PS384", hash_function: "SHA384", salt_length: 48)) 37 | register(RSAPSS.new(-39, "PS512", hash_function: "SHA512", salt_length: 64)) 38 | register(HMAC.new(4, "HMAC 256/64", hash_function: "SHA256", tag_length: 64)) 39 | register(HMAC.new(5, "HMAC 256/256", hash_function: "SHA256", tag_length: 256)) 40 | register(HMAC.new(6, "HMAC 384/384", hash_function: "SHA384", tag_length: 384)) 41 | register(HMAC.new(7, "HMAC 512/512", hash_function: "SHA512", tag_length: 512)) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/cose/algorithm/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module COSE 4 | module Algorithm 5 | class Base 6 | attr_reader :id, :name 7 | 8 | def initialize(id, name) 9 | @id = id 10 | @name = name 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/cose/algorithm/ecdsa.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/algorithm/signature_algorithm" 4 | require "cose/error" 5 | require "cose/key/curve" 6 | require "cose/key/ec2" 7 | require "openssl" 8 | require "openssl/signature_algorithm/ecdsa" 9 | 10 | module COSE 11 | module Algorithm 12 | class ECDSA < SignatureAlgorithm 13 | attr_reader :hash_function, :curve 14 | 15 | def initialize(*args, hash_function:, curve_name:) 16 | super(*args) 17 | 18 | @hash_function = hash_function 19 | @curve = COSE::Key::Curve.by_name(curve_name) || raise("Couldn't find curve with name='#{curve_name}'") 20 | end 21 | 22 | private 23 | 24 | def valid_key?(key) 25 | cose_key = to_cose_key(key) 26 | 27 | cose_key.is_a?(COSE::Key::EC2) && (!cose_key.alg || cose_key.alg == id) 28 | end 29 | 30 | def signature_algorithm_class 31 | OpenSSL::SignatureAlgorithm::ECDSA 32 | end 33 | 34 | def signature_algorithm_parameters 35 | if curve 36 | super.merge(curve: curve.pkey_name) 37 | else 38 | super 39 | end 40 | end 41 | 42 | def to_pkey(key) 43 | case key 44 | when COSE::Key::EC2 45 | key.to_pkey 46 | when OpenSSL::PKey::EC 47 | key 48 | else 49 | raise(COSE::Error, "Incompatible key for algorithm") 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/cose/algorithm/eddsa.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/algorithm/signature_algorithm" 4 | require "cose/error" 5 | require "cose/key/okp" 6 | require "openssl" 7 | 8 | module COSE 9 | module Algorithm 10 | class EdDSA < SignatureAlgorithm 11 | private 12 | 13 | def valid_key?(key) 14 | cose_key = to_cose_key(key) 15 | 16 | cose_key.is_a?(COSE::Key::OKP) && (!cose_key.alg || cose_key.alg == id) 17 | end 18 | 19 | def to_pkey(key) 20 | case key 21 | when COSE::Key::OKP 22 | key.to_pkey 23 | when OpenSSL::PKey::PKey 24 | key 25 | else 26 | raise(COSE::Error, "Incompatible key for algorithm") 27 | end 28 | end 29 | 30 | def valid_signature?(key, signature, verification_data) 31 | pkey = to_pkey(key) 32 | 33 | begin 34 | pkey.verify(nil, signature, verification_data) 35 | rescue OpenSSL::PKey::PKeyError 36 | false 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/cose/algorithm/hmac.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/algorithm/base" 4 | require "openssl" 5 | 6 | module COSE 7 | module Algorithm 8 | class HMAC < Base 9 | BYTE_LENGTH = 8 10 | 11 | attr_reader :hash_function, :tag_length 12 | 13 | def initialize(*args, hash_function:, tag_length:) 14 | super(*args) 15 | 16 | @hash_function = hash_function 17 | @tag_length = tag_length 18 | end 19 | 20 | def mac(key, to_be_signed) 21 | mac = OpenSSL::HMAC.digest(hash_function, key, to_be_signed) 22 | 23 | if tag_bytesize && tag_bytesize < mac.bytesize 24 | mac.byteslice(0, tag_bytesize) 25 | else 26 | mac 27 | end 28 | end 29 | 30 | private 31 | 32 | def tag_bytesize 33 | @tag_bytesize ||= 34 | if tag_length 35 | tag_length / BYTE_LENGTH 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/cose/algorithm/rsa_pss.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/algorithm/signature_algorithm" 4 | require "cose/key/rsa" 5 | require "cose/error" 6 | require "openssl" 7 | require "openssl/signature_algorithm/rsapss" 8 | 9 | module COSE 10 | module Algorithm 11 | class RSAPSS < SignatureAlgorithm 12 | attr_reader :hash_function, :salt_length 13 | 14 | def initialize(*args, hash_function:, salt_length:) 15 | super(*args) 16 | 17 | @hash_function = hash_function 18 | @salt_length = salt_length 19 | end 20 | 21 | private 22 | 23 | def valid_key?(key) 24 | to_cose_key(key).is_a?(COSE::Key::RSA) 25 | end 26 | 27 | def signature_algorithm_class 28 | OpenSSL::SignatureAlgorithm::RSAPSS 29 | end 30 | 31 | def to_pkey(key) 32 | case key 33 | when COSE::Key::RSA 34 | key.to_pkey 35 | when OpenSSL::PKey::RSA 36 | key 37 | else 38 | raise(COSE::Error, "Incompatible key for algorithm") 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/cose/algorithm/signature_algorithm.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/algorithm/base" 4 | require "cose/error" 5 | 6 | module COSE 7 | module Algorithm 8 | class SignatureAlgorithm < Base 9 | def verify(key, signature, verification_data) 10 | compatible_key?(key) || raise(COSE::Error, "Incompatible key for signature verification") 11 | valid_signature?(key, signature, verification_data) || raise(COSE::Error, "Signature verification failed") 12 | end 13 | 14 | def compatible_key?(key) 15 | valid_key?(key) && to_pkey(key) 16 | rescue COSE::Error 17 | false 18 | end 19 | 20 | private 21 | 22 | def valid_signature?(key, signature, verification_data) 23 | signature_algorithm = signature_algorithm_class.new(**signature_algorithm_parameters) 24 | signature_algorithm.verify_key = to_pkey(key) 25 | 26 | begin 27 | signature_algorithm.verify(signature, verification_data) 28 | rescue OpenSSL::SignatureAlgorithm::Error 29 | false 30 | end 31 | end 32 | 33 | def signature_algorithm_parameters 34 | { hash_function: hash_function } 35 | end 36 | 37 | def to_cose_key(key) 38 | case key 39 | when COSE::Key::Base 40 | key 41 | when OpenSSL::PKey::PKey 42 | COSE::Key.from_pkey(key) 43 | else 44 | raise(COSE::Error, "Don't know how to transform #{key.class} to COSE::Key") 45 | end 46 | end 47 | 48 | def signature_algorithm_class 49 | raise NotImplementedError 50 | end 51 | 52 | def valid_key?(_key) 53 | raise NotImplementedError 54 | end 55 | 56 | def to_pkey(_key) 57 | raise NotImplementedError 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/cose/encrypt.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/security_message" 4 | require "cose/recipient" 5 | 6 | module COSE 7 | class Encrypt < SecurityMessage 8 | attr_reader :ciphertext, :recipients 9 | 10 | def self.keyword_arguments_for_initialize(decoded) 11 | { ciphertext: decoded[0], recipients: decoded[1].map { |s| COSE::Recipient.deserialize(s) } } 12 | end 13 | 14 | def initialize(ciphertext:, recipients:, **keyword_arguments) 15 | super(**keyword_arguments) 16 | 17 | @ciphertext = ciphertext 18 | @recipients = recipients 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/cose/encrypt0.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/security_message" 4 | 5 | module COSE 6 | class Encrypt0 < SecurityMessage 7 | attr_reader :ciphertext 8 | 9 | def self.keyword_arguments_for_initialize(decoded) 10 | { ciphertext: decoded[0] } 11 | end 12 | 13 | def initialize(ciphertext:, **keyword_arguments) 14 | super(**keyword_arguments) 15 | 16 | @ciphertext = ciphertext 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/cose/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module COSE 4 | class Error < StandardError; end 5 | end 6 | -------------------------------------------------------------------------------- /lib/cose/key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cbor" 4 | require "cose/key/ec2" 5 | require "cose/key/okp" 6 | require "cose/key/rsa" 7 | require "cose/key/symmetric" 8 | require "openssl" 9 | 10 | module COSE 11 | class Error < StandardError; end 12 | class KeyDeserializationError < Error; end 13 | class MalformedKeyError < KeyDeserializationError; end 14 | class UnknownKeyType < KeyDeserializationError; end 15 | 16 | module Key 17 | def self.serialize(pkey) 18 | from_pkey(pkey).serialize 19 | end 20 | 21 | def self.from_pkey(pkey) 22 | case pkey 23 | when OpenSSL::PKey::EC, OpenSSL::PKey::EC::Point 24 | COSE::Key::EC2.from_pkey(pkey) 25 | when OpenSSL::PKey::RSA 26 | COSE::Key::RSA.from_pkey(pkey) 27 | when OpenSSL::PKey::PKey 28 | COSE::Key::OKP.from_pkey(pkey) 29 | else 30 | raise "Unsupported #{pkey.class} object" 31 | end 32 | end 33 | 34 | def self.deserialize(data) 35 | map = cbor_decode(data) 36 | 37 | case map[Base::LABEL_KTY] 38 | when COSE::Key::OKP::KTY_OKP 39 | COSE::Key::OKP.from_map(map) 40 | when COSE::Key::EC2::KTY_EC2 41 | COSE::Key::EC2.from_map(map) 42 | when COSE::Key::RSA::KTY_RSA 43 | COSE::Key::RSA.from_map(map) 44 | when COSE::Key::Symmetric::KTY_SYMMETRIC 45 | COSE::Key::Symmetric.from_map(map) 46 | when nil 47 | raise COSE::UnknownKeyType, "Missing required key type kty label" 48 | else 49 | raise COSE::UnknownKeyType, "Unsupported or unknown key type #{map[Base::LABEL_KTY]}" 50 | end 51 | end 52 | 53 | def self.cbor_decode(data) 54 | CBOR.decode(data) 55 | rescue CBOR::MalformedFormatError, EOFError, FloatDomainError, RegexpError, TypeError, URI::InvalidURIError 56 | raise COSE::MalformedKeyError, "Malformed CBOR key input" 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/cose/key/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cbor" 4 | 5 | module COSE 6 | module Key 7 | class Base 8 | LABEL_BASE_IV = 5 9 | LABEL_KEY_OPS = 4 10 | LABEL_ALG = 3 11 | LABEL_KID = 2 12 | LABEL_KTY = 1 13 | 14 | def self.deserialize(cbor) 15 | from_map(CBOR.decode(cbor)) 16 | end 17 | 18 | def self.from_map(map) 19 | enforce_type(map) 20 | 21 | new( 22 | base_iv: map[LABEL_BASE_IV], 23 | key_ops: map[LABEL_KEY_OPS], 24 | alg: map[LABEL_ALG], 25 | kid: map[LABEL_KID], 26 | **keyword_arguments_for_initialize(map) 27 | ) 28 | end 29 | 30 | attr_accessor :kid, :alg, :key_ops, :base_iv 31 | 32 | def initialize(kid: nil, alg: nil, key_ops: nil, base_iv: nil) 33 | @kid = kid 34 | @alg = alg 35 | @key_ops = key_ops 36 | @base_iv = base_iv 37 | end 38 | 39 | def serialize 40 | CBOR.encode(map) 41 | end 42 | 43 | def map 44 | { 45 | LABEL_BASE_IV => base_iv, 46 | LABEL_KEY_OPS => key_ops, 47 | LABEL_ALG => alg, 48 | LABEL_KID => kid, 49 | }.compact 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/cose/key/curve.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module COSE 4 | module Key 5 | # https://tools.ietf.org/html/rfc8152#section-13.1 6 | Curve = Struct.new(:id, :name, :pkey_name) do 7 | @curves = {} 8 | 9 | def self.register(id, name, pkey_name) 10 | @curves[id] = new(id, name, pkey_name) 11 | end 12 | 13 | def self.find(id) 14 | @curves[id] 15 | end 16 | 17 | def self.by_name(name) 18 | @curves.values.detect { |curve| curve.name == name } 19 | end 20 | 21 | def self.by_pkey_name(pkey_name) 22 | @curves.values.detect { |curve| curve.pkey_name == pkey_name } 23 | end 24 | 25 | def value 26 | id 27 | end 28 | end 29 | end 30 | end 31 | 32 | COSE::Key::Curve.register(1, "P-256", "prime256v1") 33 | COSE::Key::Curve.register(2, "P-384", "secp384r1") 34 | COSE::Key::Curve.register(3, "P-521", "secp521r1") 35 | COSE::Key::Curve.register(6, "Ed25519", "ED25519") 36 | COSE::Key::Curve.register(7, "Ed448", "ED448") 37 | COSE::Key::Curve.register(8, "secp256k1", "secp256k1") 38 | -------------------------------------------------------------------------------- /lib/cose/key/curve_key.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/key/base" 4 | require "openssl" 5 | 6 | module COSE 7 | module Key 8 | class CurveKey < Base 9 | LABEL_CRV = -1 10 | LABEL_X = -2 11 | LABEL_D = -4 12 | 13 | attr_reader :crv, :d, :x 14 | 15 | def self.keyword_arguments_for_initialize(map) 16 | { 17 | crv: map[LABEL_CRV], 18 | x: map[LABEL_X], 19 | d: map[LABEL_D] 20 | } 21 | end 22 | 23 | def initialize(crv:, x: nil, d: nil, **keyword_arguments) # rubocop:disable Naming/MethodParameterName 24 | super(**keyword_arguments) 25 | 26 | if !crv 27 | raise ArgumentError, "Required crv is missing" 28 | elsif !x && !d 29 | raise ArgumentError, "x and d cannot be missing simultaneously" 30 | else 31 | @crv = crv 32 | @x = x 33 | @d = d 34 | end 35 | end 36 | 37 | def map 38 | super.merge( 39 | LABEL_CRV => crv, 40 | LABEL_X => x, 41 | LABEL_D => d 42 | ).compact 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/cose/key/ec2.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/key/curve" 4 | require "cose/key/curve_key" 5 | require "openssl" 6 | 7 | module COSE 8 | module Key 9 | class EC2 < CurveKey 10 | LABEL_Y = -3 11 | 12 | KTY_EC2 = 2 13 | 14 | ZERO_BYTE = "\0".b 15 | 16 | def self.enforce_type(map) 17 | if map[LABEL_KTY] != KTY_EC2 18 | raise "Not an EC2 key" 19 | end 20 | end 21 | 22 | def self.from_pkey(pkey) 23 | curve = Curve.by_pkey_name(pkey.group.curve_name) || raise("Unsupported EC curve #{pkey.group.curve_name}") 24 | 25 | case pkey 26 | when OpenSSL::PKey::EC::Point 27 | public_key = pkey 28 | when OpenSSL::PKey::EC 29 | public_key = pkey.public_key 30 | private_key = pkey.private_key 31 | else 32 | raise "Unsupported" 33 | end 34 | 35 | if public_key 36 | bytes = public_key.to_bn.to_s(2)[1..-1] 37 | 38 | coordinate_length = bytes.size / 2 39 | 40 | x = bytes[0..(coordinate_length - 1)] 41 | y = bytes[coordinate_length..-1] 42 | end 43 | 44 | if private_key 45 | d = private_key.to_s(2) 46 | end 47 | 48 | new(crv: curve.id, x: x, y: y, d: d) 49 | end 50 | 51 | attr_reader :y 52 | 53 | def initialize(y: nil, **keyword_arguments) # rubocop:disable Naming/MethodParameterName 54 | if (!y || !keyword_arguments[:x]) && !keyword_arguments[:d] 55 | raise ArgumentError, "Both x and y are required if d is missing" 56 | else 57 | super(**keyword_arguments) 58 | 59 | @y = y 60 | end 61 | end 62 | 63 | def map 64 | super.merge( 65 | Base::LABEL_KTY => KTY_EC2, 66 | LABEL_Y => y, 67 | ).compact 68 | end 69 | 70 | def to_pkey 71 | if curve 72 | group = OpenSSL::PKey::EC::Group.new(curve.pkey_name) 73 | public_key_bn = OpenSSL::BN.new("\x04" + pad_coordinate(group, x) + pad_coordinate(group, y), 2) 74 | public_key_point = OpenSSL::PKey::EC::Point.new(group, public_key_bn) 75 | 76 | # RFC5480 SubjectPublicKeyInfo 77 | asn1 = OpenSSL::ASN1::Sequence( 78 | [ 79 | OpenSSL::ASN1::Sequence( 80 | [ 81 | OpenSSL::ASN1::ObjectId("id-ecPublicKey"), 82 | OpenSSL::ASN1::ObjectId(curve.pkey_name), 83 | ] 84 | ), 85 | OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed)) 86 | ] 87 | ) 88 | 89 | if d 90 | # RFC5915 ECPrivateKey 91 | asn1 = OpenSSL::ASN1::Sequence( 92 | [ 93 | OpenSSL::ASN1::Integer.new(1), 94 | # Not properly padded but OpenSSL doesn't mind 95 | OpenSSL::ASN1::OctetString(OpenSSL::BN.new(d, 2).to_s(2)), 96 | OpenSSL::ASN1::ObjectId(curve.pkey_name, 0, :EXPLICIT), 97 | OpenSSL::ASN1::BitString(public_key_point.to_octet_string(:uncompressed), 1, :EXPLICIT), 98 | ] 99 | ) 100 | 101 | der = asn1.to_der 102 | return OpenSSL::PKey::EC.new(der) 103 | end 104 | 105 | OpenSSL::PKey::EC.new(asn1.to_der) 106 | else 107 | raise "Unsupported curve #{crv}" 108 | end 109 | end 110 | 111 | def curve 112 | Curve.find(crv) 113 | end 114 | 115 | def self.keyword_arguments_for_initialize(map) 116 | super.merge(y: map[LABEL_Y]) 117 | end 118 | 119 | def pad_coordinate(group, coordinate) 120 | coordinate_length = (group.degree + 7) / 8 121 | padding_required = coordinate_length - coordinate.length 122 | return coordinate if padding_required <= 0 123 | 124 | (ZERO_BYTE * padding_required) + coordinate 125 | end 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /lib/cose/key/okp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/key/curve" 4 | require "cose/key/curve_key" 5 | require "openssl" 6 | 7 | module COSE 8 | module Key 9 | class OKP < CurveKey 10 | KTY_OKP = 1 11 | 12 | def self.enforce_type(map) 13 | if map[LABEL_KTY] != KTY_OKP 14 | raise "Not an OKP key" 15 | end 16 | end 17 | 18 | def self.from_pkey(pkey) 19 | curve = Curve.by_pkey_name(pkey.oid) || raise("Unsupported edwards curve #{pkey.oid}") 20 | attributes = { crv: curve.id } 21 | 22 | asymmetric_key = pkey.public_to_der 23 | public_key_bit_string = OpenSSL::ASN1.decode(asymmetric_key).value.last.value 24 | attributes[:x] = public_key_bit_string 25 | begin 26 | asymmetric_key = pkey.private_to_der 27 | private_key = OpenSSL::ASN1.decode(asymmetric_key).value.last.value 28 | curve_private_key = OpenSSL::ASN1.decode(private_key).value 29 | attributes[:d] = curve_private_key 30 | rescue OpenSSL::PKey::PKeyError 31 | # work around lack of https://github.com/ruby/openssl/pull/527, otherwise raises this error 32 | # with message 'i2d_PKCS8PrivateKey_bio: error converting private key' for public keys 33 | nil 34 | end 35 | 36 | new(**attributes) 37 | end 38 | 39 | def map 40 | super.merge(LABEL_KTY => KTY_OKP) 41 | end 42 | 43 | def to_pkey 44 | if curve 45 | private_key_algo = OpenSSL::ASN1::Sequence.new( 46 | [OpenSSL::ASN1::ObjectId.new(curve.pkey_name)] 47 | ) 48 | seq = if d 49 | version = OpenSSL::ASN1::Integer.new(0) 50 | curve_private_key = OpenSSL::ASN1::OctetString.new(d).to_der 51 | private_key = OpenSSL::ASN1::OctetString.new(curve_private_key) 52 | [version, private_key_algo, private_key] 53 | else 54 | public_key = OpenSSL::ASN1::BitString.new(x) 55 | [private_key_algo, public_key] 56 | end 57 | 58 | asymmetric_key = OpenSSL::ASN1::Sequence.new(seq) 59 | OpenSSL::PKey.read(asymmetric_key.to_der) 60 | else 61 | raise "Unsupported curve #{crv}" 62 | end 63 | end 64 | 65 | def curve 66 | Curve.find(crv) 67 | end 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /lib/cose/key/rsa.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/key/base" 4 | require "openssl" 5 | 6 | module COSE 7 | module Key 8 | class RSA < Base 9 | LABEL_N = -1 10 | LABEL_E = -2 11 | LABEL_D = -3 12 | LABEL_P = -4 13 | LABEL_Q = -5 14 | LABEL_DP = -6 15 | LABEL_DQ = -7 16 | LABEL_QINV = -8 17 | 18 | KTY_RSA = 3 19 | 20 | def self.enforce_type(map) 21 | if map[LABEL_KTY] != KTY_RSA 22 | raise "Not an RSA key" 23 | end 24 | end 25 | 26 | def self.from_pkey(pkey) 27 | params = pkey.params 28 | 29 | attributes = { 30 | n: params["n"].to_s(2), 31 | e: params["e"].to_s(2) 32 | } 33 | 34 | if pkey.private? 35 | attributes.merge!( 36 | d: params["d"].to_s(2), 37 | p: params["p"].to_s(2), 38 | q: params["q"].to_s(2), 39 | dp: params["dmp1"].to_s(2), 40 | dq: params["dmq1"].to_s(2), 41 | qinv: params["iqmp"].to_s(2) 42 | ) 43 | end 44 | 45 | new(**attributes) 46 | end 47 | 48 | attr_reader :n, :e, :d, :p, :q, :dp, :dq, :qinv 49 | 50 | def initialize(n:, e:, d: nil, p: nil, q: nil, dp: nil, dq: nil, qinv: nil, **keyword_arguments) # rubocop:disable Naming/MethodParameterName 51 | super(**keyword_arguments) 52 | 53 | if !n 54 | raise ArgumentError, "Required public field n is missing" 55 | elsif !e 56 | raise ArgumentError, "Required public field e is missing" 57 | else 58 | private_fields = { d: d, p: p, q: q, dp: dp, dq: dq, qinv: qinv } 59 | 60 | if private_fields.values.all?(&:nil?) || private_fields.values.none?(&:nil?) 61 | @n = n 62 | @e = e 63 | @d = d 64 | @p = p 65 | @q = q 66 | @dp = dp 67 | @dq = dq 68 | @qinv = qinv 69 | else 70 | missing = private_fields.detect { |_k, v| v.nil? }[0] 71 | raise ArgumentError, "Incomplete private fields, #{missing} is missing" 72 | end 73 | end 74 | end 75 | 76 | def map 77 | super.merge( 78 | Base::LABEL_KTY => KTY_RSA, 79 | LABEL_N => n, 80 | LABEL_E => e, 81 | LABEL_D => d, 82 | LABEL_P => p, 83 | LABEL_Q => q, 84 | LABEL_DP => dp, 85 | LABEL_DQ => dq, 86 | LABEL_QINV => qinv 87 | ).compact 88 | end 89 | 90 | def to_pkey 91 | # PKCS#1 RSAPublicKey 92 | asn1 = OpenSSL::ASN1::Sequence( 93 | [ 94 | OpenSSL::ASN1::Integer.new(bn(n)), 95 | OpenSSL::ASN1::Integer.new(bn(e)), 96 | ] 97 | ) 98 | pkey = OpenSSL::PKey::RSA.new(asn1.to_der) 99 | 100 | if private? 101 | # PKCS#1 RSAPrivateKey 102 | asn1 = OpenSSL::ASN1::Sequence( 103 | [ 104 | OpenSSL::ASN1::Integer.new(0), 105 | OpenSSL::ASN1::Integer.new(bn(n)), 106 | OpenSSL::ASN1::Integer.new(bn(e)), 107 | OpenSSL::ASN1::Integer.new(bn(d)), 108 | OpenSSL::ASN1::Integer.new(bn(p)), 109 | OpenSSL::ASN1::Integer.new(bn(q)), 110 | OpenSSL::ASN1::Integer.new(bn(dp)), 111 | OpenSSL::ASN1::Integer.new(bn(dq)), 112 | OpenSSL::ASN1::Integer.new(bn(qinv)), 113 | ] 114 | ) 115 | 116 | pkey = OpenSSL::PKey::RSA.new(asn1.to_der) 117 | end 118 | 119 | pkey 120 | end 121 | 122 | def self.keyword_arguments_for_initialize(map) 123 | { 124 | n: map[LABEL_N], 125 | e: map[LABEL_E], 126 | d: map[LABEL_D], 127 | p: map[LABEL_P], 128 | q: map[LABEL_Q], 129 | dp: map[LABEL_DP], 130 | dq: map[LABEL_DQ], 131 | qinv: map[LABEL_QINV] 132 | } 133 | end 134 | 135 | private 136 | 137 | def private? 138 | [d, p, q, dp, dq, qinv].none?(&:nil?) 139 | end 140 | 141 | def bn(data) 142 | if data 143 | OpenSSL::BN.new(data, 2) 144 | end 145 | end 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/cose/key/symmetric.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/key/base" 4 | 5 | module COSE 6 | module Key 7 | class Symmetric < Base 8 | LABEL_K = -1 9 | 10 | KTY_SYMMETRIC = 4 11 | 12 | attr_reader :k 13 | 14 | def self.enforce_type(map) 15 | if map[LABEL_KTY] != KTY_SYMMETRIC 16 | raise "Not a Symmetric key" 17 | end 18 | end 19 | 20 | def initialize(k:, **keyword_arguments) # rubocop:disable Naming/MethodParameterName 21 | super(**keyword_arguments) 22 | 23 | if !k 24 | raise ArgumentError, "Required key value k is missing" 25 | else 26 | @k = k 27 | end 28 | end 29 | 30 | def map 31 | super.merge(LABEL_KTY => KTY_SYMMETRIC, LABEL_K => k) 32 | end 33 | 34 | def self.keyword_arguments_for_initialize(map) 35 | { k: map[LABEL_K] } 36 | end 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/cose/mac.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/recipient" 4 | require "cose/mac0" 5 | 6 | module COSE 7 | class Mac < Mac0 8 | CONTEXT = "MAC" 9 | 10 | attr_reader :recipients 11 | 12 | def self.keyword_arguments_for_initialize(decoded) 13 | super.merge(recipients: decoded.last.map { |r| COSE::Recipient.from_array(r) }) 14 | end 15 | 16 | def self.tag 17 | 97 18 | end 19 | 20 | def initialize(recipients:, **keyword_arguments) 21 | super(**keyword_arguments) 22 | 23 | @recipients = recipients 24 | end 25 | 26 | def verify(key, external_aad = nil) 27 | recipient = recipients.detect { |r| r.headers.kid == key.kid } 28 | 29 | if recipient 30 | super 31 | else 32 | raise(COSE::Error, "No recipient match the key") 33 | end 34 | end 35 | 36 | private 37 | 38 | def context 39 | CONTEXT 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/cose/mac0.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cbor" 4 | require "cose/security_message" 5 | require "openssl" 6 | 7 | module COSE 8 | class Mac0 < SecurityMessage 9 | CONTEXT = "MAC0" 10 | 11 | attr_reader :payload, :tag 12 | 13 | def self.keyword_arguments_for_initialize(decoded) 14 | { payload: decoded[0], tag: decoded[1] } 15 | end 16 | 17 | def self.tag 18 | 17 19 | end 20 | 21 | def initialize(payload:, tag:, **keyword_arguments) 22 | super(**keyword_arguments) 23 | 24 | @payload = payload 25 | @tag = tag 26 | end 27 | 28 | def verify(key, external_aad = nil) 29 | tag == algorithm.mac(key.k, data(external_aad)) || raise(COSE::Error, "Mac0 verification failed") 30 | end 31 | 32 | private 33 | 34 | def data(external_aad = nil) 35 | CBOR.encode([context, serialized_map(protected_headers), external_aad || zero_length_bin_string, payload]) 36 | end 37 | 38 | def context 39 | CONTEXT 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/cose/recipient.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/security_message" 4 | 5 | module COSE 6 | class Recipient < SecurityMessage 7 | attr_reader :ciphertext, :recipients 8 | 9 | def self.keyword_arguments_for_initialize(decoded) 10 | keyword_arguments = { ciphertext: decoded[0] } 11 | 12 | if decoded[1] 13 | keyword_arguments[:recipients] = decoded[1].map { |s| COSE::Recipient.deserialize(s) } 14 | end 15 | 16 | keyword_arguments 17 | end 18 | 19 | def initialize(ciphertext:, recipients: nil, **keyword_arguments) 20 | super(**keyword_arguments) 21 | 22 | @ciphertext = ciphertext 23 | @recipients = recipients 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/cose/security_message.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cbor" 4 | require "cose/algorithm" 5 | require "cose/error" 6 | require "cose/security_message/headers" 7 | 8 | module COSE 9 | class SecurityMessage 10 | ZERO_LENGTH_BIN_STRING = "".b 11 | 12 | attr_reader :protected_headers, :unprotected_headers 13 | 14 | def self.deserialize(cbor) 15 | decoded = CBOR.decode(cbor) 16 | 17 | if decoded.is_a?(CBOR::Tagged) 18 | if respond_to?(:tag) && tag != decoded.tag 19 | raise(COSE::Error, "Invalid CBOR tag") 20 | end 21 | 22 | decoded = decoded.value 23 | end 24 | 25 | from_array(decoded) 26 | end 27 | 28 | def self.from_array(array) 29 | new( 30 | protected_headers: deserialize_headers(array[0]), 31 | unprotected_headers: array[1], 32 | **keyword_arguments_for_initialize(array[2..-1]) 33 | ) 34 | end 35 | 36 | def self.deserialize_headers(data) 37 | if data == ZERO_LENGTH_BIN_STRING 38 | {} 39 | else 40 | CBOR.decode(data) 41 | end 42 | end 43 | 44 | def initialize(protected_headers:, unprotected_headers:) 45 | @protected_headers = protected_headers 46 | @unprotected_headers = unprotected_headers 47 | end 48 | 49 | def algorithm 50 | @algorithm ||= COSE::Algorithm.find(headers.alg) || raise(COSE::Error, "Unsupported algorithm '#{headers.alg}'") 51 | end 52 | 53 | def headers 54 | @headers ||= Headers.new(protected_headers, unprotected_headers) 55 | end 56 | 57 | private 58 | 59 | def serialized_map(map) 60 | if map && !map.empty? 61 | map.to_cbor 62 | else 63 | zero_length_bin_string 64 | end 65 | end 66 | 67 | def zero_length_bin_string 68 | ZERO_LENGTH_BIN_STRING 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/cose/security_message/headers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module COSE 4 | class SecurityMessage 5 | class Headers 6 | HEADER_LABEL_ALG = 1 7 | HEADER_LABEL_KID = 4 8 | 9 | attr_reader :protected_bucket, :unprotected_bucket 10 | 11 | def initialize(protected_bucket, unprotected_bucket) 12 | @protected_bucket = protected_bucket 13 | @unprotected_bucket = unprotected_bucket 14 | end 15 | 16 | def alg 17 | header(HEADER_LABEL_ALG) 18 | end 19 | 20 | def kid 21 | header(HEADER_LABEL_KID) 22 | end 23 | 24 | private 25 | 26 | def header(label) 27 | protected_bucket[label] || unprotected_bucket[label] 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/cose/sign.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cbor" 4 | require "cose/error" 5 | require "cose/security_message" 6 | require "cose/signature" 7 | 8 | module COSE 9 | class Sign < SecurityMessage 10 | CONTEXT = "Signature" 11 | 12 | attr_reader :payload, :signatures 13 | 14 | def self.keyword_arguments_for_initialize(decoded) 15 | { payload: decoded[0], signatures: decoded[1].map { |s| COSE::Signature.from_array(s) } } 16 | end 17 | 18 | def self.tag 19 | 98 20 | end 21 | 22 | def initialize(payload:, signatures:, **keyword_arguments) 23 | super(**keyword_arguments) 24 | 25 | @payload = payload 26 | @signatures = signatures 27 | end 28 | 29 | def verify(key, external_aad = nil) 30 | signature = signatures.detect { |s| s.headers.kid == key.kid } 31 | 32 | if signature 33 | signature.algorithm.verify(key, signature.signature, verification_data(signature, external_aad)) 34 | else 35 | raise(COSE::Error, "No signature matches key kid") 36 | end 37 | end 38 | 39 | private 40 | 41 | def verification_data(signature, external_aad = nil) 42 | @verification_data ||= 43 | CBOR.encode( 44 | [ 45 | CONTEXT, 46 | serialized_map(protected_headers), 47 | serialized_map(signature.protected_headers), 48 | external_aad || ZERO_LENGTH_BIN_STRING, 49 | payload 50 | ] 51 | ) 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/cose/sign1.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cbor" 4 | require "cose/error" 5 | require "cose/security_message" 6 | 7 | module COSE 8 | class Sign1 < SecurityMessage 9 | CONTEXT = "Signature1" 10 | 11 | attr_reader :payload, :signature 12 | 13 | def self.keyword_arguments_for_initialize(decoded) 14 | { payload: decoded[0], signature: decoded[1] } 15 | end 16 | 17 | def self.tag 18 | 18 19 | end 20 | 21 | def initialize(payload:, signature:, **keyword_arguments) 22 | super(**keyword_arguments) 23 | 24 | @payload = payload 25 | @signature = signature 26 | end 27 | 28 | def verify(key, external_aad = nil) 29 | if key.kid == headers.kid 30 | algorithm.verify(key, signature, verification_data(external_aad)) 31 | else 32 | raise(COSE::Error, "Non matching kid") 33 | end 34 | end 35 | 36 | private 37 | 38 | def verification_data(external_aad = nil) 39 | CBOR.encode([CONTEXT, serialized_map(protected_headers), external_aad || ZERO_LENGTH_BIN_STRING, payload]) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/cose/signature.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/security_message" 4 | 5 | module COSE 6 | class Signature < SecurityMessage 7 | attr_reader :signature 8 | 9 | def self.keyword_arguments_for_initialize(decoded) 10 | { signature: decoded[0] } 11 | end 12 | 13 | def initialize(signature:, **keyword_arguments) 14 | super(**keyword_arguments) 15 | 16 | @signature = signature 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/cose/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module COSE 4 | VERSION = "1.3.1" 5 | end 6 | -------------------------------------------------------------------------------- /spec/cose/encrypt0_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/encrypt0" 4 | 5 | RSpec.describe "COSE::Encrypt0" do 6 | context ".deserialize" do 7 | before do 8 | cbor = create_security_message({ 1 => 10 }, { 5 => "init-vector".b }, "ciphertext".b) 9 | 10 | @encrypt0 = COSE::Encrypt0.deserialize(cbor) 11 | end 12 | 13 | it "returns protected headers" do 14 | expect(@encrypt0.protected_headers).to eq(1 => 10) 15 | end 16 | 17 | it "returns unprotected headers" do 18 | expect(@encrypt0.unprotected_headers).to eq(5 => "init-vector".b) 19 | end 20 | 21 | it "returns the ciphertext" do 22 | expect(@encrypt0.ciphertext).to eq("ciphertext".b) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/cose/encrypt_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/encrypt" 4 | require "cose/recipient" 5 | 6 | RSpec.describe "COSE::Encrypt" do 7 | context ".deserialize" do 8 | before do 9 | cbor = create_security_message( 10 | { 1 => 2 }, 11 | { 3 => 4 }, 12 | "ciphertext".b, 13 | [ 14 | create_security_message({ 5 => 6 }, { 6 => 7 }, "ciphertextA".b), 15 | create_security_message({ 8 => 9 }, { 10 => 11 }, "ciphertextB".b) 16 | ] 17 | ) 18 | 19 | @encrypt = COSE::Encrypt.deserialize(cbor) 20 | end 21 | 22 | it "returns protected headers" do 23 | expect(@encrypt.protected_headers).to eq(1 => 2) 24 | end 25 | 26 | it "returns unprotected headers" do 27 | expect(@encrypt.unprotected_headers).to eq(3 => 4) 28 | end 29 | 30 | it "returns the recipients" do 31 | expect(@encrypt.recipients.size).to eq(2) 32 | expect(@encrypt.recipients.all? { |s| s.is_a?(COSE::Recipient) }).to be_truthy 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/cose/key/ec2_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/key/ec2" 4 | require "openssl" 5 | 6 | RSpec.describe COSE::Key::EC2 do 7 | describe ".new" do 8 | it "validates crv presence" do 9 | expect { COSE::Key::EC2.new(crv: nil, x: "x".b, y: "y".b) }.to raise_error("Required crv is missing") 10 | end 11 | 12 | it "validates presence of at least x and y if d missing" do 13 | expect { 14 | COSE::Key::EC2.new(crv: 4, x: nil, y: nil) 15 | }.to raise_error("Both x and y are required if d is missing") 16 | 17 | expect { 18 | COSE::Key::EC2.new(crv: 4, x: "x".b, y: nil) 19 | }.to raise_error("Both x and y are required if d is missing") 20 | 21 | expect { 22 | COSE::Key::EC2.new(crv: 4, x: nil, y: "y".b) 23 | }.to raise_error("Both x and y are required if d is missing") 24 | end 25 | 26 | it "can build a public key" do 27 | key = COSE::Key::EC2.new(crv: 4, x: "x".b, y: "y".b) 28 | 29 | expect(key.crv).to eq(4) 30 | expect(key.x).to eq("x".b) 31 | expect(key.y).to eq("y".b) 32 | expect(key.d).to eq(nil) 33 | end 34 | 35 | it "can build a private key without x and y" do 36 | key = COSE::Key::EC2.new(crv: 4, d: "d".b) 37 | 38 | expect(key.crv).to eq(4) 39 | expect(key.x).to eq(nil) 40 | expect(key.y).to eq(nil) 41 | expect(key.d).to eq("d".b) 42 | end 43 | 44 | it "can build a private key with x and y" do 45 | key = COSE::Key::EC2.new(crv: 4, x: "x".b, y: "y".b, d: "d".b) 46 | 47 | expect(key.crv).to eq(4) 48 | expect(key.x).to eq("x".b) 49 | expect(key.y).to eq("y".b) 50 | expect(key.d).to eq("d".b) 51 | end 52 | end 53 | 54 | describe ".deserialize" do 55 | it "works" do 56 | key = COSE::Key::EC2.deserialize( 57 | CBOR.encode( 58 | 5 => "init-vector".b, 59 | 4 => 1, 60 | 3 => -7, 61 | 2 => "id".b, 62 | 1 => 2, 63 | -1 => 1, 64 | -2 => "x".b, 65 | -3 => "y".b, 66 | -4 => "d".b 67 | ) 68 | ) 69 | 70 | expect(key.base_iv).to eq("init-vector".b) 71 | expect(key.key_ops).to eq(1) 72 | expect(key.alg).to eq(-7) 73 | expect(key.kid).to eq("id".b) 74 | expect(key.crv).to eq(1) 75 | expect(key.x).to eq("x".b) 76 | expect(key.y).to eq("y".b) 77 | expect(key.d).to eq("d".b) 78 | end 79 | 80 | it "returns an error if key type is wrong" do 81 | expect { 82 | COSE::Key::EC2.deserialize( 83 | CBOR.encode( 84 | 1 => 4, 85 | -1 => 1, 86 | -2 => "x", 87 | -3 => "y" 88 | ) 89 | ) 90 | }.to raise_error("Not an EC2 key") 91 | end 92 | end 93 | 94 | context "#to_pkey" do 95 | it "works for an EC key in the P-256 curve" do 96 | original_pkey = OpenSSL::PKey::EC.generate("prime256v1") 97 | pkey = COSE::Key::EC2.from_pkey(original_pkey).to_pkey 98 | 99 | expect(pkey).to be_a(OpenSSL::PKey::EC) 100 | expect(pkey.group.curve_name).to eq("prime256v1") 101 | expect(pkey.public_key).to eq(original_pkey.public_key) 102 | expect(pkey.private_key).to eq(original_pkey.private_key) 103 | end 104 | 105 | it "works for an EC key in the P-384 curve" do 106 | original_pkey = OpenSSL::PKey::EC.generate("secp384r1") 107 | pkey = COSE::Key::EC2.from_pkey(original_pkey).to_pkey 108 | 109 | expect(pkey).to be_a(OpenSSL::PKey::EC) 110 | expect(pkey.group.curve_name).to eq("secp384r1") 111 | expect(pkey.public_key).to eq(original_pkey.public_key) 112 | expect(pkey.private_key).to eq(original_pkey.private_key) 113 | end 114 | 115 | it "works for an EC key in the P-521 curve" do 116 | original_pkey = OpenSSL::PKey::EC.generate("secp521r1") 117 | pkey = COSE::Key::EC2.from_pkey(original_pkey).to_pkey 118 | 119 | expect(pkey).to be_a(OpenSSL::PKey::EC) 120 | expect(pkey.group.curve_name).to eq("secp521r1") 121 | expect(pkey.public_key).to eq(original_pkey.public_key) 122 | expect(pkey.private_key).to eq(original_pkey.private_key) 123 | end 124 | 125 | it "works for an EC key in the secp256k1 curve" do 126 | original_pkey = OpenSSL::PKey::EC.generate("secp256k1") 127 | pkey = COSE::Key::EC2.from_pkey(original_pkey).to_pkey 128 | 129 | expect(pkey).to be_a(OpenSSL::PKey::EC) 130 | expect(pkey.group.curve_name).to eq("secp256k1") 131 | expect(pkey.public_key).to eq(original_pkey.public_key) 132 | expect(pkey.private_key).to eq(original_pkey.private_key) 133 | end 134 | 135 | it "works for an EC key that omits leading zero" do 136 | # x was encoded omitting a leading zero. Before calling OpenSSL we must pad it. 137 | x = ")\xC6`8\xBC\xEE\xF9*\xA4S\x9E\xA7\xFA'\xCE\xB9\x8D\x8C\xF7U\x06\xD8B\xB8\x8B\x9A\xF6\x9B\xC1\n\xCB".b 138 | y = "Y\x9C\xD0+y<\xCB9Vk-\xC4\xEB\x87\xA7\xA1\xFA\xFEF\xAD\xD7\xA6\xB8\x84\xBEm[\xD7\xAEN\xD6w".b 139 | original_key = COSE::Key::EC2.new( 140 | kid: "id".b, 141 | alg: -7, 142 | key_ops: 1, 143 | base_iv: "init-vector".b, 144 | crv: 1, 145 | x: x, 146 | y: y, 147 | ) 148 | 149 | pkey = original_key.to_pkey 150 | key = COSE::Key::EC2.from_pkey(pkey) 151 | 152 | expect(pkey).to be_a(OpenSSL::PKey::EC) 153 | expect(pkey.group.curve_name).to eq("prime256v1") 154 | expect(key.x).to eq("\0".b + x) 155 | expect(key.y).to eq(y) 156 | end 157 | end 158 | 159 | describe "#serialize" do 160 | it "works" do 161 | key = COSE::Key::EC2.new( 162 | kid: "id".b, 163 | alg: -7, 164 | key_ops: 1, 165 | base_iv: "init-vector".b, 166 | crv: 1, 167 | x: "x".b, 168 | y: "y".b, 169 | d: "d".b 170 | ) 171 | 172 | serialized_key = key.serialize 173 | 174 | map = CBOR.decode(serialized_key) 175 | 176 | expect(map[5]).to eq("init-vector".b) 177 | expect(map[4]).to eq(1) 178 | expect(map[3]).to eq(-7) 179 | expect(map[2]).to eq("id".b) 180 | expect(map[1]).to eq(2) 181 | expect(map[-1]).to eq(1) 182 | expect(map[-2]).to eq("x".b) 183 | expect(map[-3]).to eq("y".b) 184 | expect(map[-4]).to eq("d".b) 185 | end 186 | 187 | it "does not include labels without value" do 188 | key = COSE::Key::EC2.new(crv: 1, d: "d".b) 189 | 190 | serialized_key = key.serialize 191 | 192 | map = CBOR.decode(serialized_key) 193 | 194 | expect(map.keys.size).to eq(3) 195 | expect(map[1]).to eq(2) 196 | expect(map[-1]).to eq(1) 197 | expect(map[-4]).to eq("d".b) 198 | end 199 | end 200 | end 201 | -------------------------------------------------------------------------------- /spec/cose/key/okp_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cbor" 4 | require "cose/key/okp" 5 | 6 | RSpec.describe COSE::Key::OKP do 7 | describe ".new" do 8 | it "validates crv presence" do 9 | expect { COSE::Key::OKP.new(crv: nil) }.to raise_error("Required crv is missing") 10 | end 11 | 12 | it "validates presence of at least x or d" do 13 | expect { COSE::Key::OKP.new(crv: 4) }.to raise_error("x and d cannot be missing simultaneously") 14 | end 15 | 16 | it "can build a public key" do 17 | key = COSE::Key::OKP.new(crv: 4, x: "x".b) 18 | 19 | expect(key.crv).to eq(4) 20 | expect(key.x).to eq("x".b) 21 | expect(key.d).to eq(nil) 22 | end 23 | 24 | it "can build a private key without x" do 25 | key = COSE::Key::OKP.new(crv: 4, d: "d".b) 26 | 27 | expect(key.crv).to eq(4) 28 | expect(key.x).to eq(nil) 29 | expect(key.d).to eq("d".b) 30 | end 31 | 32 | it "can build a private key with x" do 33 | key = COSE::Key::OKP.new(crv: 4, x: "x".b, d: "d".b) 34 | 35 | expect(key.crv).to eq(4) 36 | expect(key.x).to eq("x".b) 37 | expect(key.d).to eq("d".b) 38 | end 39 | end 40 | 41 | context "#to_pkey" do 42 | if curve_25519_supported? 43 | it "works for an Ed25519 private key" do 44 | original_pkey = OpenSSL::PKey.generate_key("ED25519") 45 | pkey = COSE::Key::OKP.from_pkey(original_pkey).to_pkey 46 | 47 | expect(pkey).to be_a(OpenSSL::PKey::PKey) 48 | expect(pkey.oid).to eq("ED25519") 49 | expect(pkey.public_to_der).to eq(original_pkey.public_to_der) 50 | expect(pkey.private_to_der).to eq(original_pkey.private_to_der) 51 | end 52 | 53 | it "works for an Ed25519 public key" do 54 | original_pkey = OpenSSL::PKey.generate_key("ED25519") 55 | public_key = OpenSSL::PKey.read(original_pkey.public_to_der) 56 | pkey = COSE::Key::OKP.from_pkey(public_key).to_pkey 57 | 58 | expect(pkey).to be_a(OpenSSL::PKey::PKey) 59 | expect(pkey.oid).to eq("ED25519") 60 | expect(pkey.public_to_der).to eq(original_pkey.public_to_der) 61 | end 62 | 63 | it "works for an Ed448 private key" do 64 | original_pkey = OpenSSL::PKey.generate_key("ED448") 65 | pkey = COSE::Key::OKP.from_pkey(original_pkey).to_pkey 66 | 67 | expect(pkey).to be_a(OpenSSL::PKey::PKey) 68 | expect(pkey.oid).to eq("ED448") 69 | expect(pkey.public_to_der).to eq(original_pkey.public_to_der) 70 | expect(pkey.private_to_der).to eq(original_pkey.private_to_der) 71 | end 72 | 73 | it "works for an Ed448 public key" do 74 | original_pkey = OpenSSL::PKey.generate_key("ED448") 75 | public_key = OpenSSL::PKey.read(original_pkey.public_to_der) 76 | pkey = COSE::Key::OKP.from_pkey(public_key).to_pkey 77 | 78 | expect(pkey).to be_a(OpenSSL::PKey::PKey) 79 | expect(pkey.oid).to eq("ED448") 80 | expect(pkey.public_to_der).to eq(original_pkey.public_to_der) 81 | end 82 | end 83 | end 84 | 85 | describe ".deserialize" do 86 | it "works" do 87 | key = COSE::Key::OKP.deserialize( 88 | CBOR.encode( 89 | 5 => "init-vector".b, 90 | 4 => 1, 91 | 3 => 0, 92 | 2 => "id".b, 93 | 1 => 1, 94 | -1 => 4, 95 | -2 => "x".b, 96 | -4 => "d".b 97 | ) 98 | ) 99 | 100 | expect(key.base_iv).to eq("init-vector".b) 101 | expect(key.key_ops).to eq(1) 102 | expect(key.alg).to eq(0) 103 | expect(key.kid).to eq("id".b) 104 | expect(key.crv).to eq(4) 105 | expect(key.x).to eq("x".b) 106 | expect(key.d).to eq("d".b) 107 | end 108 | 109 | it "returns an error if key type is wrong" do 110 | expect { 111 | COSE::Key::OKP.deserialize( 112 | CBOR.encode( 113 | 1 => 2, 114 | -1 => 4, 115 | -2 => "x".b, 116 | ) 117 | ) 118 | }.to raise_error("Not an OKP key") 119 | end 120 | end 121 | 122 | describe "#serialize" do 123 | it "works" do 124 | key = COSE::Key::OKP.new( 125 | kid: "id".b, 126 | alg: -7, 127 | key_ops: 1, 128 | base_iv: "init-vector".b, 129 | crv: 4, 130 | x: "x".b, 131 | d: "d".b 132 | ) 133 | 134 | serialized_key = key.serialize 135 | 136 | map = CBOR.decode(serialized_key) 137 | 138 | expect(map[5]).to eq("init-vector".b) 139 | expect(map[4]).to eq(1) 140 | expect(map[3]).to eq(-7) 141 | expect(map[2]).to eq("id".b) 142 | expect(map[1]).to eq(1) 143 | expect(map[-1]).to eq(4) 144 | expect(map[-2]).to eq("x".b) 145 | expect(map[-4]).to eq("d".b) 146 | end 147 | 148 | it "does not include labels without value" do 149 | key = COSE::Key::OKP.new(crv: 4, x: "x".b) 150 | 151 | serialized_key = key.serialize 152 | 153 | map = CBOR.decode(serialized_key) 154 | 155 | expect(map.keys.size).to eq(3) 156 | expect(map[1]).to eq(1) 157 | expect(map[-1]).to eq(4) 158 | expect(map[-2]).to eq("x".b) 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /spec/cose/key/rsa_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/key/rsa" 4 | require "openssl" 5 | 6 | RSpec.describe COSE::Key::RSA do 7 | describe ".new" do 8 | it "can build a public key" do 9 | key = COSE::Key::RSA.new(n: "n".b, e: "e".b) 10 | 11 | expect(key.n).to eq("n".b) 12 | expect(key.e).to eq("e".b) 13 | end 14 | 15 | it "can build a private key with two primes" do 16 | key = COSE::Key::RSA.new( 17 | n: "n".b, 18 | e: "e".b, 19 | d: "d".b, 20 | p: "p".b, 21 | q: "q".b, 22 | dp: "dP".b, 23 | dq: "dQ".b, 24 | qinv: "qInv".b 25 | ) 26 | 27 | expect(key.n).to eq("n".b) 28 | expect(key.e).to eq("e".b) 29 | expect(key.d).to eq("d".b) 30 | expect(key.p).to eq("p".b) 31 | expect(key.q).to eq("q".b) 32 | expect(key.dp).to eq("dP".b) 33 | expect(key.dq).to eq("dQ".b) 34 | expect(key.qinv).to eq("qInv".b) 35 | end 36 | 37 | it "validates presence of all public key fields" do 38 | expect { 39 | COSE::Key::RSA.new(n: "n".b, e: nil) 40 | }.to raise_error("Required public field e is missing") 41 | 42 | expect { 43 | COSE::Key::RSA.new(n: nil, e: "e".b) 44 | }.to raise_error("Required public field n is missing") 45 | end 46 | 47 | it "validates presence of all private key fields" do 48 | private_fields = { 49 | d: "d".b, 50 | p: "p".b, 51 | q: "q".b, 52 | dp: "dP".b, 53 | dq: "dQ".b, 54 | qinv: "qInv".b 55 | } 56 | 57 | public_fields = { 58 | n: "n".b, 59 | e: "e".b 60 | } 61 | 62 | valid_arguments = public_fields.merge(private_fields) 63 | 64 | private_fields.each do |k, _v| 65 | invalid_arguments = valid_arguments.dup 66 | invalid_arguments[k] = nil 67 | 68 | expect { COSE::Key::RSA.new(**invalid_arguments) }.to raise_error("Incomplete private fields, #{k} is missing") 69 | end 70 | end 71 | end 72 | 73 | describe ".deserialize" do 74 | it "returns an error if key type is wrong" do 75 | expect { 76 | COSE::Key::RSA.deserialize( 77 | CBOR.encode( 78 | 1 => 4, 79 | -1 => "n", 80 | -2 => "e" 81 | ) 82 | ) 83 | }.to raise_error("Not an RSA key") 84 | end 85 | 86 | it "works" do 87 | key = COSE::Key::RSA.deserialize( 88 | CBOR.encode( 89 | 5 => "init-vector".b, 90 | 4 => 1, 91 | 3 => -37, 92 | 2 => "id".b, 93 | 1 => 3, 94 | -1 => "n".b, 95 | -2 => "e".b, 96 | -3 => "d".b, 97 | -4 => "p".b, 98 | -5 => "q".b, 99 | -6 => "dP".b, 100 | -7 => "dQ".b, 101 | -8 => "qInv".b 102 | ) 103 | ) 104 | 105 | expect(key.base_iv).to eq("init-vector".b) 106 | expect(key.key_ops).to eq(1) 107 | expect(key.alg).to eq(-37) 108 | expect(key.kid).to eq("id".b) 109 | expect(key.n).to eq("n".b) 110 | expect(key.e).to eq("e".b) 111 | expect(key.d).to eq("d".b) 112 | expect(key.p).to eq("p".b) 113 | expect(key.q).to eq("q".b) 114 | expect(key.dp).to eq("dP".b) 115 | expect(key.dq).to eq("dQ".b) 116 | expect(key.qinv).to eq("qInv".b) 117 | end 118 | end 119 | 120 | context "#to_pkey" do 121 | let(:original_pkey) { OpenSSL::PKey::RSA.new(2048) } 122 | 123 | let(:pkey) do 124 | COSE::Key::RSA.from_pkey(original_pkey).to_pkey 125 | end 126 | 127 | it "it generates an instance of OpenSSL::PKey::PKey" do 128 | expect(pkey).to be_a(OpenSSL::PKey::RSA) 129 | end 130 | 131 | it "it generates the same key" do 132 | pkey.params.each do |param_name, param_value| 133 | expect(param_value).to eq(original_pkey.params[param_name]), "expected key param #{param_name} to match" 134 | end 135 | end 136 | 137 | context "for a public key" do 138 | let(:original_pkey) { OpenSSL::PKey::RSA.new(2048).public_key } 139 | 140 | it "it generates an instance of OpenSSL::PKey::PKey" do 141 | expect(pkey).to be_a(OpenSSL::PKey::RSA) 142 | end 143 | 144 | it "it generates the same key" do 145 | pkey.params.each do |param_name, param_value| 146 | expect(param_value).to eq(original_pkey.params[param_name]), "expected key param #{param_name} to match" 147 | end 148 | end 149 | end 150 | end 151 | 152 | context "#serialize" do 153 | it "works" do 154 | key = COSE::Key::RSA.new( 155 | kid: "id".b, 156 | alg: -37, 157 | key_ops: 1, 158 | base_iv: "init-vector".b, 159 | n: "n".b, 160 | e: "e".b, 161 | d: "d".b, 162 | p: "p".b, 163 | q: "q".b, 164 | dp: "dP".b, 165 | dq: "dQ".b, 166 | qinv: "qInv".b 167 | ) 168 | 169 | serialized_key = key.serialize 170 | 171 | map = CBOR.decode(serialized_key) 172 | 173 | expect(map[5]).to eq("init-vector".b) 174 | expect(map[4]).to eq(1) 175 | expect(map[3]).to eq(-37) 176 | expect(map[2]).to eq("id".b) 177 | expect(map[1]).to eq(3) 178 | expect(map[-1]).to eq("n".b) 179 | expect(map[-2]).to eq("e".b) 180 | expect(map[-3]).to eq("d".b) 181 | expect(map[-4]).to eq("p".b) 182 | expect(map[-5]).to eq("q".b) 183 | expect(map[-6]).to eq("dP".b) 184 | expect(map[-7]).to eq("dQ".b) 185 | expect(map[-8]).to eq("qInv".b) 186 | end 187 | 188 | it "does not include labels without value" do 189 | key = COSE::Key::RSA.new(n: "n".b, e: "e".b) 190 | 191 | serialized_key = key.serialize 192 | 193 | map = CBOR.decode(serialized_key) 194 | 195 | expect(map.keys.size).to eq(3) 196 | expect(map[1]).to eq(3) 197 | expect(map[-1]).to eq("n".b) 198 | expect(map[-2]).to eq("e".b) 199 | end 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /spec/cose/key/symmetric_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cbor" 4 | require "cose/key/symmetric" 5 | 6 | RSpec.describe COSE::Key::Symmetric do 7 | describe ".new" do 8 | it "validates k presence" do 9 | expect { COSE::Key::Symmetric.new(k: nil) }.to raise_error("Required key value k is missing") 10 | end 11 | end 12 | 13 | describe ".deserialize" do 14 | it "works" do 15 | key = COSE::Key::Symmetric.deserialize( 16 | CBOR.encode( 17 | 5 => "init-vector".b, 18 | 4 => 1, 19 | 3 => 0, 20 | 2 => "id".b, 21 | 1 => 4, 22 | -1 => "k".b 23 | ) 24 | ) 25 | 26 | expect(key.base_iv).to eq("init-vector".b) 27 | expect(key.key_ops).to eq(1) 28 | expect(key.alg).to eq(0) 29 | expect(key.kid).to eq("id".b) 30 | expect(key.k).to eq("k".b) 31 | end 32 | 33 | it "returns an error if key type is wrong" do 34 | expect { 35 | COSE::Key::Symmetric.deserialize( 36 | CBOR.encode( 37 | 1 => 2, 38 | -1 => "k" 39 | ) 40 | ) 41 | }.to raise_error("Not a Symmetric key") 42 | end 43 | end 44 | 45 | context "#serialize" do 46 | it "works" do 47 | key = COSE::Key::Symmetric.new( 48 | kid: "id".b, 49 | alg: 0, 50 | key_ops: 1, 51 | base_iv: "init-vector".b, 52 | k: "key".b 53 | ) 54 | 55 | serialized_key = key.serialize 56 | 57 | map = CBOR.decode(serialized_key) 58 | 59 | expect(map[5]).to eq("init-vector".b) 60 | expect(map[4]).to eq(1) 61 | expect(map[3]).to eq(0) 62 | expect(map[2]).to eq("id".b) 63 | expect(map[1]).to eq(4) 64 | expect(map[-1]).to eq("key".b) 65 | end 66 | 67 | it "does not include labels without value" do 68 | key = COSE::Key::Symmetric.new(k: "k".b) 69 | 70 | serialized_key = key.serialize 71 | 72 | map = CBOR.decode(serialized_key) 73 | 74 | expect(map.keys.size).to eq(2) 75 | expect(map[1]).to eq(4) 76 | expect(map[-1]).to eq("k".b) 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/cose/key_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cbor" 4 | require "cose/key" 5 | require "openssl" 6 | 7 | RSpec.describe COSE::Key do 8 | describe ".serialize" do 9 | it "can serialize EC P-256 key" do 10 | key = OpenSSL::PKey::EC.generate("prime256v1") 11 | 12 | cbor = COSE::Key.serialize(key) 13 | map = CBOR.decode(cbor) 14 | 15 | expect(map[1]).to eq(2) 16 | expect(map[-1]).to eq(1) 17 | public_key_bytes = key.public_key.to_bn.to_s(2)[1..-1] 18 | expect(map[-2]).to eq(public_key_bytes[0..31]) 19 | expect(map[-3]).to eq(public_key_bytes[32..-1]) 20 | expect(map[-4]).to eq(key.private_key.to_s(2)) 21 | end 22 | 23 | it "can serialize EC P-384 key" do 24 | key = OpenSSL::PKey::EC.generate("secp384r1") 25 | 26 | cbor = COSE::Key.serialize(key) 27 | map = CBOR.decode(cbor) 28 | 29 | expect(map[1]).to eq(2) 30 | expect(map[-1]).to eq(2) 31 | public_key_bytes = key.public_key.to_bn.to_s(2)[1..-1] 32 | expect(map[-2]).to eq(public_key_bytes[0..47]) 33 | expect(map[-3]).to eq(public_key_bytes[48..-1]) 34 | expect(map[-4]).to eq(key.private_key.to_s(2)) 35 | end 36 | 37 | it "can serialize EC P-521 key" do 38 | key = OpenSSL::PKey::EC.generate("secp521r1") 39 | 40 | cbor = COSE::Key.serialize(key) 41 | map = CBOR.decode(cbor) 42 | 43 | expect(map[1]).to eq(2) 44 | expect(map[-1]).to eq(3) 45 | public_key_bytes = key.public_key.to_bn.to_s(2)[1..-1] 46 | expect(map[-2]).to eq(public_key_bytes[0..65]) 47 | expect(map[-3]).to eq(public_key_bytes[66..-1]) 48 | expect(map[-4]).to eq(key.private_key.to_s(2)) 49 | end 50 | 51 | it "can serialize RSA key" do 52 | key = OpenSSL::PKey::RSA.new(2048) 53 | 54 | cbor = COSE::Key.serialize(key) 55 | map = CBOR.decode(cbor) 56 | 57 | expect(map[1]).to eq(3) 58 | expect(map[-1]).to eq(key.params["n"].to_s(2)) 59 | expect(map[-2]).to eq(key.params["e"].to_s(2)) 60 | expect(map[-3]).to eq(key.params["d"].to_s(2)) 61 | expect(map[-4]).to eq(key.params["p"].to_s(2)) 62 | expect(map[-5]).to eq(key.params["q"].to_s(2)) 63 | expect(map[-6]).to eq(key.params["dmp1"].to_s(2)) 64 | expect(map[-7]).to eq(key.params["dmq1"].to_s(2)) 65 | expect(map[-8]).to eq(key.params["iqmp"].to_s(2)) 66 | end 67 | 68 | it "can serialize RSA public key" do 69 | key = OpenSSL::PKey::RSA.new(2048).public_key 70 | 71 | cbor = COSE::Key.serialize(key) 72 | map = CBOR.decode(cbor) 73 | 74 | expect(map[1]).to eq(3) 75 | expect(map[-1]).to eq(key.params["n"].to_s(2)) 76 | expect(map[-2]).to eq(key.params["e"].to_s(2)) 77 | end 78 | end 79 | 80 | describe ".deserialize" do 81 | it "returns error if unknown format" do 82 | expect { 83 | COSE::Key.deserialize( 84 | CBOR.encode( 85 | 1 => 100, 86 | -1 => "a", 87 | -2 => "b" 88 | ) 89 | ) 90 | }.to raise_error(COSE::UnknownKeyType, "Unsupported or unknown key type 100") 91 | end 92 | 93 | it "returns error if missing kty" do 94 | expect { 95 | COSE::Key.deserialize( 96 | CBOR.encode( 97 | -1 => "a", 98 | -2 => "b" 99 | ) 100 | ) 101 | }.to raise_error(COSE::UnknownKeyType, "Missing required key type kty label") 102 | end 103 | 104 | it "deserializes OKP" do 105 | key = COSE::Key.deserialize( 106 | CBOR.encode( 107 | 1 => 1, 108 | -1 => 4, 109 | -2 => "x".b, 110 | -4 => "d".b 111 | ) 112 | ) 113 | 114 | expect(key).to be_a(COSE::Key::OKP) 115 | end 116 | 117 | it "deserializes EC2" do 118 | key = COSE::Key.deserialize( 119 | CBOR.encode( 120 | 1 => 2, 121 | -1 => 1, 122 | -2 => "x", 123 | -3 => "y", 124 | -4 => "d", 125 | ) 126 | ) 127 | 128 | expect(key).to be_a(COSE::Key::EC2) 129 | end 130 | 131 | it "deserializes RSA" do 132 | key = COSE::Key.deserialize( 133 | CBOR.encode( 134 | 1 => 3, 135 | -1 => "n", 136 | -2 => "e" 137 | ) 138 | ) 139 | 140 | expect(key).to be_a(COSE::Key::RSA) 141 | end 142 | 143 | it "deserializes Symmetric" do 144 | key = COSE::Key.deserialize( 145 | CBOR.encode( 146 | 1 => 4, 147 | -1 => "k" 148 | ) 149 | ) 150 | 151 | expect(key).to be_a(COSE::Key::Symmetric) 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /spec/cose/mac0_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "base64" 4 | require "cbor" 5 | require "cose/key" 6 | require "cose/mac0" 7 | 8 | RSpec.describe "COSE::Mac0" do 9 | context ".deserialize" do 10 | before do 11 | cbor = create_security_message({ 1 => 15 }, {}, "This is the content".b, "tag".b, cbor_tag: 17) 12 | 13 | @mac0 = COSE::Mac0.deserialize(cbor) 14 | end 15 | 16 | it "returns protected headers" do 17 | expect(@mac0.protected_headers).to eq(1 => 15) 18 | end 19 | 20 | it "returns unprotected headers" do 21 | expect(@mac0.unprotected_headers).to eq({}) 22 | end 23 | 24 | it "returns payload" do 25 | expect(@mac0.payload).to eq("This is the content".b) 26 | end 27 | 28 | it "returns the signature" do 29 | expect(@mac0.tag).to eq("tag".b) 30 | end 31 | end 32 | 33 | context "#verify" do 34 | wg_examples("mac0-tests/*.json") do |example| 35 | it "passes #{example['title']}" do 36 | mac0_data = example["input"]["mac0"] 37 | key_data = mac0_data["recipients"][0]["key"] 38 | 39 | external_aad = hex_to_bytes(mac0_data["external"]) 40 | key = COSE::Key::Symmetric.new(k: Base64.urlsafe_decode64(key_data["k"])) 41 | cbor = hex_to_bytes(example["output"]["cbor"]) 42 | 43 | if example["fail"] 44 | expect { COSE::Mac0.deserialize(cbor).verify(key, external_aad) }.to raise_error(COSE::Error) 45 | else 46 | expect(COSE::Mac0.deserialize(cbor).verify(key, external_aad)).to be_truthy 47 | end 48 | end 49 | end 50 | 51 | wg_examples("hmac-examples/HMac-enc-*.json") do |example| 52 | it "passes #{example['title']}" do 53 | mac0_data = example["input"]["mac0"] 54 | key_data = mac0_data["recipients"][0]["key"] 55 | 56 | external_aad = hex_to_bytes(mac0_data["external"]) 57 | key = COSE::Key::Symmetric.new(k: Base64.urlsafe_decode64(key_data["k"])) 58 | cbor = hex_to_bytes(example["output"]["cbor"]) 59 | 60 | if example["fail"] 61 | expect { COSE::Mac0.deserialize(cbor).verify(key, external_aad) }.to raise_error(COSE::Error) 62 | else 63 | expect(COSE::Mac0.deserialize(cbor).verify(key, external_aad)).to be_truthy 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/cose/mac_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "base64" 4 | require "cbor" 5 | require "cose/error" 6 | require "cose/key" 7 | require "cose/mac" 8 | require "cose/recipient" 9 | 10 | RSpec.describe "COSE::Mac" do 11 | context ".deserialize" do 12 | before do 13 | cbor = create_security_message( 14 | { 1 => 2 }, 15 | { 3 => 4 }, 16 | "This is the content".b, 17 | "tag".b, 18 | [ 19 | [CBOR.encode({ 5 => 6 }), { 6 => 7 }, "ciphertextA".b], 20 | [CBOR.encode({ 8 => 9 }), { 10 => 11 }, "ciphertextB".b] 21 | ], 22 | cbor_tag: 97 23 | ) 24 | 25 | @mac = COSE::Mac.deserialize(cbor) 26 | end 27 | 28 | it "returns protected headers" do 29 | expect(@mac.protected_headers).to eq(1 => 2) 30 | end 31 | 32 | it "returns unprotected headers" do 33 | expect(@mac.unprotected_headers).to eq(3 => 4) 34 | end 35 | 36 | it "returns the payload" do 37 | expect(@mac.payload).to eq("This is the content".b) 38 | end 39 | 40 | it "returns the tag" do 41 | expect(@mac.tag).to eq("tag") 42 | end 43 | 44 | it "returns the recipients" do 45 | expect(@mac.recipients.size).to eq(2) 46 | expect(@mac.recipients.all? { |s| s.is_a?(COSE::Recipient) }).to be_truthy 47 | end 48 | end 49 | 50 | context "#verify" do 51 | wg_examples("mac-tests/*.json") do |example| 52 | it "passes #{example['title']}" do 53 | mac_data = example["input"]["mac"] 54 | key_data = mac_data["recipients"][0]["key"] 55 | 56 | key = COSE::Key::Symmetric.new( 57 | kid: key_data["kid"], 58 | k: Base64.urlsafe_decode64(key_data["k"]) 59 | ) 60 | 61 | external_aad = hex_to_bytes(mac_data["external"]) 62 | cbor = hex_to_bytes(example["output"]["cbor"]) 63 | 64 | if example["fail"] 65 | expect { COSE::Mac.deserialize(cbor).verify(key, external_aad) }.to raise_error(COSE::Error) 66 | else 67 | expect(COSE::Mac.deserialize(cbor).verify(key, external_aad)).to be_truthy 68 | end 69 | end 70 | end 71 | 72 | wg_examples("hmac-examples/HMac-0*.json") do |example| 73 | it "passes #{example['title']}" do 74 | mac_data = example["input"]["mac"] 75 | key_data = mac_data["recipients"][0]["key"] 76 | 77 | key = COSE::Key::Symmetric.new( 78 | kid: key_data["kid"], 79 | k: Base64.urlsafe_decode64(key_data["k"]) 80 | ) 81 | 82 | external_aad = hex_to_bytes(mac_data["external"]) 83 | cbor = hex_to_bytes(example["output"]["cbor"]) 84 | 85 | if example["fail"] 86 | expect { COSE::Mac.deserialize(cbor).verify(key, external_aad) }.to raise_error(COSE::Error) 87 | else 88 | expect(COSE::Mac.deserialize(cbor).verify(key, external_aad)).to be_truthy 89 | end 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/cose/recipient_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/recipient" 4 | 5 | RSpec.describe "COSE::Recipient" do 6 | context ".deserialize" do 7 | before do 8 | cbor = create_security_message( 9 | { 1 => 2 }, 10 | { 3 => 4 }, 11 | "ciphertext".b, 12 | [ 13 | create_security_message({ 5 => 6 }, { 6 => 7 }, "ciphertextA".b), 14 | create_security_message({ 8 => 9 }, { 10 => 11 }, "ciphertextB".b) 15 | ] 16 | ) 17 | 18 | @recipient = COSE::Recipient.deserialize(cbor) 19 | end 20 | 21 | it "returns protected headers" do 22 | expect(@recipient.protected_headers).to eq(1 => 2) 23 | end 24 | 25 | it "returns unprotected headers" do 26 | expect(@recipient.unprotected_headers).to eq(3 => 4) 27 | end 28 | 29 | it "returns the ciphertext" do 30 | expect(@recipient.ciphertext).to eq("ciphertext".b) 31 | end 32 | 33 | it "returns the recipients" do 34 | expect(@recipient.recipients.size).to eq(2) 35 | expect(@recipient.recipients.all? { |s| s.is_a?(COSE::Recipient) }).to be_truthy 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/cose/sign1_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "base64" 4 | require "cbor" 5 | require "cose/algorithm" 6 | require "cose/error" 7 | require "cose/key" 8 | require "cose/sign1" 9 | 10 | RSpec.describe "COSE::Sign1" do 11 | context ".deserialize" do 12 | before do 13 | cbor = create_security_message({ 1 => -7 }, { 4 => "11" }, "This is the content".b, "signature".b, cbor_tag: 18) 14 | 15 | @sign1 = COSE::Sign1.deserialize(cbor) 16 | end 17 | 18 | it "returns protected headers" do 19 | expect(@sign1.protected_headers).to eq(1 => -7) 20 | end 21 | 22 | it "returns unprotected headers" do 23 | expect(@sign1.unprotected_headers).to eq(4 => "11") 24 | end 25 | 26 | it "returns payload" do 27 | expect(@sign1.payload).to eq("This is the content".b) 28 | end 29 | 30 | it "returns the signature" do 31 | expect(@sign1.signature).to eq("signature".b) 32 | end 33 | end 34 | 35 | context "#verify" do 36 | wg_examples("sign1-tests/*.json") do |example| 37 | it "passes #{example['title']}" do 38 | key_data = example["input"]["sign0"]["key"] 39 | 40 | key = COSE::Key::EC2.new( 41 | kid: key_data["kid"], 42 | alg: COSE::Algorithm.by_name(example["input"]["sign0"]["alg"]).id, 43 | crv: COSE::Key::Curve.by_name(key_data["crv"]).id, 44 | x: Base64.urlsafe_decode64(key_data["x"]), 45 | y: Base64.urlsafe_decode64(key_data["y"]) 46 | ) 47 | 48 | external_aad = hex_to_bytes(example["input"]["sign0"]["external"]) 49 | cbor = hex_to_bytes(example["output"]["cbor"]) 50 | 51 | if example["fail"] 52 | expect { COSE::Sign1.deserialize(cbor).verify(key, external_aad) }.to raise_error(COSE::Error) 53 | else 54 | expect(COSE::Sign1.deserialize(cbor).verify(key, external_aad)).to be_truthy 55 | end 56 | end 57 | end 58 | 59 | # TODO: Test against ecdsa-examples/ecdsa-sig-04.json when we support implicit curve 60 | wg_examples("ecdsa-examples/ecdsa-sig-0{1,2,3}.json") do |example| 61 | it "passes #{example['title']}" do 62 | key_data = example["input"]["sign0"]["key"] 63 | 64 | key = COSE::Key::EC2.new( 65 | kid: key_data["kid"], 66 | alg: COSE::Algorithm.by_name(example["input"]["sign0"]["alg"]).id, 67 | crv: COSE::Key::Curve.by_name(key_data["crv"]).id, 68 | x: Base64.urlsafe_decode64(key_data["x"]), 69 | y: Base64.urlsafe_decode64(key_data["y"]) 70 | ) 71 | 72 | cbor = hex_to_bytes(example["output"]["cbor"]) 73 | 74 | if example["fail"] 75 | expect { COSE::Sign1.deserialize(cbor).verify(key) }.to raise_error(COSE::Error) 76 | else 77 | expect(COSE::Sign1.deserialize(cbor).verify(key)).to be_truthy 78 | end 79 | end 80 | end 81 | 82 | if curve_25519_supported? 83 | wg_examples("eddsa-examples/eddsa-sig-*.json") do |example| 84 | it "passes #{example['title']}" do 85 | key_data = example["input"]["sign0"]["key"] 86 | 87 | key = COSE::Key::OKP.new( 88 | kid: key_data["kid"], 89 | alg: COSE::Algorithm.by_name(example["input"]["sign0"]["alg"]).id, 90 | crv: COSE::Key::Curve.by_name(key_data["crv"]).id, 91 | x: hex_to_bytes(key_data["x_hex"]), 92 | d: hex_to_bytes(key_data["d_hex"]) 93 | ) 94 | 95 | cbor = hex_to_bytes(example["output"]["cbor"]) 96 | 97 | if example["fail"] 98 | expect { COSE::Sign1.deserialize(cbor).verify(key) }.to raise_error(COSE::Error) 99 | else 100 | expect(COSE::Sign1.deserialize(cbor).verify(key)).to be_truthy 101 | end 102 | end 103 | end 104 | end 105 | end 106 | end 107 | -------------------------------------------------------------------------------- /spec/cose/sign_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "base64" 4 | require "cbor" 5 | require "cose/key" 6 | require "cose/sign" 7 | require "cose/signature" 8 | 9 | RSpec.describe "COSE::Sign" do 10 | context ".deserialize" do 11 | before do 12 | cbor = create_security_message( 13 | { 1 => 2 }, 14 | { 3 => 4 }, 15 | "This is the content".b, 16 | [ 17 | [CBOR.encode({ 5 => 6 }), { 7 => 8 }, "signatureA".b], 18 | [CBOR.encode({ 9 => 10 }), { 11 => 12 }, "signatureB".b] 19 | ], 20 | cbor_tag: 98 21 | ) 22 | 23 | @sign = COSE::Sign.deserialize(cbor) 24 | end 25 | 26 | it "returns protected headers" do 27 | expect(@sign.protected_headers).to eq(1 => 2) 28 | end 29 | 30 | it "returns unprotected headers" do 31 | expect(@sign.unprotected_headers).to eq(3 => 4) 32 | end 33 | 34 | it "returns payload" do 35 | expect(@sign.payload).to eq("This is the content".b) 36 | end 37 | 38 | it "returns the signatures" do 39 | expect(@sign.signatures.size).to eq(2) 40 | expect(@sign.signatures.all? { |s| s.is_a?(COSE::Signature) }).to be_truthy 41 | end 42 | end 43 | 44 | context "#verify" do 45 | wg_examples("sign-tests/*.json") do |example| 46 | it "passes #{example['title']}" do 47 | signer_data = example["input"]["sign"]["signers"][0] 48 | key_data = signer_data["key"] 49 | 50 | key = COSE::Key::EC2.new( 51 | kid: key_data["kid"], 52 | crv: COSE::Key::Curve.by_name(key_data["crv"]).id, 53 | x: Base64.urlsafe_decode64(key_data["x"]), 54 | y: Base64.urlsafe_decode64(key_data["y"]) 55 | ) 56 | 57 | external_aad = hex_to_bytes(signer_data["external"]) 58 | cbor = hex_to_bytes(example["output"]["cbor"]) 59 | 60 | if example["fail"] 61 | expect { COSE::Sign.deserialize(cbor).verify(key, external_aad) }.to raise_error(COSE::Error) 62 | else 63 | expect(COSE::Sign.deserialize(cbor).verify(key, external_aad)).to be_truthy 64 | end 65 | end 66 | end 67 | 68 | # TODO: Test against ecdsa-examples/ecdsa-04.json when we support implicit curve 69 | wg_examples("ecdsa-examples/ecdsa-0{1,2,3}.json") do |example| 70 | it "passes #{example['title']}" do 71 | key_data = example["input"]["sign"]["signers"][0]["key"] 72 | 73 | key = COSE::Key::EC2.new( 74 | kid: key_data["kid"], 75 | crv: COSE::Key::Curve.by_name(key_data["crv"]).id, 76 | x: Base64.urlsafe_decode64(key_data["x"]), 77 | y: Base64.urlsafe_decode64(key_data["y"]) 78 | ) 79 | 80 | cbor = hex_to_bytes(example["output"]["cbor"]) 81 | 82 | if example["fail"] 83 | expect { COSE::Sign.deserialize(cbor).verify(key) }.to raise_error(COSE::Error) 84 | else 85 | expect(COSE::Sign.deserialize(cbor).verify(key)).to be_truthy 86 | end 87 | end 88 | end 89 | 90 | if curve_25519_supported? 91 | wg_examples("eddsa-examples/eddsa-0*.json") do |example| 92 | it "passes #{example['title']}" do 93 | key_data = example["input"]["sign"]["signers"][0]["key"] 94 | 95 | key = COSE::Key::OKP.new( 96 | kid: key_data["kid"], 97 | crv: COSE::Key::Curve.by_name(key_data["crv"]).id, 98 | x: hex_to_bytes(key_data["x_hex"]), 99 | d: hex_to_bytes(key_data["d_hex"]) 100 | ) 101 | 102 | cbor = hex_to_bytes(example["output"]["cbor"]) 103 | 104 | if example["fail"] 105 | expect { COSE::Sign.deserialize(cbor).verify(key) }.to raise_error(COSE::Error) 106 | else 107 | expect(COSE::Sign.deserialize(cbor).verify(key)).to be_truthy 108 | end 109 | end 110 | end 111 | end 112 | 113 | if rsa_pss_supported? 114 | wg_examples("rsa-pss-examples/*.json") do |example| 115 | it "passes #{example['title']}" do 116 | key_data = example["input"]["sign"]["signers"][0]["key"] 117 | 118 | key = COSE::Key::RSA.new( 119 | kid: key_data["kid"], 120 | n: hex_to_bytes(key_data["n_hex"]), 121 | e: hex_to_bytes(key_data["e_hex"]) 122 | ) 123 | 124 | cbor = hex_to_bytes(example["output"]["cbor"]) 125 | 126 | if example["fail"] 127 | expect { COSE::Sign.deserialize(cbor).verify(key) }.to raise_error(COSE::Error) 128 | else 129 | expect(COSE::Sign.deserialize(cbor).verify(key)).to be_truthy 130 | end 131 | end 132 | end 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /spec/cose/signature_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/signature" 4 | 5 | RSpec.describe "COSE::Signature" do 6 | context ".deserialize" do 7 | before do 8 | cbor = create_security_message({ 1 => 2 }, { 3 => 4 }, "signature".b) 9 | 10 | @signature = COSE::Signature.deserialize(cbor) 11 | end 12 | 13 | it "returns protected headers" do 14 | expect(@signature.protected_headers).to eq(1 => 2) 15 | end 16 | 17 | it "returns unprotected headers" do 18 | expect(@signature.unprotected_headers).to eq(3 => 4) 19 | end 20 | 21 | it "returns the signature" do 22 | expect(@signature.signature).to eq("signature".b) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/cose_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cose/version" 4 | 5 | RSpec.describe COSE do 6 | it "has a version number" do 7 | expect(COSE::VERSION).not_to be nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "cbor" 4 | require "byebug" 5 | require "json" 6 | 7 | RSpec.configure do |config| 8 | # Enable flags like --only-failures and --next-failure 9 | config.example_status_persistence_file_path = ".rspec_status" 10 | 11 | # Disable RSpec exposing methods globally on `Module` and `main` 12 | config.disable_monkey_patching! 13 | 14 | config.expect_with :rspec do |c| 15 | c.syntax = :expect 16 | end 17 | end 18 | 19 | def create_security_message(protected_headers, unprotected_headers, *args, cbor_tag: 0) 20 | CBOR::Tagged.new(cbor_tag, [CBOR.encode(protected_headers), unprotected_headers, *args]).to_cbor 21 | end 22 | 23 | def wg_examples(relative_glob) 24 | Dir.glob(File.expand_path("fixtures/cose-wg-examples/#{relative_glob}", __dir__)) do |file_name| 25 | yield JSON.parse(File.read(file_name)) 26 | end 27 | end 28 | 29 | def rsa_pss_supported? 30 | OpenSSL::PKey::RSA.instance_methods.include?(:verify_pss) 31 | end 32 | 33 | def curve_25519_supported? 34 | OpenSSL::OPENSSL_VERSION_NUMBER >= 0x10101000 && # >= v1.1.1 35 | defined?(OpenSSL::PKey.generate_key) 36 | end 37 | 38 | def hex_to_bytes(hex_string) 39 | [hex_string].pack("H*") 40 | end 41 | --------------------------------------------------------------------------------