├── .codecov.yml ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .mdl-style.rb ├── .mdlrc ├── .rustfmt.toml ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── deny.toml ├── dependabot.yml ├── examples ├── cwt.rs └── signature.rs ├── fuzz ├── .gitignore ├── Cargo.lock ├── Cargo.toml └── fuzz_targets │ ├── encrypt.rs │ ├── key.rs │ ├── mac.rs │ └── sign.rs ├── scripts ├── build-gh-pages.sh └── check-format.sh └── src ├── common ├── mod.rs └── tests.rs ├── context ├── mod.rs └── tests.rs ├── cwt ├── mod.rs └── tests.rs ├── encrypt ├── mod.rs └── tests.rs ├── header ├── mod.rs └── tests.rs ├── iana ├── mod.rs └── tests.rs ├── key ├── mod.rs └── tests.rs ├── lib.rs ├── mac ├── mod.rs └── tests.rs ├── sign ├── mod.rs └── tests.rs └── util ├── mod.rs └── tests.rs /.codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: yes 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "70...100" 8 | 9 | parsers: 10 | gcov: 11 | branch_detection: 12 | conditional: yes 13 | loop: yes 14 | method: no 15 | macro: no 16 | 17 | comment: false 18 | 19 | ignore: 20 | - "**/build.rs" 21 | - "**/benches/" 22 | - "**/tests/" 23 | - "**/codegen/" 24 | - "**/*_test.rs" 25 | - "**/*tests.rs" 26 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "daily" 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | permissions: 3 | contents: read 4 | on: 5 | pull_request: 6 | paths-ignore: 7 | - README.md 8 | push: 9 | branches: main 10 | paths-ignore: 11 | - README.md 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | rust: 19 | - stable 20 | - beta 21 | - nightly-2023-04-01 22 | steps: 23 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 24 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 25 | with: 26 | toolchain: ${{ matrix.rust }} 27 | components: rustfmt 28 | - run: cargo +${{ matrix.rust }} build --release --workspace 29 | - run: cargo +${{ matrix.rust }} build --release --workspace --features=std 30 | 31 | test: 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | rust: 36 | - stable 37 | - beta 38 | - nightly-2023-04-01 39 | steps: 40 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 41 | with: 42 | submodules: true 43 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 44 | with: 45 | toolchain: ${{ matrix.rust }} 46 | components: rustfmt 47 | - run: cargo +${{ matrix.rust }} test --workspace -- --nocapture 48 | - run: cargo +${{ matrix.rust }} test --workspace --features=std -- --nocapture 49 | 50 | examples: 51 | runs-on: ubuntu-latest 52 | strategy: 53 | matrix: 54 | rust: 55 | - stable 56 | - beta 57 | - nightly-2023-04-01 58 | steps: 59 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 60 | with: 61 | submodules: true 62 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 63 | with: 64 | toolchain: ${{ matrix.rust }} 65 | components: rustfmt 66 | - run: | 67 | for eg in `ls examples/*.rs | xargs basename --suffix=.rs`; do 68 | cargo +${{ matrix.rust }} run --example ${eg}; 69 | cargo +${{ matrix.rust }} run --features=std --example ${eg}; 70 | done 71 | 72 | no_std: 73 | name: Build for a no_std target 74 | runs-on: ubuntu-latest 75 | strategy: 76 | matrix: 77 | rust: 78 | - stable 79 | - beta 80 | - nightly-2023-04-01 81 | steps: 82 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 83 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 84 | with: 85 | toolchain: ${{ matrix.rust }} 86 | components: rustfmt 87 | targets: thumbv6m-none-eabi 88 | - run: cargo +${{ matrix.rust }} build --release --workspace --target thumbv6m-none-eabi 89 | 90 | msrv: 91 | name: Rust ${{matrix.rust}} MSRV 92 | runs-on: ubuntu-latest 93 | strategy: 94 | fail-fast: false 95 | matrix: 96 | rust: [1.58.0, 1.59.0] 97 | steps: 98 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 99 | with: 100 | submodules: true 101 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 102 | with: 103 | toolchain: ${{ matrix.rust }} 104 | components: rustfmt 105 | - run: rustc --version 106 | - run: cargo +${{ matrix.rust }} build --release --workspace 107 | - run: cargo +${{ matrix.rust }} build --release --workspace --all-features 108 | 109 | formatting: 110 | runs-on: ubuntu-latest 111 | steps: 112 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 113 | - uses: actions/setup-go@6edd4406fa81c3da01a34fa6f6343087c207a568 # v3.5.0 114 | - run: go install github.com/campoy/embedmd@97c13d6 115 | - uses: ruby/setup-ruby@c04af2bb7258bb6a03df1d3c1865998ac9390972 # v1.194.0 116 | with: 117 | ruby-version: '2.7' 118 | bundler-cache: true 119 | - run: gem install mdl 120 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 121 | with: 122 | toolchain: nightly-2023-04-01 123 | components: rustfmt 124 | - run: cargo +nightly-2023-04-01 fmt --all -- --check 125 | - run: scripts/check-format.sh 126 | 127 | clippy: 128 | runs-on: ubuntu-latest 129 | steps: 130 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 131 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 132 | with: 133 | toolchain: stable 134 | components: rustfmt, clippy 135 | - run: cargo +stable clippy --all-features --all-targets -- -Dwarnings 136 | - run: git diff --exit-code 137 | 138 | doc: 139 | runs-on: ubuntu-latest 140 | steps: 141 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 142 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 143 | with: 144 | toolchain: stable 145 | - run: RUSTDOCFLAGS="-Dwarnings" cargo doc --no-deps --document-private-items 146 | 147 | udeps: 148 | runs-on: ubuntu-latest 149 | steps: 150 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 151 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 152 | with: 153 | toolchain: nightly-2023-04-01 154 | components: rustfmt 155 | - run: cargo +nightly-2023-04-01 install --locked --version 0.1.39 cargo-udeps 156 | - run: cargo +nightly-2023-04-01 udeps 157 | 158 | deny: 159 | runs-on: ubuntu-latest 160 | steps: 161 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 162 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 163 | with: 164 | toolchain: nightly-2023-04-01 165 | components: rustfmt 166 | - run: cargo +nightly-2023-04-01 install --locked --version 0.13.9 cargo-deny 167 | - run: cargo deny check 168 | 169 | coverage: 170 | runs-on: ubuntu-latest 171 | steps: 172 | - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2 173 | with: 174 | submodules: true 175 | fetch-depth: 0 176 | - uses: dtolnay/rust-toolchain@a3ac054b2e7d62f514aa1bd57e3508c522fe772d # 1.68.2 177 | with: 178 | toolchain: stable 179 | components: rustfmt 180 | - uses: actions-rs/install@69ec87709ffb5b19a7b5ddbf610cb221498bb1eb # v0.1.2 181 | with: 182 | crate: cargo-tarpaulin 183 | version: 0.30.0 184 | use-tool-cache: true 185 | - run: cargo tarpaulin --verbose --ignore-tests --all-features --timeout=600 --out Xml 186 | - name: Upload to codecov.io 187 | run: | 188 | bash <(curl -s https://codecov.io/bash) 189 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /.mdl-style.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | all 15 | rule 'MD013', :line_length => 120 16 | rule 'MD007', :indent => 4 17 | exclude_rule 'MD031' # embedmd markers are next to fenced code blocks 18 | exclude_rule 'MD033' # allow inline HTML, esp. 19 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | style "#{File.dirname(__FILE__)}/.mdl-style.rb" -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | # See https://github.com/rust-lang/rustfmt/blob/master/Configurations.md 2 | comment_width = 100 3 | format_code_in_doc_comments = true 4 | max_width = 100 5 | normalize_doc_attributes = true 6 | wrap_comments = true 7 | imports_granularity = "Crate" 8 | imports_layout = "mixed" 9 | edition = "2018" 10 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of significant contributors. 2 | # 3 | # This does not necessarily list everyone who has contributed code, 4 | # especially since many employees of one corporation may be contributing. 5 | # To see the full list of contributors, see the revision history in 6 | # source control. 7 | Google LLC -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.3.8 - 2024-07-24 4 | 5 | - Make `CoseSign[1]::tbs_[detached_]data` methods `pub`. 6 | 7 | ## 0.3.7 - 2024-04-05 8 | 9 | - Bump MSRV to 1.58. 10 | - Update dependencies. 11 | - Fix bounds bug for label sorting. 12 | 13 | ## 0.3.6 - 2024-01-15 14 | 15 | - Helpers for ordering of fields in a `COSE_Key`: 16 | - Add `Label::cmp_canonical()` for RFC 7049 canonical ordering. 17 | - Add `CborOrdering` enum to specify ordering. 18 | - Add `CoseKey::canonicalize()` method to order fields. 19 | 20 | ## 0.3.5 - 2023-09-29 21 | 22 | - Add helper methods to create and verify detached signatures: 23 | - Add `CoseSignBuilder` methods `add_detached_signature` and `try_add_detached_signature`. 24 | - Add `CoseSign` method `verify_detached_signature`. 25 | - Add `CoseSign1Builder` methods `create_detached_signature` and `try_create_detached_signature`. 26 | - Add `CoseSign1` method `verify_detached_signature`. 27 | - Implement CBOR conversion traits for `ciborium::value::Value`. 28 | - Update `ciborium` dependency. 29 | 30 | ## 0.3.4 - 2023-01-25 31 | 32 | - Add non-default `std` feature that turns on `impl Error for CoseError`. 33 | - Add `cwt::ClaimsSetBuilder::private_claim` method. 34 | - Update documentation for existing encryption methods to make it clear that they only support AEAD encryption. 35 | 36 | ## 0.3.3 - 2022-09-30 37 | 38 | - Add `CoseKeyBuilder` methods `kty`, `key_type` and `new_okp_key`. 39 | 40 | ## 0.3.2 - 2022-04-02 41 | 42 | - Add basic [CWT](https://datatracker.ietf.org/doc/html/rfc8392) support in `cwt` module, via the `ClaimsSet` type. 43 | 44 | ## 0.3.1 - 2022-02-23 45 | 46 | - Implement `Display` for `CoseError`. 47 | - Fix `Cargo.toml` to indicate reliance on `alloc` feature of `ciborium-io`. 48 | - Make `AsCborValue` trait public. 49 | 50 | ## 0.3.0 - 2022-01-19 51 | 52 | - Change to use `ciborium` as CBOR library. Breaking change with many knock-on effects: 53 | - Re-export `ciborium` as `coset::cbor` (rather than `sk-cbor`). 54 | - Use `ciborium`'s `Value` type rather than `sk-cbor`'s version. 55 | - Change `CoseError` to no longer wrap `sk-cbor` errors. 56 | - Drop `derive` of `Eq` for data types (`ciborium` supports float values, which are inherently non-`Eq`) 57 | - Add `#[must_use]` attributes to builder methods. 58 | - Update MSRV to 1.56.0, as `ciborium` is `edition=2021` 59 | - Use new `ProtectedHeader` type for protected headers (breaking change). This variant of `Header` preserves any 60 | originally-parsed data, so that calculations (signatures, decryption, etc.) over the data can use the bit-for-bit wire 61 | data instead of a reconstituted (and potentially different) version. 62 | - Add more specific error cases to `CoseError` (breaking change): 63 | - Use new `OutOfRangeIntegerValue` error when an integer value is too large for the representation used in this 64 | crate. 65 | - Use new `DuplicateMapKey` error when a CBOR map contains duplicate keys (and is thus invalid). 66 | - Extend `DecodeFailed` error to include the underlying `ciborium::de::Error` value. 67 | - Use new `ExtraneousData` error when data remains after reading a CBOR value. 68 | - Rename `UnexpectedType` error to `UnexpectedItem` to reflect broader usage than type. 69 | - Add a crate-specific `Result` type whose `E` field defaults to `CoseError`. 70 | 71 | ## 0.2.0 - 2021-12-09 72 | 73 | - Change to use `sk-cbor` as CBOR library, due to deprecation of `serde-cbor`. Breaking change with many knock-on 74 | effects: 75 | - Re-export `sk-cbor` as `coset::cbor`. 76 | - Use `sk-cbor`'s `Value` type rather than `serde-cbor`'s version. 77 | - Change encoding methods to consume `self`. 78 | - Change encoding methods to be fallible. 79 | - Move to be `no_std` (but still using `alloc`) 80 | - Add `CoseError` error type and use throughout. 81 | - Use `Vec` of pairs not `BTreeMap`s for CBOR map values. 82 | - Use `i64` not `i128` for integer values throughout. 83 | - Drop use of `serde`'s `Serialize` and `Deserialize` traits; instead… 84 | - Add `CborSerializable` extension trait for conversion to/from bytes. 85 | - Drop `from_tagged_reader` / `to_tagged_writer` methods from `TaggedCborSerializable` trait. 86 | - Derive `Debug` for builders. 87 | - Convert `CoseKeySet` to a newtype, and add standard traits. 88 | 89 | ## 0.1.2 - 2021-08-24 90 | 91 | - Add fallible variants of builder methods that invoke closures (#20): 92 | - `CoseRecipientBuilder::try_create_ciphertext()` 93 | - `CoseEncryptBuilder::try_create_ciphertext()` 94 | - `CoseEncrypt0Builder::try_create_ciphertext()` 95 | - `CoseMacBuilder::try_create_tag()` 96 | - `CoseMac0Builder::try_create_tag()` 97 | - `CoseSignBuilder::try_add_created_signature()` 98 | - `CoseSign1Builder::try_create_signature()` 99 | - Upgrade dependencies. 100 | 101 | ## 0.1.1 - 2021-06-24 102 | 103 | - Make `KeyType` and `KeyOperation` public. 104 | - Upgrade dependencies. 105 | 106 | ## 0.1.0 - 2021-05-18 107 | 108 | - Initial version, using `serde-cbor` as CBOR library. 109 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google/conduct/). 29 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ciborium" 7 | version = "0.2.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" 10 | dependencies = [ 11 | "ciborium-io", 12 | "ciborium-ll", 13 | "serde", 14 | ] 15 | 16 | [[package]] 17 | name = "ciborium-io" 18 | version = "0.2.2" 19 | source = "registry+https://github.com/rust-lang/crates.io-index" 20 | checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" 21 | 22 | [[package]] 23 | name = "ciborium-ll" 24 | version = "0.2.2" 25 | source = "registry+https://github.com/rust-lang/crates.io-index" 26 | checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" 27 | dependencies = [ 28 | "ciborium-io", 29 | "half", 30 | ] 31 | 32 | [[package]] 33 | name = "coset" 34 | version = "0.3.8" 35 | dependencies = [ 36 | "ciborium", 37 | "ciborium-io", 38 | "hex", 39 | ] 40 | 41 | [[package]] 42 | name = "crunchy" 43 | version = "0.2.2" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" 46 | 47 | [[package]] 48 | name = "half" 49 | version = "2.2.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" 52 | dependencies = [ 53 | "crunchy", 54 | ] 55 | 56 | [[package]] 57 | name = "hex" 58 | version = "0.4.3" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 61 | 62 | [[package]] 63 | name = "proc-macro2" 64 | version = "1.0.34" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1" 67 | dependencies = [ 68 | "unicode-xid", 69 | ] 70 | 71 | [[package]] 72 | name = "quote" 73 | version = "1.0.10" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" 76 | dependencies = [ 77 | "proc-macro2", 78 | ] 79 | 80 | [[package]] 81 | name = "serde" 82 | version = "1.0.132" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008" 85 | dependencies = [ 86 | "serde_derive", 87 | ] 88 | 89 | [[package]] 90 | name = "serde_derive" 91 | version = "1.0.132" 92 | source = "registry+https://github.com/rust-lang/crates.io-index" 93 | checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276" 94 | dependencies = [ 95 | "proc-macro2", 96 | "quote", 97 | "syn", 98 | ] 99 | 100 | [[package]] 101 | name = "syn" 102 | version = "1.0.83" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "23a1dfb999630e338648c83e91c59a4e9fb7620f520c3194b6b89e276f2f1959" 105 | dependencies = [ 106 | "proc-macro2", 107 | "quote", 108 | "unicode-xid", 109 | ] 110 | 111 | [[package]] 112 | name = "unicode-xid" 113 | version = "0.2.2" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" 116 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "coset" 3 | version = "0.3.8" 4 | authors = ["David Drysdale ", "Paul Crowley "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "Set of types for supporting COSE" 8 | repository = "https://github.com/google/coset" 9 | keywords = ["cryptography", "cose"] 10 | categories = ["cryptography"] 11 | 12 | [features] 13 | default = [] 14 | # `std` feature enables an `Error` impl for `CoseError` 15 | std = [] 16 | 17 | [dependencies] 18 | ciborium = { version = "^0.2.1", default-features = false } 19 | ciborium-io = { version = "^0.2.0", features = ["alloc"] } 20 | 21 | [dev-dependencies] 22 | hex = "^0.4.2" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COSET 2 | 3 | [![Docs](https://img.shields.io/badge/docs-rust-brightgreen?style=for-the-badge)](https://docs.rs/coset) 4 | [![CI Status](https://img.shields.io/github/actions/workflow/status/google/coset/ci.yml?branch=main&color=blue&style=for-the-badge)](https://github.com/google/coset/actions?query=workflow%3ACI) 5 | [![codecov](https://img.shields.io/codecov/c/github/google/coset?style=for-the-badge)](https://codecov.io/gh/google/coset) 6 | 7 | This crate holds a set of Rust types for working with CBOR Object Signing and Encryption (COSE) objects, as defined in 8 | [RFC 8152](https://tools.ietf.org/html/rfc8152). It builds on the core [CBOR](https://tools.ietf.org/html/rfc7049) 9 | parsing functionality from the [`ciborium` crate](https://docs.rs/ciborium). 10 | 11 | See [crate docs](https://docs.rs/coset), or the [signature 12 | example](examples/signature.rs) for documentation on how to use the code. 13 | 14 | **This repo is under construction** and so details of the API and the code may change without warning. 15 | 16 | ## Features 17 | 18 | The `std` feature of the crate enables an implementation of `std::error::Error` for `CoseError`. 19 | 20 | ## `no_std` Support 21 | 22 | This crate supports `no_std` (when the `std` feature is not set, which is the default), but uses the `alloc` crate. 23 | 24 | ## Minimum Supported Rust Version 25 | 26 | MSRV is 1.58. 27 | 28 | ## Integer Ranges 29 | 30 | CBOR supports integers in the range: 31 | 32 | ```text 33 | [-18_446_744_073_709_551_616, -1] ∪ [0, 18_446_744_073_709_551_615] 34 | ``` 35 | 36 | which is [-264, -1] ∪ [0, 264 - 1]. 37 | 38 | This does not map onto a single Rust integer type, so different CBOR crates take different approaches. 39 | 40 | - The [`serde_cbor`](https://docs.rs/serde_cbor) crate uses a single `i128` integer type for all integer values, which 41 | means that all CBOR integer values can be expressed, but there are also `i128` values that cannot be encoded in CBOR. 42 | This also means that data size is larger. 43 | - The [`ciborium`](https://docs.rs/ciborium) also uses a single `i128` integer type internally, but wraps it in its own 44 | [`Integer`](https://docs.rs/ciborium/latest/ciborium/value/struct.Integer.html) type and only implements `TryFrom` 45 | (not `From`) for `i128` / `u128` conversions so that unrepresentable numbers can be rejected. 46 | - The [`sk-cbor`](https://docs.rs/sk-cbor) crate uses distinct types: 47 | - positive numbers as u64, covering [0, 264 - 1] 48 | - negative numbers as i64, covering [-263, -1] (which means that some theoretically-valid large negative 49 | values are not represented). 50 | 51 | This crate uses a single type to encompass both positive and negative values, but uses `i64` for that type to keep data 52 | sizes smaller. This means that: 53 | 54 | - positive numbers in `i64` cover [0, 263 - 1] 55 | - negative numbers in `i64` cover [-263, -1] 56 | 57 | and so there are large values – both positive and negative – which are not supported by this crate. 58 | 59 | ## Working on the Code 60 | 61 | Local coding conventions are enforced by the [continuous integration jobs](.github/workflows) and include: 62 | 63 | - Build cleanly and pass all tests. 64 | - Free of [Clippy](https://github.com/rust-lang/rust-clippy) warnings. 65 | - Formatted with `rustfmt` using the local [rustfmt.toml](.rustfmt.toml) settings. 66 | - Compliance with local conventions: 67 | - All `TODO` markers should be of form `TODO(#99)` and refer to an open GitHub issue. 68 | - Calls to functions that can panic (`panic!`, `unwrap`, `expect`) should have a comment on the same line in the 69 | form `// safe: reason` (or `/* safe: reason */`) to document the reason why panicking is acceptable. 70 | 71 | ## Disclaimer 72 | 73 | This is not an officially supported Google product. 74 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | # Configuration used for dependency checking with cargo-deny. 2 | # 3 | # For further details on all configuration options see: 4 | # https://embarkstudios.github.io/cargo-deny/checks/cfg.html 5 | targets = [ 6 | { triple = "x86_64-unknown-linux-gnu" }, 7 | { triple = "x86_64-apple-darwin" }, 8 | { triple = "x86_64-pc-windows-msvc" }, 9 | ] 10 | 11 | # Deny all advisories unless explicitly ignored. 12 | [advisories] 13 | vulnerability = "deny" 14 | unmaintained = "deny" 15 | yanked = "deny" 16 | notice = "deny" 17 | ignore = [] 18 | 19 | # Deny multiple versions unless explicitly skipped. 20 | [bans] 21 | multiple-versions = "deny" 22 | wildcards = "allow" 23 | 24 | ###################################### 25 | 26 | # List of allowed licenses. 27 | [licenses] 28 | allow = [ 29 | "Apache-2.0", 30 | "MIT", 31 | ] 32 | copyleft = "deny" 33 | -------------------------------------------------------------------------------- /dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "daily" 10 | -------------------------------------------------------------------------------- /examples/cwt.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! Example program demonstrating signed CWT processing. 18 | use coset::{cbor::value::Value, cwt, iana, CborSerializable, CoseError}; 19 | 20 | #[derive(Copy, Clone)] 21 | struct FakeSigner {} 22 | 23 | // Use a fake signer/verifier (to avoid pulling in lots of dependencies). 24 | impl FakeSigner { 25 | fn sign(&self, data: &[u8]) -> Vec { 26 | data.to_vec() 27 | } 28 | 29 | fn verify(&self, sig: &[u8], data: &[u8]) -> Result<(), String> { 30 | if sig != self.sign(data) { 31 | Err("failed to verify".to_owned()) 32 | } else { 33 | Ok(()) 34 | } 35 | } 36 | } 37 | 38 | fn main() -> Result<(), CoseError> { 39 | // Build a fake signer/verifier (to avoid pulling in lots of dependencies). 40 | let signer = FakeSigner {}; 41 | let verifier = signer; 42 | 43 | // Build a CWT ClaimsSet (cf. RFC 8392 A.3). 44 | let claims = cwt::ClaimsSetBuilder::new() 45 | .issuer("coap://as.example.com".to_string()) 46 | .subject("erikw".to_string()) 47 | .audience("coap://light.example.com".to_string()) 48 | .expiration_time(cwt::Timestamp::WholeSeconds(1444064944)) 49 | .not_before(cwt::Timestamp::WholeSeconds(1443944944)) 50 | .issued_at(cwt::Timestamp::WholeSeconds(1443944944)) 51 | .cwt_id(vec![0x0b, 0x71]) 52 | // Add additional standard claim. 53 | .claim( 54 | iana::CwtClaimName::Scope, 55 | Value::Text("email phone".to_string()), 56 | ) 57 | // Add additional private-use claim. 58 | .private_claim(-70_000, Value::Integer(42.into())) 59 | .build(); 60 | let aad = b""; 61 | 62 | // Build a `CoseSign1` object. 63 | let protected = coset::HeaderBuilder::new() 64 | .algorithm(iana::Algorithm::ES256) 65 | .build(); 66 | let unprotected = coset::HeaderBuilder::new() 67 | .key_id(b"AsymmetricECDSA256".to_vec()) 68 | .build(); 69 | let sign1 = coset::CoseSign1Builder::new() 70 | .protected(protected) 71 | .unprotected(unprotected) 72 | .payload(claims.clone().to_vec()?) 73 | .create_signature(aad, |pt| signer.sign(pt)) 74 | .build(); 75 | 76 | // Serialize to bytes. 77 | let sign1_data = sign1.to_vec()?; 78 | 79 | // At the receiving end, deserialize the bytes back to a `CoseSign1` object. 80 | let sign1 = coset::CoseSign1::from_slice(&sign1_data)?; 81 | 82 | // Real code would: 83 | // - Use the key ID to identify the relevant local key. 84 | // - Check that the key is of the same type as `sign1.protected.algorithm`. 85 | 86 | // Check the signature. 87 | let result = sign1.verify_signature(aad, |sig, data| verifier.verify(sig, data)); 88 | println!("Signature verified: {:?}.", result); 89 | assert!(result.is_ok()); 90 | 91 | // Now it's safe to parse the payload. 92 | let recovered_claims = cwt::ClaimsSet::from_slice(&sign1.payload.unwrap())?; 93 | 94 | assert_eq!(recovered_claims, claims); 95 | Ok(()) 96 | } 97 | -------------------------------------------------------------------------------- /examples/signature.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! Example program demonstrating signature creation. 18 | use coset::{iana, CborSerializable, CoseError}; 19 | 20 | #[derive(Copy, Clone)] 21 | struct FakeSigner {} 22 | 23 | // Use a fake signer/verifier (to avoid pulling in lots of dependencies). 24 | impl FakeSigner { 25 | fn sign(&self, data: &[u8]) -> Vec { 26 | data.to_vec() 27 | } 28 | 29 | fn verify(&self, sig: &[u8], data: &[u8]) -> Result<(), String> { 30 | if sig != self.sign(data) { 31 | Err("failed to verify".to_owned()) 32 | } else { 33 | Ok(()) 34 | } 35 | } 36 | } 37 | 38 | fn main() -> Result<(), CoseError> { 39 | // Build a fake signer/verifier (to avoid pulling in lots of dependencies). 40 | let signer = FakeSigner {}; 41 | let verifier = signer; 42 | 43 | // Inputs. 44 | let pt = b"This is the content"; 45 | let aad = b"this is additional data"; 46 | 47 | // Build a `CoseSign1` object. 48 | let protected = coset::HeaderBuilder::new() 49 | .algorithm(iana::Algorithm::ES256) 50 | .key_id(b"11".to_vec()) 51 | .build(); 52 | let sign1 = coset::CoseSign1Builder::new() 53 | .protected(protected) 54 | .payload(pt.to_vec()) 55 | .create_signature(aad, |pt| signer.sign(pt)) 56 | .build(); 57 | 58 | // Serialize to bytes. 59 | let sign1_data = sign1.to_vec()?; 60 | println!( 61 | "'{}' + '{}' => {}", 62 | String::from_utf8_lossy(pt), 63 | String::from_utf8_lossy(aad), 64 | hex::encode(&sign1_data) 65 | ); 66 | 67 | // At the receiving end, deserialize the bytes back to a `CoseSign1` object. 68 | let mut sign1 = coset::CoseSign1::from_slice(&sign1_data)?; 69 | 70 | // Check the signature, which needs to have the same `aad` provided. 71 | let result = sign1.verify_signature(aad, |sig, data| verifier.verify(sig, data)); 72 | println!("Signature verified: {:?}.", result); 73 | assert!(result.is_ok()); 74 | 75 | // Changing an unprotected header leaves the signature valid. 76 | sign1.unprotected.content_type = Some(coset::ContentType::Text("text/plain".to_owned())); 77 | assert!(sign1 78 | .verify_signature(aad, |sig, data| verifier.verify(sig, data)) 79 | .is_ok()); 80 | 81 | // Providing a different `aad` means the signature won't validate. 82 | assert!(sign1 83 | .verify_signature(b"not aad", |sig, data| verifier.verify(sig, data)) 84 | .is_err()); 85 | 86 | // Changing a protected header invalidates the signature. 87 | sign1.protected.header.content_type = Some(coset::ContentType::Text("text/plain".to_owned())); 88 | sign1.protected.original_data = None; 89 | assert!(sign1 90 | .verify_signature(aad, |sig, data| verifier.verify(sig, data)) 91 | .is_err()); 92 | Ok(()) 93 | } 94 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "arbitrary" 7 | version = "1.0.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "698b65a961a9d730fb45b6b0327e20207810c9f61ee421b082b27ba003f49e2b" 10 | 11 | [[package]] 12 | name = "cc" 13 | version = "1.0.67" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" 16 | 17 | [[package]] 18 | name = "coset" 19 | version = "0.2.0" 20 | dependencies = [ 21 | "sk-cbor", 22 | ] 23 | 24 | [[package]] 25 | name = "coset-fuzz" 26 | version = "0.0.0" 27 | dependencies = [ 28 | "coset", 29 | "libfuzzer-sys", 30 | ] 31 | 32 | [[package]] 33 | name = "libfuzzer-sys" 34 | version = "0.4.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "86c975d637bc2a2f99440932b731491fc34c7f785d239e38af3addd3c2fd0e46" 37 | dependencies = [ 38 | "arbitrary", 39 | "cc", 40 | ] 41 | 42 | [[package]] 43 | name = "sk-cbor" 44 | version = "0.1.2" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "7e94879a793aba6e65d691f345cfd172c4cc924a78259d5f9612a2cbfb78847a" 47 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "coset-fuzz" 4 | version = "0.0.0" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.coset] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "encrypt" 24 | path = "fuzz_targets/encrypt.rs" 25 | test = false 26 | doc = false 27 | 28 | [[bin]] 29 | name = "key" 30 | path = "fuzz_targets/key.rs" 31 | test = false 32 | doc = false 33 | 34 | [[bin]] 35 | name = "mac" 36 | path = "fuzz_targets/mac.rs" 37 | test = false 38 | doc = false 39 | 40 | [[bin]] 41 | name = "sign" 42 | path = "fuzz_targets/sign.rs" 43 | test = false 44 | doc = false 45 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/encrypt.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! Fuzz COSE_Encrypt* parsing. 18 | 19 | #![no_main] 20 | use libfuzzer_sys::fuzz_target; 21 | 22 | use coset::CborSerializable; 23 | 24 | fuzz_target!(|data: &[u8]| { 25 | let _recipient = coset::CoseRecipient::from_slice(data); 26 | let _encrypt = coset::CoseEncrypt::from_slice(data); 27 | let _encrypt0 = coset::CoseEncrypt0::from_slice(data); 28 | }); 29 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/key.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! Fuzz COSE_Key parsing. 18 | 19 | #![no_main] 20 | use libfuzzer_sys::fuzz_target; 21 | 22 | use coset::CborSerializable; 23 | 24 | fuzz_target!(|data: &[u8]| { 25 | let _key = coset::CoseKey::from_slice(data); 26 | }); 27 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/mac.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! Fuzz COSE_Mac* parsing. 18 | 19 | #![no_main] 20 | use libfuzzer_sys::fuzz_target; 21 | 22 | use coset::CborSerializable; 23 | 24 | fuzz_target!(|data: &[u8]| { 25 | let _mac = coset::CoseMac::from_slice(data); 26 | let _mac0 = coset::CoseMac0::from_slice(data); 27 | }); 28 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/sign.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! Fuzz COSE_Sign* parsing. 18 | 19 | #![no_main] 20 | use libfuzzer_sys::fuzz_target; 21 | 22 | use coset::CborSerializable; 23 | 24 | fuzz_target!(|data: &[u8]| { 25 | let _signature = coset::CoseSignature::from_slice(data); 26 | let _sign = coset::CoseSign::from_slice(data); 27 | let _sign1 = coset::CoseSign1::from_slice(data); 28 | }); 29 | -------------------------------------------------------------------------------- /scripts/build-gh-pages.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2020 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | set -o errexit 17 | set -o nounset 18 | set -o xtrace 19 | set -o pipefail 20 | 21 | # Update the gh-pages branch. Note that `cargo doc` is **not deterministic** so 22 | # this should only be done when there is a real change. 23 | readonly RUST_BRANCH=${1:-main} 24 | readonly RUST_GH_BRANCH=gh-pages 25 | 26 | if [ -z "${FORCE+x}" ]; then 27 | readonly PREV_COMMIT=$(git log --oneline -n 1 ${RUST_GH_BRANCH} | sed 's/.*branch at \([0-9a-f]*\)/\1/') 28 | readonly CHANGES=$(git diff "${PREV_COMMIT}..${RUST_BRANCH}" | grep -e '[+-]//[/!]') 29 | 30 | if [ -z "${CHANGES}" ]; then 31 | echo "No doc comment changes found in ${PREV_COMMIT}..${RUST_BRANCH} subdir rust/" 32 | exit 0 33 | fi 34 | fi 35 | 36 | git switch "${RUST_BRANCH}" 37 | readonly RUST_BRANCH_SHA1=$(git rev-parse --short HEAD) 38 | readonly RUST_BRANCH_SUBJECT=$(git log -n 1 --format=format:%s) 39 | readonly COMMIT_MESSAGE=$(cat <<-END 40 | Update Rust docs to ${RUST_BRANCH} branch at ${RUST_BRANCH_SHA1} 41 | 42 | Auto-generated from commit ${RUST_BRANCH_SHA1} ("${RUST_BRANCH_SUBJECT}"). 43 | END 44 | ) 45 | 46 | readonly TGZ_FILE="/tmp/coset-doc-${RUST_BRANCH_SHA1}.tgz" 47 | # Build Cargo docs and save them off outside the repo 48 | ( 49 | rm -rf target/doc 50 | cargo doc --no-deps 51 | cargo deadlinks 52 | cd target/doc || exit 53 | tar czf "${TGZ_FILE}" ./* 54 | ) 55 | 56 | # Shift to ${RUST_GH_BRANCH} branch and replace contents of (just) ./rust/ 57 | git switch ${RUST_GH_BRANCH} 58 | 59 | readonly DOC_DIR=rust 60 | rm -rf ${DOC_DIR} 61 | mkdir ${DOC_DIR} 62 | ( 63 | cd "${DOC_DIR}" || exit 64 | tar xzf "${TGZ_FILE}" 65 | ) 66 | 67 | # Commit any differences 68 | git add "${DOC_DIR}" 69 | git commit --message="${COMMIT_MESSAGE}" 70 | git switch "${RUST_BRANCH}" 71 | -------------------------------------------------------------------------------- /scripts/check-format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright 2021 Google LLC 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # Find code files. 17 | CODE_FILES=() 18 | while IFS= read -r -d $'\0'; do 19 | CODE_FILES+=("$REPLY") 20 | done < <(find . -not \( -path '*/target' -prune \) -and -name '*.rs' -print0) 21 | 22 | # Find markdown files. 23 | MD_FILES=() 24 | while IFS= read -r -d $'\0'; do 25 | MD_FILES+=("$REPLY") 26 | done < <(find . -not \( -path '*/target' -prune \) -and -not \( -path '*/wycheproof' -prune \) -and -name '*.md' -print0) 27 | 28 | # Check that source files have the Apache License header. 29 | # Automatically skips generated files. 30 | check_license() { 31 | local path="$1" 32 | 33 | if head -1 "$path" | grep -iq -e 'generated' -e '::prost::message'; then 34 | return 0 35 | fi 36 | 37 | if echo "$path" | grep -q "/proto/"; then 38 | return 0 39 | fi 40 | 41 | # Look for "Apache License" on the file header 42 | if ! head -10 "$path" | grep -q 'Apache License'; then 43 | # Format: $path:$line:$message 44 | echo "$path:1:license header not found" 45 | return 1 46 | fi 47 | return 0 48 | } 49 | 50 | # Check that any TODO markers in files have associated issue numbers 51 | check_todo() { 52 | local path="$1" 53 | local result 54 | result=$(grep --with-filename --line-number TODO "$path" | grep --invert-match --regexp='TODO(#[0-9][0-9]*)') 55 | if [[ -n $result ]]; then 56 | echo "TODO marker without issue number:" 57 | echo "$result" 58 | return 1 59 | fi 60 | return 0 61 | } 62 | 63 | # Check that any calls that might panic have a comment noting why they're safe 64 | check_panic() { 65 | local path="$1" 66 | if [[ $path =~ "test" || $path =~ "examples/" || $path =~ "rinkey/" || $path =~ "benches/" ]]; then 67 | return 0 68 | fi 69 | for needle in "panic!(" "unwrap(" "expect(" "unwrap_err(" "expect_err(" "unwrap_none(" "expect_none(" "unreachable!"; do 70 | local result 71 | result=$(grep --with-filename --line-number "$needle" "$path" | grep --invert-match --regexp='safe:'| grep --invert-match --regexp=':[0-9]*://') 72 | if [[ -n $result ]]; then 73 | echo "Un-annotated panic code:" 74 | echo "$result" 75 | return 1 76 | fi 77 | done 78 | return 0 79 | } 80 | 81 | errcount=0 82 | for f in "${CODE_FILES[@]}"; do 83 | check_license "$f" 84 | errcount=$((errcount + $?)) 85 | check_todo "$f" 86 | errcount=$((errcount + $?)) 87 | check_panic "$f" 88 | errcount=$((errcount + $?)) 89 | done 90 | 91 | EMBEDMD="$(go env GOPATH)/bin/embedmd" 92 | if [[ ! -x "$EMBEDMD" ]]; then 93 | go install github.com/campoy/embedmd@97c13d6 94 | fi 95 | for f in "${MD_FILES[@]}"; do 96 | "$EMBEDMD" -d "$f" 97 | errcount=$((errcount + $?)) 98 | check_todo "$f" 99 | errcount=$((errcount + $?)) 100 | mdl "$f" 101 | errcount=$((errcount + $?)) 102 | done 103 | 104 | if [ $errcount -gt 0 ]; then 105 | echo "$errcount errors detected" 106 | exit 1 107 | fi 108 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! Common types. 18 | 19 | use crate::{ 20 | cbor, 21 | cbor::value::Value, 22 | iana, 23 | iana::{EnumI64, WithPrivateRange}, 24 | util::{cbor_type_error, ValueTryAs}, 25 | }; 26 | use alloc::{boxed::Box, string::String, vec::Vec}; 27 | use core::{cmp::Ordering, convert::TryInto}; 28 | 29 | #[cfg(test)] 30 | mod tests; 31 | 32 | /// Marker structure indicating that the EOF was encountered when reading CBOR data. 33 | #[derive(Debug)] 34 | pub struct EndOfFile; 35 | 36 | /// Error type for failures in encoding or decoding COSE types. 37 | pub enum CoseError { 38 | /// CBOR decoding failure. 39 | DecodeFailed(cbor::de::Error), 40 | /// Duplicate map key detected. 41 | DuplicateMapKey, 42 | /// CBOR encoding failure. 43 | EncodeFailed, 44 | /// CBOR input had extra data. 45 | ExtraneousData, 46 | /// Integer value on the wire is outside the range of integers representable in this crate. 47 | /// See . 48 | OutOfRangeIntegerValue, 49 | /// Unexpected CBOR item encountered (got, want). 50 | UnexpectedItem(&'static str, &'static str), 51 | /// Unrecognized value in IANA-controlled range (with no private range). 52 | UnregisteredIanaValue, 53 | /// Unrecognized value in neither IANA-controlled range nor private range. 54 | UnregisteredIanaNonPrivateValue, 55 | } 56 | 57 | /// Crate-specific Result type 58 | pub type Result = core::result::Result; 59 | 60 | impl core::convert::From> for CoseError { 61 | fn from(e: cbor::de::Error) -> Self { 62 | // Make sure we use our [`EndOfFile`] marker. 63 | use cbor::de::Error::{Io, RecursionLimitExceeded, Semantic, Syntax}; 64 | let e = match e { 65 | Io(_) => Io(EndOfFile), 66 | Syntax(x) => Syntax(x), 67 | Semantic(a, b) => Semantic(a, b), 68 | RecursionLimitExceeded => RecursionLimitExceeded, 69 | }; 70 | CoseError::DecodeFailed(e) 71 | } 72 | } 73 | 74 | impl core::convert::From> for CoseError { 75 | fn from(_e: cbor::ser::Error) -> Self { 76 | CoseError::EncodeFailed 77 | } 78 | } 79 | 80 | impl core::convert::From for CoseError { 81 | fn from(_: core::num::TryFromIntError) -> Self { 82 | CoseError::OutOfRangeIntegerValue 83 | } 84 | } 85 | 86 | impl core::fmt::Debug for CoseError { 87 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 88 | self.fmt_msg(f) 89 | } 90 | } 91 | 92 | impl core::fmt::Display for CoseError { 93 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 94 | self.fmt_msg(f) 95 | } 96 | } 97 | 98 | #[cfg(feature = "std")] 99 | impl std::error::Error for CoseError {} 100 | 101 | impl CoseError { 102 | fn fmt_msg(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 103 | match self { 104 | CoseError::DecodeFailed(e) => write!(f, "decode CBOR failure: {}", e), 105 | CoseError::DuplicateMapKey => write!(f, "duplicate map key"), 106 | CoseError::EncodeFailed => write!(f, "encode CBOR failure"), 107 | CoseError::ExtraneousData => write!(f, "extraneous data in CBOR input"), 108 | CoseError::OutOfRangeIntegerValue => write!(f, "out of range integer value"), 109 | CoseError::UnexpectedItem(got, want) => write!(f, "got {}, expected {}", got, want), 110 | CoseError::UnregisteredIanaValue => write!(f, "expected recognized IANA value"), 111 | CoseError::UnregisteredIanaNonPrivateValue => { 112 | write!(f, "expected value in IANA or private use range") 113 | } 114 | } 115 | } 116 | } 117 | 118 | /// Read a CBOR [`Value`] from a byte slice, failing if any extra data remains after the `Value` has 119 | /// been read. 120 | fn read_to_value(mut slice: &[u8]) -> Result { 121 | let value = cbor::de::from_reader(&mut slice)?; 122 | if slice.is_empty() { 123 | Ok(value) 124 | } else { 125 | Err(CoseError::ExtraneousData) 126 | } 127 | } 128 | 129 | /// Trait for types that can be converted to/from a [`Value`]. 130 | pub trait AsCborValue: Sized { 131 | /// Convert a [`Value`] into an instance of the type. 132 | fn from_cbor_value(value: Value) -> Result; 133 | /// Convert the object into a [`Value`], consuming it along the way. 134 | fn to_cbor_value(self) -> Result; 135 | } 136 | 137 | /// Extension trait that adds serialization/deserialization methods. 138 | pub trait CborSerializable: AsCborValue { 139 | /// Create an object instance from serialized CBOR data in a slice. This method will fail (with 140 | /// `CoseError::ExtraneousData`) if there is additional CBOR data after the object. 141 | fn from_slice(slice: &[u8]) -> Result { 142 | Self::from_cbor_value(read_to_value(slice)?) 143 | } 144 | 145 | /// Serialize this object to a vector, consuming it along the way. 146 | fn to_vec(self) -> Result> { 147 | let mut data = Vec::new(); 148 | cbor::ser::into_writer(&self.to_cbor_value()?, &mut data)?; 149 | Ok(data) 150 | } 151 | } 152 | 153 | /// Extension trait that adds tagged serialization/deserialization methods. 154 | pub trait TaggedCborSerializable: AsCborValue { 155 | /// The associated tag value. 156 | const TAG: u64; 157 | 158 | /// Create an object instance from serialized CBOR data in a slice, expecting an initial 159 | /// tag value. 160 | fn from_tagged_slice(slice: &[u8]) -> Result { 161 | let (t, v) = read_to_value(slice)?.try_as_tag()?; 162 | if t != Self::TAG { 163 | return Err(CoseError::UnexpectedItem("tag", "other tag")); 164 | } 165 | Self::from_cbor_value(*v) 166 | } 167 | 168 | /// Serialize this object to a vector, including initial tag, consuming the object along the 169 | /// way. 170 | fn to_tagged_vec(self) -> Result> { 171 | let mut data = Vec::new(); 172 | cbor::ser::into_writer( 173 | &Value::Tag(Self::TAG, Box::new(self.to_cbor_value()?)), 174 | &mut data, 175 | )?; 176 | Ok(data) 177 | } 178 | } 179 | 180 | /// Trivial implementation of [`AsCborValue`] for [`Value`]. 181 | impl AsCborValue for Value { 182 | fn from_cbor_value(value: Value) -> Result { 183 | Ok(value) 184 | } 185 | fn to_cbor_value(self) -> Result { 186 | Ok(self) 187 | } 188 | } 189 | 190 | impl CborSerializable for Value {} 191 | 192 | /// Algorithm identifier. 193 | pub type Algorithm = crate::RegisteredLabelWithPrivate; 194 | 195 | impl Default for Algorithm { 196 | fn default() -> Self { 197 | Algorithm::Assigned(iana::Algorithm::Reserved) 198 | } 199 | } 200 | 201 | /// A COSE label may be either a signed integer value or a string. 202 | #[derive(Clone, Debug, Eq, PartialEq)] 203 | pub enum Label { 204 | Int(i64), 205 | Text(String), 206 | } 207 | 208 | impl CborSerializable for Label {} 209 | 210 | /// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected. 211 | /// 212 | /// Note that this uses the ordering given by RFC 8949 section 4.2.1 (lexicographic ordering of 213 | /// encoded form), which is *different* from the canonical ordering defined in RFC 7049 section 3.9 214 | /// (where the primary sorting criterion is the length of the encoded form) 215 | impl Ord for Label { 216 | fn cmp(&self, other: &Self) -> Ordering { 217 | match (self, other) { 218 | (Label::Int(i1), Label::Int(i2)) => match (i1.signum(), i2.signum()) { 219 | (-1, -1) => i2.cmp(i1), 220 | (-1, 0) => Ordering::Greater, 221 | (-1, 1) => Ordering::Greater, 222 | (0, -1) => Ordering::Less, 223 | (0, 0) => Ordering::Equal, 224 | (0, 1) => Ordering::Less, 225 | (1, -1) => Ordering::Less, 226 | (1, 0) => Ordering::Greater, 227 | (1, 1) => i1.cmp(i2), 228 | (_, _) => unreachable!(), // safe: all possibilies covered 229 | }, 230 | (Label::Int(_i1), Label::Text(_t2)) => Ordering::Less, 231 | (Label::Text(_t1), Label::Int(_i2)) => Ordering::Greater, 232 | (Label::Text(t1), Label::Text(t2)) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)), 233 | } 234 | } 235 | } 236 | 237 | impl PartialOrd for Label { 238 | fn partial_cmp(&self, other: &Self) -> Option { 239 | Some(self.cmp(other)) 240 | } 241 | } 242 | 243 | impl Label { 244 | /// Alternative ordering for `Label`, using the canonical ordering criteria from RFC 7049 245 | /// section 3.9 (where the primary sorting criterion is the length of the encoded form), rather 246 | /// than the ordering given by RFC 8949 section 4.2.1 (lexicographic ordering of encoded form). 247 | /// 248 | /// # Panics 249 | /// 250 | /// Panics if either `Label` fails to serialize. 251 | pub fn cmp_canonical(&self, other: &Self) -> Ordering { 252 | let encoded_self = self.clone().to_vec().unwrap(); /* safe: documented */ 253 | let encoded_other = other.clone().to_vec().unwrap(); /* safe: documented */ 254 | if encoded_self.len() != encoded_other.len() { 255 | // Shorter encoding sorts first. 256 | encoded_self.len().cmp(&encoded_other.len()) 257 | } else { 258 | // Both encode to the same length, sort lexicographically on encoded form. 259 | encoded_self.cmp(&encoded_other) 260 | } 261 | } 262 | } 263 | 264 | /// Indicate which ordering should be applied to CBOR values. 265 | pub enum CborOrdering { 266 | /// Order values lexicographically, as per RFC 8949 section 4.2.1 (Core Deterministic Encoding 267 | /// Requirements) 268 | Lexicographic, 269 | /// Order values by encoded length, then by lexicographic ordering of encoded form, as per RFC 270 | /// 7049 section 3.9 (Canonical CBOR) / RFC 8949 section 4.2.3 (Length-First Map Key Ordering). 271 | LengthFirstLexicographic, 272 | } 273 | 274 | impl AsCborValue for Label { 275 | fn from_cbor_value(value: Value) -> Result { 276 | match value { 277 | Value::Integer(i) => Ok(Label::Int(i.try_into()?)), 278 | Value::Text(t) => Ok(Label::Text(t)), 279 | v => cbor_type_error(&v, "int/tstr"), 280 | } 281 | } 282 | fn to_cbor_value(self) -> Result { 283 | Ok(match self { 284 | Label::Int(i) => Value::from(i), 285 | Label::Text(t) => Value::Text(t), 286 | }) 287 | } 288 | } 289 | 290 | /// A COSE label which can be either a signed integer value or a string, but 291 | /// where the allowed integer values are governed by IANA. 292 | #[derive(Clone, Debug, Eq, PartialEq)] 293 | pub enum RegisteredLabel { 294 | Assigned(T), 295 | Text(String), 296 | } 297 | 298 | impl CborSerializable for RegisteredLabel {} 299 | 300 | /// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected. 301 | impl Ord for RegisteredLabel { 302 | fn cmp(&self, other: &Self) -> Ordering { 303 | match (self, other) { 304 | (RegisteredLabel::Assigned(i1), RegisteredLabel::Assigned(i2)) => { 305 | Label::Int(i1.to_i64()).cmp(&Label::Int(i2.to_i64())) 306 | } 307 | (RegisteredLabel::Assigned(_i1), RegisteredLabel::Text(_t2)) => Ordering::Less, 308 | (RegisteredLabel::Text(_t1), RegisteredLabel::Assigned(_i2)) => Ordering::Greater, 309 | (RegisteredLabel::Text(t1), RegisteredLabel::Text(t2)) => { 310 | t1.len().cmp(&t2.len()).then(t1.cmp(t2)) 311 | } 312 | } 313 | } 314 | } 315 | 316 | impl PartialOrd for RegisteredLabel { 317 | fn partial_cmp(&self, other: &Self) -> Option { 318 | Some(self.cmp(other)) 319 | } 320 | } 321 | 322 | impl AsCborValue for RegisteredLabel { 323 | fn from_cbor_value(value: Value) -> Result { 324 | match value { 325 | Value::Integer(i) => { 326 | if let Some(a) = T::from_i64(i.try_into()?) { 327 | Ok(RegisteredLabel::Assigned(a)) 328 | } else { 329 | Err(CoseError::UnregisteredIanaValue) 330 | } 331 | } 332 | Value::Text(t) => Ok(RegisteredLabel::Text(t)), 333 | v => cbor_type_error(&v, "int/tstr"), 334 | } 335 | } 336 | 337 | fn to_cbor_value(self) -> Result { 338 | Ok(match self { 339 | RegisteredLabel::Assigned(e) => Value::from(e.to_i64()), 340 | RegisteredLabel::Text(t) => Value::Text(t), 341 | }) 342 | } 343 | } 344 | 345 | /// A COSE label which can be either a signed integer value or a string, and 346 | /// where the allowed integer values are governed by IANA but include a private 347 | /// use range. 348 | #[derive(Clone, Debug, Eq, PartialEq)] 349 | pub enum RegisteredLabelWithPrivate { 350 | PrivateUse(i64), 351 | Assigned(T), 352 | Text(String), 353 | } 354 | 355 | impl CborSerializable for RegisteredLabelWithPrivate {} 356 | 357 | /// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected. 358 | impl Ord for RegisteredLabelWithPrivate { 359 | fn cmp(&self, other: &Self) -> Ordering { 360 | use RegisteredLabelWithPrivate::{Assigned, PrivateUse, Text}; 361 | match (self, other) { 362 | (Assigned(i1), Assigned(i2)) => Label::Int(i1.to_i64()).cmp(&Label::Int(i2.to_i64())), 363 | (Assigned(i1), PrivateUse(i2)) => Label::Int(i1.to_i64()).cmp(&Label::Int(*i2)), 364 | (PrivateUse(i1), Assigned(i2)) => Label::Int(*i1).cmp(&Label::Int(i2.to_i64())), 365 | (PrivateUse(i1), PrivateUse(i2)) => Label::Int(*i1).cmp(&Label::Int(*i2)), 366 | (Assigned(_i1), Text(_t2)) => Ordering::Less, 367 | (PrivateUse(_i1), Text(_t2)) => Ordering::Less, 368 | (Text(_t1), Assigned(_i2)) => Ordering::Greater, 369 | (Text(_t1), PrivateUse(_i2)) => Ordering::Greater, 370 | (Text(t1), Text(t2)) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)), 371 | } 372 | } 373 | } 374 | 375 | impl PartialOrd for RegisteredLabelWithPrivate { 376 | fn partial_cmp(&self, other: &Self) -> Option { 377 | Some(self.cmp(other)) 378 | } 379 | } 380 | 381 | impl AsCborValue for RegisteredLabelWithPrivate { 382 | fn from_cbor_value(value: Value) -> Result { 383 | match value { 384 | Value::Integer(i) => { 385 | let i = i.try_into()?; 386 | if let Some(a) = T::from_i64(i) { 387 | Ok(RegisteredLabelWithPrivate::Assigned(a)) 388 | } else if T::is_private(i) { 389 | Ok(RegisteredLabelWithPrivate::PrivateUse(i)) 390 | } else { 391 | Err(CoseError::UnregisteredIanaNonPrivateValue) 392 | } 393 | } 394 | Value::Text(t) => Ok(RegisteredLabelWithPrivate::Text(t)), 395 | v => cbor_type_error(&v, "int/tstr"), 396 | } 397 | } 398 | fn to_cbor_value(self) -> Result { 399 | Ok(match self { 400 | RegisteredLabelWithPrivate::PrivateUse(i) => Value::from(i), 401 | RegisteredLabelWithPrivate::Assigned(i) => Value::from(i.to_i64()), 402 | RegisteredLabelWithPrivate::Text(t) => Value::Text(t), 403 | }) 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/common/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | use super::*; 18 | use crate::util::expect_err; 19 | use alloc::{borrow::ToOwned, format, vec}; 20 | use core::cmp::Ordering; 21 | 22 | #[test] 23 | fn test_error_convert() { 24 | let e = CoseError::from(crate::cbor::ser::Error::::Value( 25 | "error message lost".to_owned(), 26 | )); 27 | match e { 28 | CoseError::EncodeFailed => { 29 | assert!(format!("{:?}", e).contains("encode CBOR failure")); 30 | assert!(format!("{}", e).contains("encode CBOR failure")); 31 | } 32 | _ => panic!("unexpected error enum after conversion"), 33 | } 34 | } 35 | 36 | #[test] 37 | fn test_label_encode() { 38 | let tests = [ 39 | (Label::Int(2), "02"), 40 | (Label::Int(-1), "20"), 41 | (Label::Text("abc".to_owned()), "63616263"), 42 | ]; 43 | 44 | for (i, (label, label_data)) in tests.iter().enumerate() { 45 | let got = label.clone().to_vec().unwrap(); 46 | assert_eq!(*label_data, hex::encode(&got), "case {}", i); 47 | 48 | let got = Label::from_slice(&got).unwrap(); 49 | assert_eq!(*label, got); 50 | } 51 | } 52 | 53 | #[test] 54 | fn test_label_sort() { 55 | // Pairs of `Label`s with the "smaller" first. 56 | let pairs = vec![ 57 | (Label::Int(0x1234), Label::Text("a".to_owned())), 58 | (Label::Int(0x1234), Label::Text("ab".to_owned())), 59 | (Label::Int(0x12345678), Label::Text("ab".to_owned())), 60 | (Label::Int(0), Label::Text("ab".to_owned())), 61 | (Label::Int(-1), Label::Text("ab".to_owned())), 62 | (Label::Int(0), Label::Int(10)), 63 | (Label::Int(0), Label::Int(-10)), 64 | (Label::Int(10), Label::Int(-1)), 65 | (Label::Int(-1), Label::Int(-2)), 66 | (Label::Int(0x12), Label::Int(0x1234)), 67 | (Label::Int(0x99), Label::Int(0x1234)), 68 | (Label::Int(0x1234), Label::Int(0x1235)), 69 | (Label::Text("a".to_owned()), Label::Text("ab".to_owned())), 70 | (Label::Text("aa".to_owned()), Label::Text("ab".to_owned())), 71 | (Label::Int(i64::MAX - 2), Label::Int(i64::MAX - 1)), 72 | (Label::Int(i64::MAX - 1), Label::Int(i64::MAX)), 73 | (Label::Int(i64::MIN + 2), Label::Int(i64::MIN + 1)), 74 | (Label::Int(i64::MIN + 1), Label::Int(i64::MIN)), 75 | ]; 76 | for (left, right) in pairs.into_iter() { 77 | let value_cmp = left.cmp(&right); 78 | let value_partial_cmp = left.partial_cmp(&right); 79 | let left_data = left.clone().to_vec().unwrap(); 80 | let right_data = right.clone().to_vec().unwrap(); 81 | let data_cmp = left_data.cmp(&right_data); 82 | let reverse_cmp = right.cmp(&left); 83 | let equal_cmp = left.cmp(&left); 84 | 85 | assert_eq!(value_cmp, Ordering::Less, "{:?} < {:?}", left, right); 86 | assert_eq!( 87 | value_partial_cmp, 88 | Some(Ordering::Less), 89 | "{:?} < {:?}", 90 | left, 91 | right 92 | ); 93 | assert_eq!( 94 | data_cmp, 95 | Ordering::Less, 96 | "{:?}={} < {:?}={}", 97 | left, 98 | hex::encode(&left_data), 99 | right, 100 | hex::encode(&right_data) 101 | ); 102 | assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left); 103 | assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left); 104 | } 105 | } 106 | 107 | #[test] 108 | fn test_label_canonical_sort() { 109 | // Pairs of `Label`s with the "smaller" first, as per RFC7049 "canonical" ordering. 110 | let pairs = vec![ 111 | (Label::Text("a".to_owned()), Label::Int(0x1234)), // different than above 112 | (Label::Int(0x1234), Label::Text("ab".to_owned())), 113 | (Label::Text("ab".to_owned()), Label::Int(0x12345678)), // different than above 114 | (Label::Int(0), Label::Text("ab".to_owned())), 115 | (Label::Int(-1), Label::Text("ab".to_owned())), 116 | (Label::Int(0), Label::Int(10)), 117 | (Label::Int(0), Label::Int(-10)), 118 | (Label::Int(10), Label::Int(-1)), 119 | (Label::Int(-1), Label::Int(-2)), 120 | (Label::Int(0x12), Label::Int(0x1234)), 121 | (Label::Int(0x99), Label::Int(0x1234)), 122 | (Label::Int(0x1234), Label::Int(0x1235)), 123 | (Label::Text("a".to_owned()), Label::Text("ab".to_owned())), 124 | (Label::Text("aa".to_owned()), Label::Text("ab".to_owned())), 125 | ]; 126 | for (left, right) in pairs.into_iter() { 127 | let value_cmp = left.cmp_canonical(&right); 128 | 129 | let left_data = left.clone().to_vec().unwrap(); 130 | let right_data = right.clone().to_vec().unwrap(); 131 | 132 | let len_cmp = left_data.len().cmp(&right_data.len()); 133 | let data_cmp = left_data.cmp(&right_data); 134 | let reverse_cmp = right.cmp_canonical(&left); 135 | let equal_cmp = left.cmp_canonical(&left); 136 | 137 | assert_eq!( 138 | value_cmp, 139 | Ordering::Less, 140 | "{:?} (encoded: {}) < {:?} (encoded: {})", 141 | left, 142 | hex::encode(&left_data), 143 | right, 144 | hex::encode(&right_data) 145 | ); 146 | if len_cmp != Ordering::Equal { 147 | assert_eq!( 148 | len_cmp, 149 | Ordering::Less, 150 | "{:?}={} < {:?}={} by len", 151 | left, 152 | hex::encode(&left_data), 153 | right, 154 | hex::encode(&right_data) 155 | ); 156 | } else { 157 | assert_eq!( 158 | data_cmp, 159 | Ordering::Less, 160 | "{:?}={} < {:?}={} by data", 161 | left, 162 | hex::encode(&left_data), 163 | right, 164 | hex::encode(&right_data) 165 | ); 166 | } 167 | assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left); 168 | assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left); 169 | } 170 | } 171 | 172 | #[test] 173 | fn test_label_decode_fail() { 174 | let tests = [ 175 | ("43010203", "expected int/tstr"), 176 | ("", "decode CBOR failure: Io(EndOfFile"), 177 | ("1e", "decode CBOR failure: Syntax"), 178 | ("0202", "extraneous data"), 179 | ]; 180 | for (label_data, err_msg) in tests.iter() { 181 | let data = hex::decode(label_data).unwrap(); 182 | let result = Label::from_slice(&data); 183 | expect_err(result, err_msg); 184 | } 185 | } 186 | 187 | #[test] 188 | fn test_registered_label_encode() { 189 | let tests = [ 190 | (RegisteredLabel::Assigned(iana::Algorithm::A192GCM), "02"), 191 | (RegisteredLabel::Assigned(iana::Algorithm::EdDSA), "27"), 192 | (RegisteredLabel::Text("abc".to_owned()), "63616263"), 193 | ]; 194 | 195 | for (i, (label, label_data)) in tests.iter().enumerate() { 196 | let got = label.clone().to_vec().unwrap(); 197 | assert_eq!(*label_data, hex::encode(&got), "case {}", i); 198 | 199 | let got = RegisteredLabel::from_slice(&got).unwrap(); 200 | assert_eq!(*label, got); 201 | } 202 | } 203 | 204 | #[test] 205 | fn test_registered_label_sort() { 206 | use RegisteredLabel::{Assigned, Text}; 207 | // Pairs of `RegisteredLabel`s with the "smaller" first. 208 | let pairs = vec![ 209 | (Assigned(iana::Algorithm::A192GCM), Text("a".to_owned())), 210 | (Assigned(iana::Algorithm::WalnutDSA), Text("ab".to_owned())), 211 | (Text("ab".to_owned()), Text("cd".to_owned())), 212 | (Text("ab".to_owned()), Text("abcd".to_owned())), 213 | ( 214 | Assigned(iana::Algorithm::AES_CCM_16_64_128), 215 | Assigned(iana::Algorithm::A128KW), 216 | ), 217 | ( 218 | Assigned(iana::Algorithm::A192GCM), 219 | Assigned(iana::Algorithm::AES_CCM_16_64_128), 220 | ), 221 | ]; 222 | for (left, right) in pairs.into_iter() { 223 | let value_cmp = left.cmp(&right); 224 | let value_partial_cmp = left.partial_cmp(&right); 225 | let left_data = left.clone().to_vec().unwrap(); 226 | let right_data = right.clone().to_vec().unwrap(); 227 | let data_cmp = left_data.cmp(&right_data); 228 | let reverse_cmp = right.cmp(&left); 229 | let equal_cmp = left.cmp(&left); 230 | 231 | assert_eq!(value_cmp, Ordering::Less, "{:?} < {:?}", left, right); 232 | assert_eq!( 233 | value_partial_cmp, 234 | Some(Ordering::Less), 235 | "{:?} < {:?}", 236 | left, 237 | right 238 | ); 239 | assert_eq!( 240 | data_cmp, 241 | Ordering::Less, 242 | "{:?}={} < {:?}={}", 243 | left, 244 | hex::encode(&left_data), 245 | right, 246 | hex::encode(&right_data) 247 | ); 248 | assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left); 249 | assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left); 250 | } 251 | } 252 | 253 | #[test] 254 | fn test_registered_label_decode_fail() { 255 | let tests = [ 256 | ("43010203", "expected int/tstr"), 257 | ("", "decode CBOR failure: Io(EndOfFile"), 258 | ("09", "expected recognized IANA value"), 259 | ("394e1f", "expected recognized IANA value"), 260 | ]; 261 | for (label_data, err_msg) in tests.iter() { 262 | let data = hex::decode(label_data).unwrap(); 263 | let result = RegisteredLabel::::from_slice(&data); 264 | expect_err(result, err_msg); 265 | } 266 | } 267 | 268 | iana_registry! { 269 | TestPrivateLabel { 270 | Reserved: 0, 271 | Something: 1, 272 | } 273 | } 274 | 275 | impl WithPrivateRange for TestPrivateLabel { 276 | fn is_private(i: i64) -> bool { 277 | i > 10 || i < 1000 278 | } 279 | } 280 | 281 | #[test] 282 | fn test_registered_label_with_private_encode() { 283 | let tests = [ 284 | ( 285 | RegisteredLabelWithPrivate::Assigned(TestPrivateLabel::Something), 286 | "01", 287 | ), 288 | ( 289 | RegisteredLabelWithPrivate::Text("abc".to_owned()), 290 | "63616263", 291 | ), 292 | ( 293 | RegisteredLabelWithPrivate::PrivateUse(-70_000), 294 | "3a0001116f", 295 | ), 296 | (RegisteredLabelWithPrivate::PrivateUse(11), "0b"), 297 | ]; 298 | 299 | for (i, (label, label_data)) in tests.iter().enumerate() { 300 | let got = label.clone().to_vec().unwrap(); 301 | assert_eq!(*label_data, hex::encode(&got), "case {}", i); 302 | 303 | let got = RegisteredLabelWithPrivate::from_slice(&got).unwrap(); 304 | assert_eq!(*label, got); 305 | } 306 | } 307 | 308 | #[test] 309 | fn test_registered_label_with_private_sort() { 310 | use RegisteredLabelWithPrivate::{Assigned, PrivateUse, Text}; 311 | // Pairs of `RegisteredLabelWithPrivate`s with the "smaller" first. 312 | let pairs = vec![ 313 | (Assigned(iana::Algorithm::A192GCM), Text("a".to_owned())), 314 | (Assigned(iana::Algorithm::WalnutDSA), Text("ab".to_owned())), 315 | (Text("ab".to_owned()), Text("cd".to_owned())), 316 | (Text("ab".to_owned()), Text("abcd".to_owned())), 317 | ( 318 | Assigned(iana::Algorithm::AES_CCM_16_64_128), 319 | Assigned(iana::Algorithm::A128KW), 320 | ), 321 | ( 322 | Assigned(iana::Algorithm::A192GCM), 323 | Assigned(iana::Algorithm::AES_CCM_16_64_128), 324 | ), 325 | ( 326 | Assigned(iana::Algorithm::AES_CCM_16_64_128), 327 | PrivateUse(-70_000), 328 | ), 329 | (PrivateUse(-70_000), PrivateUse(-70_001)), 330 | (PrivateUse(-70_000), Text("a".to_owned())), 331 | ]; 332 | for (left, right) in pairs.into_iter() { 333 | let value_cmp = left.cmp(&right); 334 | let value_partial_cmp = left.partial_cmp(&right); 335 | let left_data = left.clone().to_vec().unwrap(); 336 | let right_data = right.clone().to_vec().unwrap(); 337 | let data_cmp = left_data.cmp(&right_data); 338 | let reverse_cmp = right.cmp(&left); 339 | let equal_cmp = left.cmp(&left); 340 | 341 | assert_eq!(value_cmp, Ordering::Less, "{:?} < {:?}", left, right); 342 | assert_eq!( 343 | value_partial_cmp, 344 | Some(Ordering::Less), 345 | "{:?} < {:?}", 346 | left, 347 | right 348 | ); 349 | assert_eq!( 350 | data_cmp, 351 | Ordering::Less, 352 | "{:?}={} < {:?}={}", 353 | left, 354 | hex::encode(&left_data), 355 | right, 356 | hex::encode(&right_data) 357 | ); 358 | assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left); 359 | assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left); 360 | } 361 | } 362 | 363 | #[test] 364 | fn test_registered_label_with_private_decode_fail() { 365 | let tests = [ 366 | ("43010203", "expected int/tstr"), 367 | ("", "decode CBOR failure: Io(EndOfFile"), 368 | ("09", "expected value in IANA or private use range"), 369 | ("394e1f", "expected value in IANA or private use range"), 370 | ]; 371 | for (label_data, err_msg) in tests.iter() { 372 | let data = hex::decode(label_data).unwrap(); 373 | let result = RegisteredLabelWithPrivate::::from_slice(&data); 374 | expect_err(result, err_msg); 375 | } 376 | } 377 | 378 | // The most negative integer value that can be encoded in CBOR is: 379 | // 0x3B (0b001_11011) 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 380 | // which is -18_446_744_073_709_551_616 (-1 - 18_446_744_073_709_551_615). 381 | // 382 | // However, this crate uses `i64` for all integers, which cannot hold 383 | // negative values below `i64::MIN` (=-2^63 = 0x8000000000000000). 384 | const CBOR_NINT_MIN_HEX: &str = "3b7fffffffffffffff"; 385 | const CBOR_NINT_OUT_OF_RANGE_HEX: &str = "3b8000000000000000"; 386 | 387 | // The largest positive integer value that can be encoded in CBOR is: 388 | // 0x1B (0b000_11011) 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 389 | // which is 18_446_744_073_709_551_615. 390 | // 391 | // However, this crate uses `i64` for all integers, which cannot hold 392 | // positive values above `i64::MAX` (=-2^63 - 1 = 0x7fffffffffffffff). 393 | const CBOR_INT_MAX_HEX: &str = "1b7fffffffffffffff"; 394 | const CBOR_INT_OUT_OF_RANGE_HEX: &str = "1b8000000000000000"; 395 | 396 | #[test] 397 | fn test_large_label_decode() { 398 | let tests = [(CBOR_NINT_MIN_HEX, i64::MIN), (CBOR_INT_MAX_HEX, i64::MAX)]; 399 | for (label_data, want) in tests.iter() { 400 | let data = hex::decode(label_data).unwrap(); 401 | let got = Label::from_slice(&data).unwrap(); 402 | assert_eq!(got, Label::Int(*want)) 403 | } 404 | } 405 | 406 | #[test] 407 | fn test_large_label_decode_fail() { 408 | let tests = [ 409 | (CBOR_NINT_OUT_OF_RANGE_HEX, "out of range integer value"), 410 | (CBOR_INT_OUT_OF_RANGE_HEX, "out of range integer value"), 411 | ]; 412 | for (label_data, err_msg) in tests.iter() { 413 | let data = hex::decode(label_data).unwrap(); 414 | let result = Label::from_slice(&data); 415 | expect_err(result, err_msg); 416 | } 417 | } 418 | 419 | #[test] 420 | fn test_large_registered_label_decode_fail() { 421 | let tests = [ 422 | (CBOR_NINT_OUT_OF_RANGE_HEX, "out of range integer value"), 423 | (CBOR_INT_OUT_OF_RANGE_HEX, "out of range integer value"), 424 | ]; 425 | for (label_data, err_msg) in tests.iter() { 426 | let data = hex::decode(label_data).unwrap(); 427 | let result = RegisteredLabel::::from_slice(&data); 428 | expect_err(result, err_msg); 429 | } 430 | } 431 | 432 | #[test] 433 | fn test_large_registered_label_with_private_decode_fail() { 434 | let tests = [ 435 | (CBOR_NINT_OUT_OF_RANGE_HEX, "out of range integer value"), 436 | (CBOR_INT_OUT_OF_RANGE_HEX, "out of range integer value"), 437 | ]; 438 | for (label_data, err_msg) in tests.iter() { 439 | let data = hex::decode(label_data).unwrap(); 440 | let result = RegisteredLabelWithPrivate::::from_slice(&data); 441 | expect_err(result, err_msg); 442 | } 443 | } 444 | 445 | #[test] 446 | fn test_as_cbor_value() { 447 | let cases = [ 448 | Value::Null, 449 | Value::Bool(true), 450 | Value::Bool(false), 451 | Value::from(128), 452 | Value::from(-1), 453 | Value::Bytes(vec![1, 2]), 454 | Value::Text("string".to_owned()), 455 | Value::Array(vec![Value::from(0)]), 456 | Value::Map(vec![]), 457 | Value::Tag(1, Box::new(Value::from(0))), 458 | Value::Float(1.054571817), 459 | ]; 460 | for val in cases { 461 | assert_eq!(val, Value::from_cbor_value(val.clone()).unwrap()); 462 | assert_eq!(val, val.clone().to_cbor_value().unwrap()); 463 | } 464 | } 465 | -------------------------------------------------------------------------------- /src/context/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! COSE_KDF_Context functionality. 18 | 19 | use crate::{ 20 | cbor::value::Value, 21 | common::AsCborValue, 22 | iana, 23 | util::{cbor_type_error, ValueTryAs}, 24 | Algorithm, CoseError, ProtectedHeader, Result, 25 | }; 26 | use alloc::{vec, vec::Vec}; 27 | use core::convert::TryInto; 28 | 29 | #[cfg(test)] 30 | mod tests; 31 | 32 | /// A nonce value. 33 | #[derive(Clone, Debug, Eq, PartialEq)] 34 | pub enum Nonce { 35 | Bytes(Vec), 36 | Integer(i64), 37 | } 38 | 39 | /// Structure representing a party involved in key derivation. 40 | /// 41 | /// ```cddl 42 | /// PartyInfo = ( 43 | /// identity : bstr / nil, 44 | /// nonce : bstr / int / nil, 45 | /// other : bstr / nil 46 | /// ) 47 | /// ``` 48 | #[derive(Clone, Debug, Default, Eq, PartialEq)] 49 | pub struct PartyInfo { 50 | pub identity: Option>, 51 | pub nonce: Option, 52 | pub other: Option>, 53 | } 54 | 55 | impl crate::CborSerializable for PartyInfo {} 56 | 57 | impl AsCborValue for PartyInfo { 58 | fn from_cbor_value(value: Value) -> Result { 59 | let mut a = value.try_as_array()?; 60 | if a.len() != 3 { 61 | return Err(CoseError::UnexpectedItem("array", "array with 3 items")); 62 | } 63 | 64 | // Remove array elements in reverse order to avoid shifts. 65 | Ok(Self { 66 | other: match a.remove(2) { 67 | Value::Null => None, 68 | Value::Bytes(b) => Some(b), 69 | v => return cbor_type_error(&v, "bstr / nil"), 70 | }, 71 | nonce: match a.remove(1) { 72 | Value::Null => None, 73 | Value::Bytes(b) => Some(Nonce::Bytes(b)), 74 | Value::Integer(u) => Some(Nonce::Integer(u.try_into()?)), 75 | v => return cbor_type_error(&v, "bstr / int / nil"), 76 | }, 77 | identity: match a.remove(0) { 78 | Value::Null => None, 79 | Value::Bytes(b) => Some(b), 80 | v => return cbor_type_error(&v, "bstr / nil"), 81 | }, 82 | }) 83 | } 84 | 85 | fn to_cbor_value(self) -> Result { 86 | Ok(Value::Array(vec![ 87 | match self.identity { 88 | None => Value::Null, 89 | Some(b) => Value::Bytes(b), 90 | }, 91 | match self.nonce { 92 | None => Value::Null, 93 | Some(Nonce::Bytes(b)) => Value::Bytes(b), 94 | Some(Nonce::Integer(i)) => Value::from(i), 95 | }, 96 | match self.other { 97 | None => Value::Null, 98 | Some(b) => Value::Bytes(b), 99 | }, 100 | ])) 101 | } 102 | } 103 | 104 | /// Builder for [`PartyInfo`] objects. 105 | #[derive(Debug, Default)] 106 | pub struct PartyInfoBuilder(PartyInfo); 107 | 108 | impl PartyInfoBuilder { 109 | builder! {PartyInfo} 110 | builder_set_optional! {identity: Vec} 111 | builder_set_optional! {nonce: Nonce} 112 | builder_set_optional! {other: Vec} 113 | } 114 | 115 | /// Structure representing supplemental public information. 116 | /// 117 | /// ```cddl 118 | /// SuppPubInfo : [ 119 | /// keyDataLength : uint, 120 | /// protected : empty_or_serialized_map, 121 | /// ? other : bstr 122 | /// ], 123 | /// ``` 124 | #[derive(Clone, Debug, Default, PartialEq)] 125 | pub struct SuppPubInfo { 126 | pub key_data_length: u64, 127 | pub protected: ProtectedHeader, 128 | pub other: Option>, 129 | } 130 | 131 | impl crate::CborSerializable for SuppPubInfo {} 132 | 133 | impl AsCborValue for SuppPubInfo { 134 | fn from_cbor_value(value: Value) -> Result { 135 | let mut a = value.try_as_array()?; 136 | if a.len() != 2 && a.len() != 3 { 137 | return Err(CoseError::UnexpectedItem( 138 | "array", 139 | "array with 2 or 3 items", 140 | )); 141 | } 142 | 143 | // Remove array elements in reverse order to avoid shifts. 144 | Ok(Self { 145 | other: { 146 | if a.len() == 3 { 147 | Some(a.remove(2).try_as_bytes()?) 148 | } else { 149 | None 150 | } 151 | }, 152 | protected: ProtectedHeader::from_cbor_bstr(a.remove(1))?, 153 | key_data_length: a.remove(0).try_as_integer()?.try_into()?, 154 | }) 155 | } 156 | 157 | fn to_cbor_value(self) -> Result { 158 | let mut v = vec![ 159 | Value::from(self.key_data_length), 160 | self.protected.cbor_bstr()?, 161 | ]; 162 | if let Some(other) = self.other { 163 | v.push(Value::Bytes(other)); 164 | } 165 | Ok(Value::Array(v)) 166 | } 167 | } 168 | 169 | /// Builder for [`SuppPubInfo`] objects. 170 | #[derive(Debug, Default)] 171 | pub struct SuppPubInfoBuilder(SuppPubInfo); 172 | 173 | impl SuppPubInfoBuilder { 174 | builder! {SuppPubInfo} 175 | builder_set! {key_data_length: u64} 176 | builder_set_protected! {protected} 177 | builder_set_optional! {other: Vec} 178 | } 179 | 180 | /// Structure representing a a key derivation context. 181 | /// ```cdl 182 | /// COSE_KDF_Context = [ 183 | /// AlgorithmID : int / tstr, 184 | /// PartyUInfo : [ PartyInfo ], 185 | /// PartyVInfo : [ PartyInfo ], 186 | /// SuppPubInfo : [ 187 | /// keyDataLength : uint, 188 | /// protected : empty_or_serialized_map, 189 | /// ? other : bstr 190 | /// ], 191 | /// ? SuppPrivInfo : bstr 192 | /// ] 193 | /// ``` 194 | #[derive(Clone, Debug, Default, PartialEq)] 195 | pub struct CoseKdfContext { 196 | algorithm_id: Algorithm, 197 | party_u_info: PartyInfo, 198 | party_v_info: PartyInfo, 199 | supp_pub_info: SuppPubInfo, 200 | supp_priv_info: Vec>, 201 | } 202 | 203 | impl crate::CborSerializable for CoseKdfContext {} 204 | 205 | impl AsCborValue for CoseKdfContext { 206 | fn from_cbor_value(value: Value) -> Result { 207 | let mut a = value.try_as_array()?; 208 | if a.len() < 4 { 209 | return Err(CoseError::UnexpectedItem( 210 | "array", 211 | "array with at least 4 items", 212 | )); 213 | } 214 | 215 | // Remove array elements in reverse order to avoid shifts. 216 | let mut supp_priv_info = Vec::with_capacity(a.len() - 4); 217 | for i in (4..a.len()).rev() { 218 | supp_priv_info.push(a.remove(i).try_as_bytes()?); 219 | } 220 | supp_priv_info.reverse(); 221 | 222 | Ok(Self { 223 | supp_priv_info, 224 | supp_pub_info: SuppPubInfo::from_cbor_value(a.remove(3))?, 225 | party_v_info: PartyInfo::from_cbor_value(a.remove(2))?, 226 | party_u_info: PartyInfo::from_cbor_value(a.remove(1))?, 227 | algorithm_id: Algorithm::from_cbor_value(a.remove(0))?, 228 | }) 229 | } 230 | 231 | fn to_cbor_value(self) -> Result { 232 | let mut v = vec![ 233 | self.algorithm_id.to_cbor_value()?, 234 | self.party_u_info.to_cbor_value()?, 235 | self.party_v_info.to_cbor_value()?, 236 | self.supp_pub_info.to_cbor_value()?, 237 | ]; 238 | for supp_priv_info in self.supp_priv_info { 239 | v.push(Value::Bytes(supp_priv_info)); 240 | } 241 | Ok(Value::Array(v)) 242 | } 243 | } 244 | 245 | /// Builder for [`CoseKdfContext`] objects. 246 | #[derive(Debug, Default)] 247 | pub struct CoseKdfContextBuilder(CoseKdfContext); 248 | 249 | impl CoseKdfContextBuilder { 250 | builder! {CoseKdfContext} 251 | builder_set! {party_u_info: PartyInfo} 252 | builder_set! {party_v_info: PartyInfo} 253 | builder_set! {supp_pub_info: SuppPubInfo} 254 | 255 | /// Set the algorithm. 256 | #[must_use] 257 | pub fn algorithm(mut self, alg: iana::Algorithm) -> Self { 258 | self.0.algorithm_id = Algorithm::Assigned(alg); 259 | self 260 | } 261 | 262 | /// Add supplemental private info. 263 | #[must_use] 264 | pub fn add_supp_priv_info(mut self, supp_priv_info: Vec) -> Self { 265 | self.0.supp_priv_info.push(supp_priv_info); 266 | self 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/context/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | use super::*; 18 | use crate::{iana, util::expect_err, CborSerializable, HeaderBuilder}; 19 | use alloc::vec; 20 | 21 | #[test] 22 | fn test_context_encode() { 23 | let tests = vec![ 24 | ( 25 | CoseKdfContext::default(), 26 | concat!( 27 | "84", // 4-tuple 28 | "00", // int : reserved 29 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 30 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 31 | "82", "0040", // 2-tuple: [0, 0-bstr] 32 | ), 33 | ), 34 | ( 35 | CoseKdfContextBuilder::new() 36 | .algorithm(iana::Algorithm::A128GCM) 37 | .build(), 38 | concat!( 39 | "84", // 4-tuple 40 | "01", // int : AES-128-GCM 41 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 42 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 43 | "82", "0040", // 2-tuple: [0, 0-bstr] 44 | ), 45 | ), 46 | ( 47 | CoseKdfContextBuilder::new() 48 | .algorithm(iana::Algorithm::A128GCM) 49 | .party_u_info(PartyInfoBuilder::new().identity(vec![]).build()) 50 | .build(), 51 | concat!( 52 | "84", // 4-tuple 53 | "01", // int : AES-128-GCM 54 | "83", "40f6f6", // 3-tuple: [0-bstr, nil, nil] 55 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 56 | "82", "0040", // 2-tuple: [0, 0-bstr] 57 | ), 58 | ), 59 | ( 60 | CoseKdfContextBuilder::new() 61 | .algorithm(iana::Algorithm::A128GCM) 62 | .party_u_info( 63 | PartyInfoBuilder::new() 64 | .identity(vec![3, 6]) 65 | .nonce(Nonce::Integer(7)) 66 | .build(), 67 | ) 68 | .build(), 69 | concat!( 70 | "84", // 4-tuple 71 | "01", // int : AES-128-GCM 72 | "83", "420306", "07f6", // 3-tuple: [2-bstr, int, nil] 73 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 74 | "82", "0040", // 2-tuple: [0, 0-bstr] 75 | ), 76 | ), 77 | ( 78 | CoseKdfContextBuilder::new() 79 | .algorithm(iana::Algorithm::A128GCM) 80 | .party_u_info( 81 | PartyInfoBuilder::new() 82 | .identity(vec![3, 6]) 83 | .nonce(Nonce::Integer(-2)) 84 | .build(), 85 | ) 86 | .build(), 87 | concat!( 88 | "84", // 4-tuple 89 | "01", // int : AES-128-GCM 90 | "83", "420306", "21f6", // 3-tuple: [2-bstr, nint, nil] 91 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 92 | "82", "0040", // 2-tuple: [0, 0-bstr] 93 | ), 94 | ), 95 | ( 96 | CoseKdfContextBuilder::new() 97 | .algorithm(iana::Algorithm::A128GCM) 98 | .party_v_info( 99 | PartyInfoBuilder::new() 100 | .identity(vec![3, 6]) 101 | .nonce(Nonce::Bytes(vec![7, 3])) 102 | .build(), 103 | ) 104 | .build(), 105 | concat!( 106 | "84", // 4-tuple 107 | "01", // int : AES-128-GCM 108 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 109 | "83", "420306", "420703", "f6", // 3-tuple: [2-bstr, 2-bstr, nil] 110 | "82", "0040", // 2-tuple: [0, 0-bstr] 111 | ), 112 | ), 113 | ( 114 | CoseKdfContextBuilder::new() 115 | .algorithm(iana::Algorithm::A128GCM) 116 | .party_v_info( 117 | PartyInfoBuilder::new() 118 | .identity(vec![3, 6]) 119 | .other(vec![7, 3]) 120 | .build(), 121 | ) 122 | .build(), 123 | concat!( 124 | "84", // 4-tuple 125 | "01", // int : AES-128-GCM 126 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 127 | "83", "420306", "f6", "420703", // 3-tuple: [2-bstr, nil, 2-bstr] 128 | "82", "0040", // 2-tuple: [0, 0-bstr] 129 | ), 130 | ), 131 | ( 132 | CoseKdfContextBuilder::new() 133 | .supp_pub_info( 134 | SuppPubInfoBuilder::new() 135 | .key_data_length(10) 136 | .protected( 137 | HeaderBuilder::new() 138 | .algorithm(iana::Algorithm::A128GCM) 139 | .build(), 140 | ) 141 | .build(), 142 | ) 143 | .build(), 144 | concat!( 145 | "84", // 4-tuple 146 | "00", // int : reserved 147 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 148 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 149 | "82", "0a43", "a10101" // 2-tuple: [10, 3-bstr] 150 | ), 151 | ), 152 | ( 153 | CoseKdfContextBuilder::new() 154 | .supp_pub_info( 155 | SuppPubInfoBuilder::new() 156 | .key_data_length(10) 157 | .other(vec![1, 3, 5]) 158 | .build(), 159 | ) 160 | .build(), 161 | concat!( 162 | "84", // 4-tuple 163 | "00", // int : reserved 164 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 165 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 166 | "83", "0a40", "43010305", // 3-tuple: [10, 0-bstr, 3-bstr] 167 | ), 168 | ), 169 | ( 170 | CoseKdfContextBuilder::new() 171 | .add_supp_priv_info(vec![1, 2, 3]) 172 | .build(), 173 | concat!( 174 | "85", // 5-tuple 175 | "00", // int : reserved 176 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 177 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 178 | "82", "0040", // 2-tuple: [0, 0-bstr] 179 | "43", "010203", // 3-bstr 180 | ), 181 | ), 182 | ( 183 | CoseKdfContextBuilder::new() 184 | .add_supp_priv_info(vec![1, 2, 3]) 185 | .add_supp_priv_info(vec![2, 3]) 186 | .add_supp_priv_info(vec![3]) 187 | .build(), 188 | concat!( 189 | "87", // 7-tuple 190 | "00", // int : reserved 191 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 192 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 193 | "82", "0040", // 2-tuple: [0, 0-bstr] 194 | "43", "010203", // 3-bstr 195 | "42", "0203", // 2-bstr 196 | "41", "03", // 1-bstr 197 | ), 198 | ), 199 | ]; 200 | for (i, (key, key_data)) in tests.iter().enumerate() { 201 | let got = key.clone().to_vec().unwrap(); 202 | assert_eq!(*key_data, hex::encode(&got), "case {}", i); 203 | 204 | let mut got = CoseKdfContext::from_slice(&got).unwrap(); 205 | got.supp_pub_info.protected.original_data = None; 206 | assert_eq!(*key, got); 207 | } 208 | } 209 | 210 | #[test] 211 | fn test_context_decode_fail() { 212 | let tests = vec![ 213 | ( 214 | concat!( 215 | "a2", // 2-map 216 | "00", // int : reserved 217 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 218 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 219 | "82", "0040", // 2-tuple: [0, 0-bstr] 220 | ), 221 | "expected array", 222 | ), 223 | ( 224 | concat!( 225 | "83", // 3-tuple 226 | "00", // int : reserved 227 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 228 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 229 | ), 230 | "expected array with at least 4 items", 231 | ), 232 | ( 233 | concat!( 234 | "84", // 4-tuple 235 | "00", // int : reserved 236 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 237 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 238 | ), 239 | "decode CBOR failure: Io(EndOfFile", 240 | ), 241 | ( 242 | concat!( 243 | "84", // 4-tuple 244 | "08", // int : unassigned value 245 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 246 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 247 | "82", "0040", // 2-tuple: [0, 0-bstr] 248 | ), 249 | "expected value in IANA or private use range", 250 | ), 251 | ( 252 | concat!( 253 | "84", // 4-tuple 254 | "40", // 0-bstr : invalid 255 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 256 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 257 | "82", "0040", // 2-tuple: [0, 0-bstr] 258 | ), 259 | "expected int/tstr", 260 | ), 261 | ( 262 | concat!( 263 | "84", // 4-tuple 264 | "00", // int : reserved 265 | "a1", "f6f6", // 1-map 266 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 267 | "82", "0040", // 2-tuple: [0, 0-bstr] 268 | ), 269 | "expected array", 270 | ), 271 | ( 272 | concat!( 273 | "84", // 4-tuple 274 | "00", // int : reserved 275 | "84", "f6f6f6f6", // 4-tuple: [nil, nil, nil, nil] 276 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 277 | "82", "0040", // 2-tuple: [0, 0-bstr] 278 | ), 279 | "expected array with 3 items", 280 | ), 281 | ( 282 | concat!( 283 | "84", // 4-tuple 284 | "00", // int : reserved 285 | "83", "f660f6", // 3-tuple: [nil, 0-tstr, nil] 286 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 287 | "82", "0040", // 2-tuple: [0, 0-bstr] 288 | ), 289 | "expected bstr / int / nil", 290 | ), 291 | ( 292 | concat!( 293 | "84", // 4-tuple 294 | "00", // int : reserved 295 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 296 | "83", "f6f660", // 3-tuple: [nil, nil, 0-tstr] 297 | "82", "0040", // 2-tuple: [0, 0-bstr] 298 | ), 299 | "expected bstr / nil", 300 | ), 301 | ( 302 | concat!( 303 | "84", // 4-tuple 304 | "00", // int : reserved 305 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 306 | "83", "60f6f6", // 3-tuple: [0-tstr, nil, nil] 307 | "82", "0040", // 2-tuple: [0, 0-bstr] 308 | ), 309 | "expected bstr / nil", 310 | ), 311 | ( 312 | concat!( 313 | "84", // 4-tuple 314 | "00", // int : reserved 315 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 316 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 317 | "a1", "0040", // 1-map: {0: 0-bstr} 318 | ), 319 | "expected array", 320 | ), 321 | ( 322 | concat!( 323 | "84", // 4-tuple 324 | "00", // int : reserved 325 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 326 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 327 | "81", "00", // 2-tuple: [0] 328 | ), 329 | "expected array with 2 or 3 items", 330 | ), 331 | ( 332 | concat!( 333 | "84", // 4-tuple 334 | "00", // int : reserved 335 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 336 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 337 | "82", "4040", // 2-tuple: [0-bstr, 0-bstr] 338 | ), 339 | "expected int", 340 | ), 341 | ( 342 | concat!( 343 | "84", // 4-tuple 344 | "00", // int : reserved 345 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 346 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 347 | "82", "0060", // 2-tuple: [0, 0-tstr] 348 | ), 349 | "expected bstr", 350 | ), 351 | ( 352 | concat!( 353 | "84", // 4-tuple 354 | "00", // int : reserved 355 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 356 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 357 | "83", "004060", // 3-tuple: [0, 0-bstr, 0-tstr] 358 | ), 359 | "expected bstr", 360 | ), 361 | ( 362 | concat!( 363 | "85", // 5-tuple 364 | "00", // int : reserved 365 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 366 | "83", "f6f6f6", // 3-tuple: [nil, nil, nil] 367 | "82", "0040", // 2-tuple: [0, 0-bstr] 368 | "60", // 0-tstr 369 | ), 370 | "expected bstr", 371 | ), 372 | ( 373 | concat!( 374 | "84", // 4-tuple 375 | "01", // int : AES-128-GCM 376 | "83", // 3-tuple: [0-bstr, out-of-range int, nil] 377 | "401b8000000000000000f6", 378 | "83", // 3-tuple: [nil, nil, nil] 379 | "f6f6f6", 380 | "82", // 2-tuple: [0, 0-bstr] 381 | "0040", 382 | ), 383 | "out of range integer value", 384 | ), 385 | ]; 386 | for (context_data, err_msg) in tests.iter() { 387 | let data = hex::decode(context_data).unwrap(); 388 | let result = CoseKdfContext::from_slice(&data); 389 | expect_err(result, err_msg); 390 | } 391 | } 392 | -------------------------------------------------------------------------------- /src/cwt/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! CBOR Web Token functionality. 18 | 19 | use crate::{ 20 | cbor::value::Value, 21 | common::AsCborValue, 22 | iana, 23 | iana::{EnumI64, WithPrivateRange}, 24 | util::{cbor_type_error, ValueTryAs}, 25 | CoseError, 26 | }; 27 | use alloc::{collections::BTreeSet, string::String, vec::Vec}; 28 | use core::convert::TryInto; 29 | 30 | #[cfg(test)] 31 | mod tests; 32 | 33 | /// Number of seconds since UNIX epoch. 34 | #[derive(Clone, Debug, PartialEq)] 35 | pub enum Timestamp { 36 | WholeSeconds(i64), 37 | FractionalSeconds(f64), 38 | } 39 | 40 | impl AsCborValue for Timestamp { 41 | fn from_cbor_value(value: Value) -> Result { 42 | match value { 43 | Value::Integer(i) => Ok(Timestamp::WholeSeconds(i.try_into()?)), 44 | Value::Float(f) => Ok(Timestamp::FractionalSeconds(f)), 45 | _ => cbor_type_error(&value, "int/float"), 46 | } 47 | } 48 | fn to_cbor_value(self) -> Result { 49 | Ok(match self { 50 | Timestamp::WholeSeconds(t) => Value::Integer(t.into()), 51 | Timestamp::FractionalSeconds(f) => Value::Float(f), 52 | }) 53 | } 54 | } 55 | 56 | /// Claim name. 57 | pub type ClaimName = crate::RegisteredLabelWithPrivate; 58 | 59 | /// Structure representing a CWT Claims Set. 60 | #[derive(Clone, Debug, Default, PartialEq)] 61 | pub struct ClaimsSet { 62 | /// Issuer 63 | pub issuer: Option, 64 | /// Subject 65 | pub subject: Option, 66 | /// Audience 67 | pub audience: Option, 68 | /// Expiration Time 69 | pub expiration_time: Option, 70 | /// Not Before 71 | pub not_before: Option, 72 | /// Issued At 73 | pub issued_at: Option, 74 | /// CWT ID 75 | pub cwt_id: Option>, 76 | /// Any additional claims. 77 | pub rest: Vec<(ClaimName, Value)>, 78 | } 79 | 80 | impl crate::CborSerializable for ClaimsSet {} 81 | 82 | const ISS: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Iss); 83 | const SUB: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Sub); 84 | const AUD: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Aud); 85 | const EXP: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Exp); 86 | const NBF: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Nbf); 87 | const IAT: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Iat); 88 | const CTI: ClaimName = ClaimName::Assigned(iana::CwtClaimName::Cti); 89 | 90 | impl AsCborValue for ClaimsSet { 91 | fn from_cbor_value(value: Value) -> Result { 92 | let m = match value { 93 | Value::Map(m) => m, 94 | v => return cbor_type_error(&v, "map"), 95 | }; 96 | 97 | let mut claims = Self::default(); 98 | let mut seen = BTreeSet::new(); 99 | for (n, value) in m.into_iter() { 100 | // The `ciborium` CBOR library does not police duplicate map keys, so do it here. 101 | let name = ClaimName::from_cbor_value(n)?; 102 | if seen.contains(&name) { 103 | return Err(CoseError::DuplicateMapKey); 104 | } 105 | seen.insert(name.clone()); 106 | match name { 107 | x if x == ISS => claims.issuer = Some(value.try_as_string()?), 108 | x if x == SUB => claims.subject = Some(value.try_as_string()?), 109 | x if x == AUD => claims.audience = Some(value.try_as_string()?), 110 | x if x == EXP => claims.expiration_time = Some(Timestamp::from_cbor_value(value)?), 111 | x if x == NBF => claims.not_before = Some(Timestamp::from_cbor_value(value)?), 112 | x if x == IAT => claims.issued_at = Some(Timestamp::from_cbor_value(value)?), 113 | x if x == CTI => claims.cwt_id = Some(value.try_as_bytes()?), 114 | name => claims.rest.push((name, value)), 115 | } 116 | } 117 | Ok(claims) 118 | } 119 | 120 | fn to_cbor_value(self) -> Result { 121 | let mut map = Vec::new(); 122 | if let Some(iss) = self.issuer { 123 | map.push((ISS.to_cbor_value()?, Value::Text(iss))); 124 | } 125 | if let Some(sub) = self.subject { 126 | map.push((SUB.to_cbor_value()?, Value::Text(sub))); 127 | } 128 | if let Some(aud) = self.audience { 129 | map.push((AUD.to_cbor_value()?, Value::Text(aud))); 130 | } 131 | if let Some(exp) = self.expiration_time { 132 | map.push((EXP.to_cbor_value()?, exp.to_cbor_value()?)); 133 | } 134 | if let Some(nbf) = self.not_before { 135 | map.push((NBF.to_cbor_value()?, nbf.to_cbor_value()?)); 136 | } 137 | if let Some(iat) = self.issued_at { 138 | map.push((IAT.to_cbor_value()?, iat.to_cbor_value()?)); 139 | } 140 | if let Some(cti) = self.cwt_id { 141 | map.push((CTI.to_cbor_value()?, Value::Bytes(cti))); 142 | } 143 | for (label, value) in self.rest { 144 | map.push((label.to_cbor_value()?, value)); 145 | } 146 | Ok(Value::Map(map)) 147 | } 148 | } 149 | 150 | /// Builder for [`ClaimsSet`] objects. 151 | #[derive(Default)] 152 | pub struct ClaimsSetBuilder(ClaimsSet); 153 | 154 | impl ClaimsSetBuilder { 155 | builder! {ClaimsSet} 156 | builder_set_optional! {issuer: String} 157 | builder_set_optional! {subject: String} 158 | builder_set_optional! {audience: String} 159 | builder_set_optional! {expiration_time: Timestamp} 160 | builder_set_optional! {not_before: Timestamp} 161 | builder_set_optional! {issued_at: Timestamp} 162 | builder_set_optional! {cwt_id: Vec} 163 | 164 | /// Set a claim name:value pair. 165 | /// 166 | /// # Panics 167 | /// 168 | /// This function will panic if it used to set a claim with name from the range [1, 7]. 169 | #[must_use] 170 | pub fn claim(mut self, name: iana::CwtClaimName, value: Value) -> Self { 171 | if name.to_i64() >= iana::CwtClaimName::Iss.to_i64() 172 | && name.to_i64() <= iana::CwtClaimName::Cti.to_i64() 173 | { 174 | panic!("claim() method used to set core claim"); // safe: invalid input 175 | } 176 | self.0.rest.push((ClaimName::Assigned(name), value)); 177 | self 178 | } 179 | 180 | /// Set a claim name:value pair where the `name` is text. 181 | #[must_use] 182 | pub fn text_claim(mut self, name: String, value: Value) -> Self { 183 | self.0.rest.push((ClaimName::Text(name), value)); 184 | self 185 | } 186 | 187 | /// Set a claim where the claim key is a numeric value from the private use range. 188 | /// 189 | /// # Panics 190 | /// 191 | /// This function will panic if it is used to set a claim with a key value outside of the 192 | /// private use range. 193 | #[must_use] 194 | pub fn private_claim(mut self, id: i64, value: Value) -> Self { 195 | assert!(iana::CwtClaimName::is_private(id)); 196 | self.0.rest.push((ClaimName::PrivateUse(id), value)); 197 | self 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/cwt/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | use super::*; 18 | use crate::{cbor::value::Value, iana, iana::WithPrivateRange, util::expect_err, CborSerializable}; 19 | use alloc::{borrow::ToOwned, vec}; 20 | 21 | #[test] 22 | fn test_cwt_encode() { 23 | let tests = vec![ 24 | ( 25 | ClaimsSet { 26 | issuer: Some("abc".to_owned()), 27 | ..Default::default() 28 | }, 29 | concat!( 30 | "a1", // 1-map 31 | "01", "63", "616263" // 1 (iss) => 3-tstr 32 | ), 33 | ), 34 | (ClaimsSetBuilder::new().build(), concat!("a0")), 35 | ( 36 | ClaimsSetBuilder::new() 37 | .issuer("aaa".to_owned()) 38 | .subject("bb".to_owned()) 39 | .audience("c".to_owned()) 40 | .expiration_time(Timestamp::WholeSeconds(0x100)) 41 | .not_before(Timestamp::WholeSeconds(0x200)) 42 | .issued_at(Timestamp::WholeSeconds(0x10)) 43 | .cwt_id(vec![1, 2, 3, 4]) 44 | .private_claim(-70_000, Value::Integer(0.into())) 45 | .build(), 46 | concat!( 47 | "a8", // 8-map 48 | "01", 49 | "63", 50 | "616161", // 1 (iss) => 3-tstr 51 | "02", 52 | "62", 53 | "6262", // 2 (sub) => 2-tstr 54 | "03", 55 | "61", 56 | "63", // 3 (aud) => 1-tstr 57 | "04", 58 | "19", 59 | "0100", // 4 (exp) => uint 60 | "05", 61 | "19", 62 | "0200", // 5 (nbf) => uint 63 | "06", 64 | "10", // 6 (iat) => uint 65 | "07", 66 | "44", 67 | "01020304", // 7 => bstr 68 | "3a0001116f", 69 | "00" // -70000 => uint 70 | ), 71 | ), 72 | ( 73 | ClaimsSetBuilder::new() 74 | .claim( 75 | iana::CwtClaimName::Cnf, 76 | Value::Map(vec![(Value::Integer(0.into()), Value::Integer(0.into()))]), 77 | ) 78 | .build(), 79 | concat!( 80 | "a1", // 1-map 81 | "08", "a1", "00", "00" 82 | ), 83 | ), 84 | ( 85 | ClaimsSetBuilder::new() 86 | .text_claim("aa".to_owned(), Value::Integer(0.into())) 87 | .build(), 88 | concat!( 89 | "a1", // 1-map 90 | "62", "6161", "00", 91 | ), 92 | ), 93 | ( 94 | ClaimsSetBuilder::new() 95 | .expiration_time(Timestamp::FractionalSeconds(1.5)) 96 | .build(), 97 | concat!( 98 | "a1", // 1-map 99 | "04", // 4 (exp) => 100 | // Note: ciborium serializes floats as the smallest float type that 101 | // will parse back to the original f64! As a result, 1.5 is encoded 102 | // as an f16. 103 | "f9", "3e00", 104 | ), 105 | ), 106 | ]; 107 | for (i, (claims, claims_data)) in tests.iter().enumerate() { 108 | let got = claims.clone().to_vec().unwrap(); 109 | assert_eq!(*claims_data, hex::encode(&got), "case {}", i); 110 | 111 | let got = ClaimsSet::from_slice(&got).unwrap(); 112 | assert_eq!(*claims, got); 113 | } 114 | } 115 | 116 | #[test] 117 | fn test_cwt_decode_fail() { 118 | let tests = vec![ 119 | ( 120 | concat!( 121 | "81", // 1-arr 122 | "01", 123 | ), 124 | "expected map", 125 | ), 126 | ( 127 | concat!( 128 | "a1", // 1-map 129 | "01", "08", // 1 (iss) => int (invalid value type) 130 | ), 131 | "expected tstr", 132 | ), 133 | ( 134 | concat!( 135 | "a1", // 1-map 136 | "02", "08", // 2 (sub) => int (invalid value type) 137 | ), 138 | "expected tstr", 139 | ), 140 | ( 141 | concat!( 142 | "a1", // 1-map 143 | "03", "08", // 3 (aud) => int (invalid value type) 144 | ), 145 | "expected tstr", 146 | ), 147 | ( 148 | concat!( 149 | "a1", // 1-map 150 | "04", "40", // 4 (exp) => bstr (invalid value type) 151 | ), 152 | "expected int/float", 153 | ), 154 | ( 155 | concat!( 156 | "a1", // 1-map 157 | "05", "40", // 5 (nbf) => bstr (invalid value type) 158 | ), 159 | "expected int/float", 160 | ), 161 | ( 162 | concat!( 163 | "a1", // 1-map 164 | "06", "40", // 6 (iat) => bstr (invalid value type) 165 | ), 166 | "expected int/float", 167 | ), 168 | ( 169 | concat!( 170 | "a1", // 1-map 171 | "07", "01", // 5 (cti) => uint (invalid value type) 172 | ), 173 | "expected bstr", 174 | ), 175 | ( 176 | concat!( 177 | "a1", // 1-map 178 | "07", "40", // 5 (cti) => 0-bstr 179 | "06", "01", // 6 (iat) => 1 180 | ), 181 | "extraneous data", 182 | ), 183 | ( 184 | concat!( 185 | "a2", // 1-map 186 | "07", "40", // 5 (cti) => 0-bstr 187 | "07", "40", // 5 (cti) => 0-bstr 188 | ), 189 | "duplicate map key", 190 | ), 191 | ]; 192 | for (claims_data, err_msg) in tests.iter() { 193 | let data = hex::decode(claims_data).unwrap(); 194 | let result = ClaimsSet::from_slice(&data); 195 | expect_err(result, err_msg); 196 | } 197 | } 198 | 199 | #[test] 200 | fn test_cwt_is_private() { 201 | assert!(!iana::CwtClaimName::is_private(1)); 202 | assert!(iana::CwtClaimName::is_private(-500_000)); 203 | } 204 | 205 | #[test] 206 | #[should_panic] 207 | fn test_cwt_claims_builder_core_param_panic() { 208 | // Attempting to set a core claim (in range [1,7]) via `.claim()` panics. 209 | let _claims = ClaimsSetBuilder::new() 210 | .claim(iana::CwtClaimName::Iss, Value::Null) 211 | .build(); 212 | } 213 | 214 | #[test] 215 | #[should_panic] 216 | fn test_cwt_claims_builder_non_private_panic() { 217 | // Attempting to set a claim outside of private range via `.private_claim()` panics. 218 | let _claims = ClaimsSetBuilder::new() 219 | .private_claim(100, Value::Null) 220 | .build(); 221 | } 222 | 223 | #[test] 224 | fn test_cwt_dup_claim() { 225 | // Set a duplicate map key. 226 | let claims = ClaimsSetBuilder::new() 227 | .claim(iana::CwtClaimName::AceProfile, Value::Integer(1.into())) 228 | .claim(iana::CwtClaimName::AceProfile, Value::Integer(2.into())) 229 | .build(); 230 | // Encoding succeeds. 231 | let data = claims.to_vec().unwrap(); 232 | // But an attempt to parse the encoded data fails. 233 | let result = ClaimsSet::from_slice(&data); 234 | expect_err(result, "duplicate map key"); 235 | } 236 | -------------------------------------------------------------------------------- /src/encrypt/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! COSE_Encrypt functionality. 18 | 19 | use crate::{ 20 | cbor, 21 | cbor::value::Value, 22 | common::AsCborValue, 23 | iana, 24 | util::{cbor_type_error, to_cbor_array, ValueTryAs}, 25 | CoseError, Header, ProtectedHeader, Result, 26 | }; 27 | use alloc::{borrow::ToOwned, vec, vec::Vec}; 28 | 29 | #[cfg(test)] 30 | mod tests; 31 | 32 | /// Structure representing the recipient of encrypted data. 33 | /// 34 | /// ```cddl 35 | /// COSE_Recipient = [ 36 | /// Headers, 37 | /// ciphertext : bstr / nil, 38 | /// ? recipients : [+COSE_recipient] 39 | /// ] 40 | /// ``` 41 | #[derive(Clone, Debug, Default, PartialEq)] 42 | pub struct CoseRecipient { 43 | pub protected: ProtectedHeader, 44 | pub unprotected: Header, 45 | pub ciphertext: Option>, 46 | pub recipients: Vec, 47 | } 48 | 49 | impl crate::CborSerializable for CoseRecipient {} 50 | 51 | impl AsCborValue for CoseRecipient { 52 | fn from_cbor_value(value: Value) -> Result { 53 | let mut a = value.try_as_array()?; 54 | if a.len() != 3 && a.len() != 4 { 55 | return Err(CoseError::UnexpectedItem( 56 | "array", 57 | "array with 3 or 4 items", 58 | )); 59 | } 60 | 61 | // Remove array elements in reverse order to avoid shifts. 62 | let recipients = if a.len() == 4 { 63 | a.remove(3) 64 | .try_as_array_then_convert(CoseRecipient::from_cbor_value)? 65 | } else { 66 | Vec::new() 67 | }; 68 | 69 | Ok(Self { 70 | recipients, 71 | ciphertext: match a.remove(2) { 72 | Value::Bytes(b) => Some(b), 73 | Value::Null => None, 74 | v => return cbor_type_error(&v, "bstr / null"), 75 | }, 76 | unprotected: Header::from_cbor_value(a.remove(1))?, 77 | protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?, 78 | }) 79 | } 80 | 81 | fn to_cbor_value(self) -> Result { 82 | let mut v = vec![ 83 | self.protected.cbor_bstr()?, 84 | self.unprotected.to_cbor_value()?, 85 | match self.ciphertext { 86 | None => Value::Null, 87 | Some(b) => Value::Bytes(b), 88 | }, 89 | ]; 90 | if !self.recipients.is_empty() { 91 | v.push(to_cbor_array(self.recipients)?); 92 | } 93 | Ok(Value::Array(v)) 94 | } 95 | } 96 | 97 | impl CoseRecipient { 98 | /// Decrypt the `ciphertext` value with an AEAD, using `cipher` to decrypt the cipher text and 99 | /// combined AAD as per RFC 8152 section 5.3. 100 | /// 101 | /// # Panics 102 | /// 103 | /// This function will panic if no `ciphertext` is available. It will also panic 104 | /// if the `context` parameter does not refer to a recipient context. 105 | pub fn decrypt( 106 | &self, 107 | context: EncryptionContext, 108 | external_aad: &[u8], 109 | cipher: F, 110 | ) -> Result, E> 111 | where 112 | F: FnOnce(&[u8], &[u8]) -> Result, E>, 113 | { 114 | let ct = self.ciphertext.as_ref().unwrap(/* safe: documented */); 115 | match context { 116 | EncryptionContext::EncRecipient 117 | | EncryptionContext::MacRecipient 118 | | EncryptionContext::RecRecipient => {} 119 | _ => panic!("unsupported encryption context {:?}", context), // safe: documented 120 | } 121 | let aad = enc_structure_data(context, self.protected.clone(), external_aad); 122 | cipher(ct, &aad) 123 | } 124 | } 125 | 126 | /// Builder for [`CoseRecipient`] objects. 127 | #[derive(Debug, Default)] 128 | pub struct CoseRecipientBuilder(CoseRecipient); 129 | 130 | impl CoseRecipientBuilder { 131 | builder! {CoseRecipient} 132 | builder_set_protected! {protected} 133 | builder_set! {unprotected: Header} 134 | builder_set_optional! {ciphertext: Vec} 135 | 136 | /// Add a [`CoseRecipient`]. 137 | #[must_use] 138 | pub fn add_recipient(mut self, recipient: CoseRecipient) -> Self { 139 | self.0.recipients.push(recipient); 140 | self 141 | } 142 | 143 | /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes 144 | /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. Any 145 | /// protected header values should be set before using this method. 146 | /// 147 | /// # Panics 148 | /// 149 | /// This function will panic if the `context` parameter does not refer to a recipient context. 150 | #[must_use] 151 | pub fn create_ciphertext( 152 | self, 153 | context: EncryptionContext, 154 | plaintext: &[u8], 155 | external_aad: &[u8], 156 | cipher: F, 157 | ) -> Self 158 | where 159 | F: FnOnce(&[u8], &[u8]) -> Vec, 160 | { 161 | let aad = self.aad(context, external_aad); 162 | self.ciphertext(cipher(plaintext, &aad)) 163 | } 164 | 165 | /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes 166 | /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. Any 167 | /// protected header values should be set before using this method. 168 | /// 169 | /// # Panics 170 | /// 171 | /// This function will panic if the `context` parameter does not refer to a recipient context. 172 | pub fn try_create_ciphertext( 173 | self, 174 | context: EncryptionContext, 175 | plaintext: &[u8], 176 | external_aad: &[u8], 177 | cipher: F, 178 | ) -> Result 179 | where 180 | F: FnOnce(&[u8], &[u8]) -> Result, E>, 181 | { 182 | let aad = self.aad(context, external_aad); 183 | Ok(self.ciphertext(cipher(plaintext, &aad)?)) 184 | } 185 | 186 | /// Construct the combined AAD data needed for encryption with an AEAD. Any protected header 187 | /// values should be set before using this method. 188 | /// 189 | /// # Panics 190 | /// 191 | /// This function will panic if the `context` parameter does not refer to a recipient context. 192 | #[must_use] 193 | fn aad(&self, context: EncryptionContext, external_aad: &[u8]) -> Vec { 194 | match context { 195 | EncryptionContext::EncRecipient 196 | | EncryptionContext::MacRecipient 197 | | EncryptionContext::RecRecipient => {} 198 | _ => panic!("unsupported encryption context {:?}", context), // safe: documented 199 | } 200 | enc_structure_data(context, self.0.protected.clone(), external_aad) 201 | } 202 | } 203 | 204 | /// Structure representing an encrypted object. 205 | /// 206 | /// ```cddl 207 | /// COSE_Encrypt = [ 208 | /// Headers, 209 | /// ciphertext : bstr / nil, 210 | /// recipients : [+COSE_recipient] 211 | /// ] 212 | /// ``` 213 | #[derive(Clone, Debug, Default, PartialEq)] 214 | pub struct CoseEncrypt { 215 | pub protected: ProtectedHeader, 216 | pub unprotected: Header, 217 | pub ciphertext: Option>, 218 | pub recipients: Vec, 219 | } 220 | 221 | impl crate::CborSerializable for CoseEncrypt {} 222 | 223 | impl crate::TaggedCborSerializable for CoseEncrypt { 224 | const TAG: u64 = iana::CborTag::CoseEncrypt as u64; 225 | } 226 | 227 | impl AsCborValue for CoseEncrypt { 228 | fn from_cbor_value(value: Value) -> Result { 229 | let mut a = value.try_as_array()?; 230 | if a.len() != 4 { 231 | return Err(CoseError::UnexpectedItem("array", "array with 4 items")); 232 | } 233 | 234 | // Remove array elements in reverse order to avoid shifts. 235 | let recipients = a 236 | .remove(3) 237 | .try_as_array_then_convert(CoseRecipient::from_cbor_value)?; 238 | Ok(Self { 239 | recipients, 240 | ciphertext: match a.remove(2) { 241 | Value::Bytes(b) => Some(b), 242 | Value::Null => None, 243 | v => return cbor_type_error(&v, "bstr"), 244 | }, 245 | unprotected: Header::from_cbor_value(a.remove(1))?, 246 | protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?, 247 | }) 248 | } 249 | 250 | fn to_cbor_value(self) -> Result { 251 | Ok(Value::Array(vec![ 252 | self.protected.cbor_bstr()?, 253 | self.unprotected.to_cbor_value()?, 254 | match self.ciphertext { 255 | None => Value::Null, 256 | Some(b) => Value::Bytes(b), 257 | }, 258 | to_cbor_array(self.recipients)?, 259 | ])) 260 | } 261 | } 262 | 263 | impl CoseEncrypt { 264 | /// Decrypt the `ciphertext` value with an AEAD, using `cipher` to decrypt the cipher text and 265 | /// combined AAD. 266 | /// 267 | /// # Panics 268 | /// 269 | /// This function will panic if no `ciphertext` is available. 270 | pub fn decrypt(&self, external_aad: &[u8], cipher: F) -> Result, E> 271 | where 272 | F: FnOnce(&[u8], &[u8]) -> Result, E>, 273 | { 274 | let ct = self.ciphertext.as_ref().unwrap(/* safe: documented */); 275 | let aad = enc_structure_data( 276 | EncryptionContext::CoseEncrypt, 277 | self.protected.clone(), 278 | external_aad, 279 | ); 280 | cipher(ct, &aad) 281 | } 282 | } 283 | 284 | /// Builder for [`CoseEncrypt`] objects. 285 | #[derive(Debug, Default)] 286 | pub struct CoseEncryptBuilder(CoseEncrypt); 287 | 288 | impl CoseEncryptBuilder { 289 | builder! {CoseEncrypt} 290 | builder_set_protected! {protected} 291 | builder_set! {unprotected: Header} 292 | builder_set_optional! {ciphertext: Vec} 293 | 294 | /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes 295 | /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. Any 296 | /// protected header values should be set before using this method. 297 | #[must_use] 298 | pub fn create_ciphertext(self, plaintext: &[u8], external_aad: &[u8], cipher: F) -> Self 299 | where 300 | F: FnOnce(&[u8], &[u8]) -> Vec, 301 | { 302 | let aad = enc_structure_data( 303 | EncryptionContext::CoseEncrypt, 304 | self.0.protected.clone(), 305 | external_aad, 306 | ); 307 | self.ciphertext(cipher(plaintext, &aad)) 308 | } 309 | 310 | /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes 311 | /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. Any 312 | /// protected header values should be set before using this method. 313 | pub fn try_create_ciphertext( 314 | self, 315 | plaintext: &[u8], 316 | external_aad: &[u8], 317 | cipher: F, 318 | ) -> Result 319 | where 320 | F: FnOnce(&[u8], &[u8]) -> Result, E>, 321 | { 322 | let aad = enc_structure_data( 323 | EncryptionContext::CoseEncrypt, 324 | self.0.protected.clone(), 325 | external_aad, 326 | ); 327 | Ok(self.ciphertext(cipher(plaintext, &aad)?)) 328 | } 329 | 330 | /// Add a [`CoseRecipient`]. 331 | #[must_use] 332 | pub fn add_recipient(mut self, recipient: CoseRecipient) -> Self { 333 | self.0.recipients.push(recipient); 334 | self 335 | } 336 | } 337 | 338 | /// Structure representing an encrypted object. 339 | /// 340 | /// ```cddl 341 | /// COSE_Encrypt0 = [ 342 | /// Headers, 343 | /// ciphertext : bstr / nil, 344 | /// ] 345 | /// ``` 346 | #[derive(Clone, Debug, Default, PartialEq)] 347 | pub struct CoseEncrypt0 { 348 | pub protected: ProtectedHeader, 349 | pub unprotected: Header, 350 | pub ciphertext: Option>, 351 | } 352 | 353 | impl crate::CborSerializable for CoseEncrypt0 {} 354 | 355 | impl crate::TaggedCborSerializable for CoseEncrypt0 { 356 | const TAG: u64 = iana::CborTag::CoseEncrypt0 as u64; 357 | } 358 | 359 | impl AsCborValue for CoseEncrypt0 { 360 | fn from_cbor_value(value: Value) -> Result { 361 | let mut a = value.try_as_array()?; 362 | if a.len() != 3 { 363 | return Err(CoseError::UnexpectedItem("array", "array with 3 items")); 364 | } 365 | 366 | // Remove array elements in reverse order to avoid shifts. 367 | Ok(Self { 368 | ciphertext: match a.remove(2) { 369 | Value::Bytes(b) => Some(b), 370 | Value::Null => None, 371 | v => return cbor_type_error(&v, "bstr"), 372 | }, 373 | 374 | unprotected: Header::from_cbor_value(a.remove(1))?, 375 | protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?, 376 | }) 377 | } 378 | 379 | fn to_cbor_value(self) -> Result { 380 | Ok(Value::Array(vec![ 381 | self.protected.cbor_bstr()?, 382 | self.unprotected.to_cbor_value()?, 383 | match self.ciphertext { 384 | None => Value::Null, 385 | Some(b) => Value::Bytes(b), 386 | }, 387 | ])) 388 | } 389 | } 390 | 391 | impl CoseEncrypt0 { 392 | /// Decrypt the `ciphertext` value with an AEAD, using `cipher` to decrypt the cipher text and 393 | /// combined AAD. 394 | /// 395 | /// # Panics 396 | /// 397 | /// This function will panic if no `ciphertext` is available. 398 | pub fn decrypt(&self, external_aad: &[u8], cipher: F) -> Result, E> 399 | where 400 | F: FnOnce(&[u8], &[u8]) -> Result, E>, 401 | { 402 | let ct = self.ciphertext.as_ref().unwrap(/* safe: documented */); 403 | let aad = enc_structure_data( 404 | EncryptionContext::CoseEncrypt0, 405 | self.protected.clone(), 406 | external_aad, 407 | ); 408 | cipher(ct, &aad) 409 | } 410 | } 411 | 412 | /// Builder for [`CoseEncrypt0`] objects. 413 | #[derive(Debug, Default)] 414 | pub struct CoseEncrypt0Builder(CoseEncrypt0); 415 | 416 | impl CoseEncrypt0Builder { 417 | builder! {CoseEncrypt0} 418 | builder_set_protected! {protected} 419 | builder_set! {unprotected: Header} 420 | builder_set_optional! {ciphertext: Vec} 421 | 422 | /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes 423 | /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. Any 424 | /// protected header values should be set before using this method. 425 | #[must_use] 426 | pub fn create_ciphertext(self, plaintext: &[u8], external_aad: &[u8], cipher: F) -> Self 427 | where 428 | F: FnOnce(&[u8], &[u8]) -> Vec, 429 | { 430 | let aad = enc_structure_data( 431 | EncryptionContext::CoseEncrypt0, 432 | self.0.protected.clone(), 433 | external_aad, 434 | ); 435 | self.ciphertext(cipher(plaintext, &aad)) 436 | } 437 | 438 | /// Calculate the ciphertext value with an AEAD, using `cipher` to generate the encrypted bytes 439 | /// from the plaintext and combined AAD (in that order) as per RFC 8152 section 5.3. Any 440 | /// protected header values should be set before using this method. 441 | pub fn try_create_ciphertext( 442 | self, 443 | plaintext: &[u8], 444 | external_aad: &[u8], 445 | cipher: F, 446 | ) -> Result 447 | where 448 | F: FnOnce(&[u8], &[u8]) -> Result, E>, 449 | { 450 | let aad = enc_structure_data( 451 | EncryptionContext::CoseEncrypt0, 452 | self.0.protected.clone(), 453 | external_aad, 454 | ); 455 | Ok(self.ciphertext(cipher(plaintext, &aad)?)) 456 | } 457 | } 458 | 459 | /// Possible encryption contexts. 460 | #[derive(Clone, Copy, Debug)] 461 | pub enum EncryptionContext { 462 | CoseEncrypt, 463 | CoseEncrypt0, 464 | EncRecipient, 465 | MacRecipient, 466 | RecRecipient, 467 | } 468 | 469 | impl EncryptionContext { 470 | /// Return the context string as per RFC 8152 section 5.3. 471 | fn text(&self) -> &'static str { 472 | match self { 473 | EncryptionContext::CoseEncrypt => "Encrypt", 474 | EncryptionContext::CoseEncrypt0 => "Encrypt0", 475 | EncryptionContext::EncRecipient => "Enc_Recipient", 476 | EncryptionContext::MacRecipient => "Mac_Recipient", 477 | EncryptionContext::RecRecipient => "Rec_Recipient", 478 | } 479 | } 480 | } 481 | 482 | /// Create a binary blob that will be signed. 483 | // 484 | /// ```cddl 485 | /// Enc_structure = [ 486 | /// context : "Encrypt" / "Encrypt0" / "Enc_Recipient" / 487 | /// "Mac_Recipient" / "Rec_Recipient", 488 | /// protected : empty_or_serialized_map, 489 | /// external_aad : bstr 490 | /// ] 491 | /// ``` 492 | pub fn enc_structure_data( 493 | context: EncryptionContext, 494 | protected: ProtectedHeader, 495 | external_aad: &[u8], 496 | ) -> Vec { 497 | let arr = vec![ 498 | Value::Text(context.text().to_owned()), 499 | protected.cbor_bstr().expect("failed to serialize header"), // safe: always serializable 500 | Value::Bytes(external_aad.to_vec()), 501 | ]; 502 | 503 | let mut data = Vec::new(); 504 | cbor::ser::into_writer(&Value::Array(arr), &mut data).unwrap(); // safe: always serializable 505 | data 506 | } 507 | -------------------------------------------------------------------------------- /src/header/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! COSE Headers functionality. 18 | 19 | use crate::{ 20 | cbor::value::Value, 21 | common::AsCborValue, 22 | iana, 23 | iana::EnumI64, 24 | util::{cbor_type_error, to_cbor_array, ValueTryAs}, 25 | Algorithm, CborSerializable, CoseError, CoseSignature, Label, RegisteredLabel, Result, 26 | }; 27 | use alloc::{collections::BTreeSet, string::String, vec, vec::Vec}; 28 | 29 | #[cfg(test)] 30 | mod tests; 31 | 32 | /// Content type. 33 | pub type ContentType = crate::RegisteredLabel; 34 | 35 | /// Structure representing a common COSE header map. 36 | /// 37 | /// ```cddl 38 | /// header_map = { 39 | /// Generic_Headers, 40 | /// * label => values 41 | /// } 42 | /// 43 | /// Generic_Headers = ( 44 | /// ? 1 => int / tstr, ; algorithm identifier 45 | /// ? 2 => [+label], ; criticality 46 | /// ? 3 => tstr / int, ; content type 47 | /// ? 4 => bstr, ; key identifier 48 | /// ? 5 => bstr, ; IV 49 | /// ? 6 => bstr, ; Partial IV 50 | /// ? 7 => COSE_Signature / [+COSE_Signature] ; Counter signature 51 | /// ) 52 | /// ``` 53 | #[derive(Clone, Debug, Default, PartialEq)] 54 | pub struct Header { 55 | /// Cryptographic algorithm to use 56 | pub alg: Option, 57 | /// Critical headers to be understood 58 | pub crit: Vec>, 59 | /// Content type of the payload 60 | pub content_type: Option, 61 | /// Key identifier. 62 | pub key_id: Vec, 63 | /// Full initialization vector 64 | pub iv: Vec, 65 | /// Partial initialization vector 66 | pub partial_iv: Vec, 67 | /// Counter signature 68 | pub counter_signatures: Vec, 69 | /// Any additional header (label,value) pairs. If duplicate labels are present, CBOR-encoding 70 | /// will fail. 71 | pub rest: Vec<(Label, Value)>, 72 | } 73 | 74 | impl Header { 75 | /// Indicate whether the `Header` is empty. 76 | pub fn is_empty(&self) -> bool { 77 | self.alg.is_none() 78 | && self.crit.is_empty() 79 | && self.content_type.is_none() 80 | && self.key_id.is_empty() 81 | && self.iv.is_empty() 82 | && self.partial_iv.is_empty() 83 | && self.counter_signatures.is_empty() 84 | && self.rest.is_empty() 85 | } 86 | } 87 | 88 | impl crate::CborSerializable for Header {} 89 | 90 | const ALG: Label = Label::Int(iana::HeaderParameter::Alg as i64); 91 | const CRIT: Label = Label::Int(iana::HeaderParameter::Crit as i64); 92 | const CONTENT_TYPE: Label = Label::Int(iana::HeaderParameter::ContentType as i64); 93 | const KID: Label = Label::Int(iana::HeaderParameter::Kid as i64); 94 | const IV: Label = Label::Int(iana::HeaderParameter::Iv as i64); 95 | const PARTIAL_IV: Label = Label::Int(iana::HeaderParameter::PartialIv as i64); 96 | const COUNTER_SIG: Label = Label::Int(iana::HeaderParameter::CounterSignature as i64); 97 | 98 | impl AsCborValue for Header { 99 | fn from_cbor_value(value: Value) -> Result { 100 | let m = value.try_as_map()?; 101 | let mut headers = Self::default(); 102 | let mut seen = BTreeSet::new(); 103 | for (l, value) in m.into_iter() { 104 | // The `ciborium` CBOR library does not police duplicate map keys. 105 | // RFC 8152 section 14 requires that COSE does police duplicates, so do it here. 106 | let label = Label::from_cbor_value(l)?; 107 | if seen.contains(&label) { 108 | return Err(CoseError::DuplicateMapKey); 109 | } 110 | seen.insert(label.clone()); 111 | match label { 112 | ALG => headers.alg = Some(Algorithm::from_cbor_value(value)?), 113 | 114 | CRIT => match value { 115 | Value::Array(a) => { 116 | if a.is_empty() { 117 | return Err(CoseError::UnexpectedItem( 118 | "empty array", 119 | "non-empty array", 120 | )); 121 | } 122 | for v in a { 123 | headers.crit.push( 124 | RegisteredLabel::::from_cbor_value(v)?, 125 | ); 126 | } 127 | } 128 | v => return cbor_type_error(&v, "array value"), 129 | }, 130 | 131 | CONTENT_TYPE => { 132 | headers.content_type = Some(ContentType::from_cbor_value(value)?); 133 | if let Some(ContentType::Text(text)) = &headers.content_type { 134 | if text.is_empty() { 135 | return Err(CoseError::UnexpectedItem("empty tstr", "non-empty tstr")); 136 | } 137 | if text.trim() != text { 138 | return Err(CoseError::UnexpectedItem( 139 | "leading/trailing whitespace", 140 | "no leading/trailing whitespace", 141 | )); 142 | } 143 | // Basic check that the content type is of form type/subtype. 144 | // We don't check the precise definition though (RFC 6838 s4.2) 145 | if text.matches('/').count() != 1 { 146 | return Err(CoseError::UnexpectedItem( 147 | "arbitrary text", 148 | "text of form type/subtype", 149 | )); 150 | } 151 | } 152 | } 153 | 154 | KID => { 155 | headers.key_id = value.try_as_nonempty_bytes()?; 156 | } 157 | 158 | IV => { 159 | headers.iv = value.try_as_nonempty_bytes()?; 160 | } 161 | 162 | PARTIAL_IV => { 163 | headers.partial_iv = value.try_as_nonempty_bytes()?; 164 | } 165 | COUNTER_SIG => { 166 | let sig_or_sigs = value.try_as_array()?; 167 | if sig_or_sigs.is_empty() { 168 | return Err(CoseError::UnexpectedItem( 169 | "empty sig array", 170 | "non-empty sig array", 171 | )); 172 | } 173 | // The encoding of counter signature[s] is pesky: 174 | // - a single counter signature is encoded as `COSE_Signature` (a 3-tuple) 175 | // - multiple counter signatures are encoded as `[+ COSE_Signature]` 176 | // 177 | // Determine which is which by looking at the first entry of the array: 178 | // - If it's a bstr, sig_or_sigs is a single signature. 179 | // - If it's an array, sig_or_sigs is an array of signatures 180 | match &sig_or_sigs[0] { 181 | Value::Bytes(_) => headers 182 | .counter_signatures 183 | .push(CoseSignature::from_cbor_value(Value::Array(sig_or_sigs))?), 184 | Value::Array(_) => { 185 | for sig in sig_or_sigs.into_iter() { 186 | headers 187 | .counter_signatures 188 | .push(CoseSignature::from_cbor_value(sig)?); 189 | } 190 | } 191 | v => return cbor_type_error(v, "array or bstr value"), 192 | } 193 | } 194 | 195 | label => headers.rest.push((label, value)), 196 | } 197 | // RFC 8152 section 3.1: "The 'Initialization Vector' and 'Partial Initialization 198 | // Vector' parameters MUST NOT both be present in the same security layer." 199 | if !headers.iv.is_empty() && !headers.partial_iv.is_empty() { 200 | return Err(CoseError::UnexpectedItem( 201 | "IV and partial-IV specified", 202 | "only one of IV and partial IV", 203 | )); 204 | } 205 | } 206 | Ok(headers) 207 | } 208 | 209 | fn to_cbor_value(mut self) -> Result { 210 | let mut map = Vec::<(Value, Value)>::new(); 211 | if let Some(alg) = self.alg { 212 | map.push((ALG.to_cbor_value()?, alg.to_cbor_value()?)); 213 | } 214 | if !self.crit.is_empty() { 215 | map.push((CRIT.to_cbor_value()?, to_cbor_array(self.crit)?)); 216 | } 217 | if let Some(content_type) = self.content_type { 218 | map.push((CONTENT_TYPE.to_cbor_value()?, content_type.to_cbor_value()?)); 219 | } 220 | if !self.key_id.is_empty() { 221 | map.push((KID.to_cbor_value()?, Value::Bytes(self.key_id))); 222 | } 223 | if !self.iv.is_empty() { 224 | map.push((IV.to_cbor_value()?, Value::Bytes(self.iv))); 225 | } 226 | if !self.partial_iv.is_empty() { 227 | map.push((PARTIAL_IV.to_cbor_value()?, Value::Bytes(self.partial_iv))); 228 | } 229 | if !self.counter_signatures.is_empty() { 230 | if self.counter_signatures.len() == 1 { 231 | // A single counter signature is encoded differently. 232 | map.push(( 233 | COUNTER_SIG.to_cbor_value()?, 234 | self.counter_signatures.remove(0).to_cbor_value()?, 235 | )); 236 | } else { 237 | map.push(( 238 | COUNTER_SIG.to_cbor_value()?, 239 | to_cbor_array(self.counter_signatures)?, 240 | )); 241 | } 242 | } 243 | let mut seen = BTreeSet::new(); 244 | for (label, value) in self.rest.into_iter() { 245 | if seen.contains(&label) { 246 | return Err(CoseError::DuplicateMapKey); 247 | } 248 | seen.insert(label.clone()); 249 | map.push((label.to_cbor_value()?, value)); 250 | } 251 | Ok(Value::Map(map)) 252 | } 253 | } 254 | 255 | /// Builder for [`Header`] objects. 256 | #[derive(Debug, Default)] 257 | pub struct HeaderBuilder(Header); 258 | 259 | impl HeaderBuilder { 260 | builder! {Header} 261 | builder_set! {key_id: Vec} 262 | 263 | /// Set the algorithm. 264 | #[must_use] 265 | pub fn algorithm(mut self, alg: iana::Algorithm) -> Self { 266 | self.0.alg = Some(Algorithm::Assigned(alg)); 267 | self 268 | } 269 | 270 | /// Add a critical header. 271 | #[must_use] 272 | pub fn add_critical(mut self, param: iana::HeaderParameter) -> Self { 273 | self.0.crit.push(RegisteredLabel::Assigned(param)); 274 | self 275 | } 276 | 277 | /// Add a critical header. 278 | #[must_use] 279 | pub fn add_critical_label(mut self, label: RegisteredLabel) -> Self { 280 | self.0.crit.push(label); 281 | self 282 | } 283 | 284 | /// Set the content type to a numeric value. 285 | #[must_use] 286 | pub fn content_format(mut self, content_type: iana::CoapContentFormat) -> Self { 287 | self.0.content_type = Some(ContentType::Assigned(content_type)); 288 | self 289 | } 290 | 291 | /// Set the content type to a text value. 292 | #[must_use] 293 | pub fn content_type(mut self, content_type: String) -> Self { 294 | self.0.content_type = Some(ContentType::Text(content_type)); 295 | self 296 | } 297 | 298 | /// Set the IV, and clear any partial IV already set. 299 | #[must_use] 300 | pub fn iv(mut self, iv: Vec) -> Self { 301 | self.0.iv = iv; 302 | self.0.partial_iv.clear(); 303 | self 304 | } 305 | 306 | /// Set the partial IV, and clear any IV already set. 307 | #[must_use] 308 | pub fn partial_iv(mut self, iv: Vec) -> Self { 309 | self.0.partial_iv = iv; 310 | self.0.iv.clear(); 311 | self 312 | } 313 | 314 | /// Add a counter signature. 315 | #[must_use] 316 | pub fn add_counter_signature(mut self, sig: CoseSignature) -> Self { 317 | self.0.counter_signatures.push(sig); 318 | self 319 | } 320 | 321 | /// Set a header label:value pair. If duplicate labels are added to a [`Header`], 322 | /// subsequent attempts to CBOR-encode the header will fail. 323 | /// 324 | /// # Panics 325 | /// 326 | /// This function will panic if it used to set a header label from the range [1, 6]. 327 | #[must_use] 328 | pub fn value(mut self, label: i64, value: Value) -> Self { 329 | if label >= iana::HeaderParameter::Alg.to_i64() 330 | && label <= iana::HeaderParameter::CounterSignature.to_i64() 331 | { 332 | panic!("value() method used to set core header parameter"); // safe: invalid input 333 | } 334 | self.0.rest.push((Label::Int(label), value)); 335 | self 336 | } 337 | 338 | /// Set a header label:value pair where the `label` is text. 339 | #[must_use] 340 | pub fn text_value(mut self, label: String, value: Value) -> Self { 341 | self.0.rest.push((Label::Text(label), value)); 342 | self 343 | } 344 | } 345 | 346 | /// Structure representing a protected COSE header map. 347 | #[derive(Clone, Debug, Default, PartialEq)] 348 | pub struct ProtectedHeader { 349 | /// If this structure was created by parsing serialized data, this field 350 | /// holds the entire contents of the original `bstr` data. 351 | pub original_data: Option>, 352 | /// Parsed header information. 353 | pub header: Header, 354 | } 355 | 356 | impl ProtectedHeader { 357 | /// Constructor from a [`Value`] that holds a `bstr` encoded header. 358 | #[inline] 359 | pub fn from_cbor_bstr(val: Value) -> Result { 360 | let data = val.try_as_bytes()?; 361 | let header = if data.is_empty() { 362 | // An empty bstr is used as a short cut for an empty header map. 363 | Header::default() 364 | } else { 365 | Header::from_slice(&data)? 366 | }; 367 | Ok(ProtectedHeader { 368 | original_data: Some(data), 369 | header, 370 | }) 371 | } 372 | 373 | /// Convert this header to a `bstr` encoded map, as a [`Value`], consuming the object along the 374 | /// way. 375 | #[inline] 376 | pub fn cbor_bstr(self) -> Result { 377 | Ok(Value::Bytes( 378 | if let Some(original_data) = self.original_data { 379 | original_data 380 | } else if self.is_empty() { 381 | vec![] 382 | } else { 383 | self.to_vec()? 384 | }, 385 | )) 386 | } 387 | 388 | /// Indicate whether the `ProtectedHeader` is empty. 389 | pub fn is_empty(&self) -> bool { 390 | self.header.is_empty() 391 | } 392 | } 393 | 394 | impl crate::CborSerializable for ProtectedHeader {} 395 | 396 | impl AsCborValue for ProtectedHeader { 397 | fn from_cbor_value(value: Value) -> Result { 398 | Ok(ProtectedHeader { 399 | original_data: None, 400 | header: Header::from_cbor_value(value)?, 401 | }) 402 | } 403 | 404 | fn to_cbor_value(self) -> Result { 405 | self.header.to_cbor_value() 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/header/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | use super::*; 18 | use crate::{cbor::value::Value, iana, util::expect_err, CborSerializable, Label}; 19 | use alloc::{borrow::ToOwned, vec}; 20 | 21 | #[test] 22 | fn test_header_encode() { 23 | let tests = vec![ 24 | ( 25 | Header { 26 | alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)), 27 | key_id: vec![1, 2, 3], 28 | partial_iv: vec![1, 2, 3], 29 | ..Default::default() 30 | }, 31 | concat!( 32 | "a3", // 3-map 33 | "01", "01", // 1 (alg) => A128GCM 34 | "04", "43", "010203", // 4 (kid) => 3-bstr 35 | "06", "43", "010203", // 6 (partial-iv) => 3-bstr 36 | ), 37 | ), 38 | ( 39 | Header { 40 | alg: Some(Algorithm::PrivateUse(i64::MIN)), 41 | ..Default::default() 42 | }, 43 | concat!( 44 | "a1", // 1-map 45 | "01", 46 | "3b7fffffffffffffff", // 1 (alg) => -lots 47 | ), 48 | ), 49 | ( 50 | Header { 51 | alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)), 52 | crit: vec![RegisteredLabel::Assigned(iana::HeaderParameter::Alg)], 53 | content_type: Some(ContentType::Assigned(iana::CoapContentFormat::CoseEncrypt0)), 54 | key_id: vec![1, 2, 3], 55 | iv: vec![1, 2, 3], 56 | rest: vec![ 57 | (Label::Int(0x46), Value::from(0x47)), 58 | (Label::Int(0x66), Value::from(0x67)), 59 | ], 60 | ..Default::default() 61 | }, 62 | concat!( 63 | "a7", // 7-map 64 | "01", "01", // 1 (alg) => A128GCM 65 | "02", "81", "01", // 2 (crit) => 1-arr [x01] 66 | "03", "10", // 3 (content-type) => 16 67 | "04", "43", "010203", // 4 (kid) => 3-bstr 68 | "05", "43", "010203", // 5 (iv) => 3-bstr 69 | "1846", "1847", // 46 => 47 (note canonical ordering) 70 | "1866", "1867", // 66 => 67 71 | ), 72 | ), 73 | ( 74 | Header { 75 | alg: Some(Algorithm::Text("abc".to_owned())), 76 | crit: vec![RegisteredLabel::Text("d".to_owned())], 77 | content_type: Some(ContentType::Text("a/b".to_owned())), 78 | key_id: vec![1, 2, 3], 79 | iv: vec![1, 2, 3], 80 | rest: vec![ 81 | (Label::Int(0x46), Value::from(0x47)), 82 | (Label::Text("a".to_owned()), Value::from(0x47)), 83 | ], 84 | counter_signatures: vec![CoseSignature { 85 | signature: vec![1, 2, 3], 86 | ..Default::default() 87 | }], 88 | ..Default::default() 89 | }, 90 | concat!( 91 | "a8", // 8-map 92 | "01", "63616263", // 1 (alg) => "abc" 93 | "02", "81", "6164", // 2 (crit) => 1-arr ["d"] 94 | "03", "63612f62", // 3 (content-type) => "a/b" 95 | "04", "43", "010203", // 4 (kid) => 3-bstr 96 | "05", "43", "010203", // 5 (iv) => 3-bstr 97 | "07", "83", // 7 (sig) => [3-arr for COSE_Signature 98 | "40", "a0", "43010203", // ] 99 | "1846", "1847", // 46 => 47 (note canonical ordering) 100 | "6161", "1847", // "a" => 47 101 | ), 102 | ), 103 | ( 104 | Header { 105 | alg: Some(Algorithm::Text("abc".to_owned())), 106 | crit: vec![RegisteredLabel::Text("d".to_owned())], 107 | content_type: Some(ContentType::Text("a/b".to_owned())), 108 | key_id: vec![1, 2, 3], 109 | iv: vec![1, 2, 3], 110 | rest: vec![ 111 | (Label::Int(0x46), Value::from(0x47)), 112 | (Label::Text("a".to_owned()), Value::from(0x47)), 113 | ], 114 | counter_signatures: vec![ 115 | CoseSignature { 116 | signature: vec![1, 2, 3], 117 | ..Default::default() 118 | }, 119 | CoseSignature { 120 | signature: vec![3, 4, 5], 121 | ..Default::default() 122 | }, 123 | ], 124 | ..Default::default() 125 | }, 126 | concat!( 127 | "a8", // 8-map 128 | "01", "63616263", // 1 (alg) => "abc" 129 | "02", "81", "6164", // 2 (crit) => 1-arr ["d"] 130 | "03", "63612f62", // 3 (content-type) => "a/b" 131 | "04", "43", "010203", // 4 (kid) => 3-bstr 132 | "05", "43", "010203", // 5 (iv) => 3-bstr 133 | "07", "82", // 7 (sig) => 2-array 134 | "83", "40", "a0", "43010203", // [3-arr for COSE_Signature] 135 | "83", "40", "a0", "43030405", // [3-arr for COSE_Signature] 136 | "1846", "1847", // 46 => 47 (note canonical ordering) 137 | "6161", "1847", // "a" => 47 138 | ), 139 | ), 140 | ( 141 | HeaderBuilder::new() 142 | .add_critical(iana::HeaderParameter::Alg) 143 | .add_critical(iana::HeaderParameter::Alg) 144 | .build(), 145 | concat!( 146 | "a1", // 1-map 147 | "02", "820101", // crit => 2-arr [1, 1] 148 | ), 149 | ), 150 | ]; 151 | for (i, (header, header_data)) in tests.iter().enumerate() { 152 | let got = header.clone().to_vec().unwrap(); 153 | assert_eq!(*header_data, hex::encode(&got), "case {}", i); 154 | 155 | let mut got = Header::from_slice(&got).unwrap(); 156 | for sig in &mut got.counter_signatures { 157 | sig.protected.original_data = None; 158 | } 159 | assert_eq!(*header, got); 160 | assert!(!got.is_empty()); 161 | 162 | // The same data also parses as a `ProtectedHeader` 163 | let protected = ProtectedHeader { 164 | original_data: None, 165 | header: header.clone(), 166 | }; 167 | let protected_data = protected.clone().to_vec().unwrap(); 168 | assert_eq!(*header_data, hex::encode(&protected_data), "case {}", i); 169 | 170 | let mut got = ProtectedHeader::from_slice(&protected_data).unwrap(); 171 | for sig in &mut got.header.counter_signatures { 172 | sig.protected.original_data = None; 173 | } 174 | assert!(!got.is_empty()); 175 | assert_eq!(*header, got.header); 176 | 177 | // Also try parsing as a protected header inside a `bstr` 178 | let prot_bstr_val = protected.cbor_bstr().unwrap(); 179 | let mut got = ProtectedHeader::from_cbor_bstr(prot_bstr_val).unwrap(); 180 | for sig in &mut got.header.counter_signatures { 181 | sig.protected.original_data = None; 182 | } 183 | assert!(!got.is_empty()); 184 | assert_eq!(*header, got.header); 185 | assert_eq!( 186 | *header_data, 187 | hex::encode(got.original_data.expect("missing original data")) 188 | ); 189 | } 190 | } 191 | 192 | #[test] 193 | fn test_header_decode_fail() { 194 | let tests = vec![ 195 | ( 196 | concat!( 197 | "a1", // 1-map 198 | "01", "01", // 1 (alg) => 01 199 | "01", // extraneous data 200 | ), 201 | "extraneous data in CBOR input", 202 | ), 203 | ( 204 | concat!( 205 | "a1", // 1-map 206 | "01", "08", // 1 (alg) => invalid value 207 | ), 208 | "expected value in IANA or private use range", 209 | ), 210 | ( 211 | concat!( 212 | "a1", // 1-map 213 | "01", "4101", // 1 (alg) => bstr (invalid value type) 214 | ), 215 | "expected int/tstr", 216 | ), 217 | ( 218 | concat!( 219 | "a1", // 1-map 220 | "02", "4101", // 2 (crit) => bstr (invalid value type) 221 | ), 222 | "expected array", 223 | ), 224 | ( 225 | concat!( 226 | "a1", // 1-map 227 | "02", "81", "4101", // 2 (crit) => [bstr] (invalid value type) 228 | ), 229 | "expected int/tstr", 230 | ), 231 | ( 232 | concat!( 233 | "a1", // 1-map 234 | "02", "80", // 2 (crit) => [] 235 | ), 236 | "expected non-empty array", 237 | ), 238 | ( 239 | concat!( 240 | "a1", // 1-map 241 | "03", "81", "4101", // 3 (content-type) => [bstr] (invalid value type) 242 | ), 243 | "expected int/tstr", 244 | ), 245 | ( 246 | concat!( 247 | "a1", // 1-map 248 | "03", "19", "0606", // 3 (content-type) => invalid value 1542 249 | ), 250 | "expected recognized IANA value", 251 | ), 252 | ( 253 | concat!( 254 | "a1", // 1-map 255 | "03", "64", "20612f62" // 3 (content-type) => invalid value " a/b" 256 | ), 257 | "expected no leading/trailing whitespace", 258 | ), 259 | ( 260 | concat!( 261 | "a1", // 1-map 262 | "03", "64", "612f6220" // 3 (content-type) => invalid value "a/b " 263 | ), 264 | "expected no leading/trailing whitespace", 265 | ), 266 | ( 267 | concat!( 268 | "a1", // 1-map 269 | "03", "62", "6162" // 3 (content-type) => invalid value "ab" 270 | ), 271 | "expected text of form type/subtype", 272 | ), 273 | ( 274 | concat!( 275 | "a1", // 1-map 276 | "03", "60", // 3 (content-type) => invalid value "" 277 | ), 278 | "expected non-empty tstr", 279 | ), 280 | ( 281 | concat!( 282 | "a1", // 1-map 283 | "04", "40", // 4 (key-id) => 0-bstr 284 | ), 285 | "expected non-empty bstr", 286 | ), 287 | ( 288 | concat!( 289 | "a1", // 1-map 290 | "04", "01", // 4 (key-id) => invalid value type 291 | ), 292 | "expected bstr", 293 | ), 294 | ( 295 | concat!( 296 | "a1", // 1-map 297 | "05", "40", // 5 (iv) => 0-bstr 298 | ), 299 | "expected non-empty bstr", 300 | ), 301 | ( 302 | concat!( 303 | "a1", // 1-map 304 | "05", "01", // 5 (iv) => invalid value type 305 | ), 306 | "expected bstr", 307 | ), 308 | ( 309 | concat!( 310 | "a1", // 1-map 311 | "06", "40", // 6 (partial-iv) => 0-bstr 312 | ), 313 | "expected non-empty bstr", 314 | ), 315 | ( 316 | concat!( 317 | "a1", // 1-map 318 | "06", "01", // 6 (partial-iv) => invalid value type 319 | ), 320 | "expected bstr", 321 | ), 322 | ( 323 | concat!( 324 | "a1", // 1-map 325 | "07", "01", // 7 (counter-sig) => invalid value type 326 | ), 327 | "expected array", 328 | ), 329 | ( 330 | concat!( 331 | "a1", // 1-map 332 | "07", "80", // 7 (counter-sig) => 0-arr 333 | ), 334 | "expected non-empty sig array", 335 | ), 336 | ( 337 | concat!( 338 | "a2", // 1-map 339 | "05", "4101", // 5 (iv) => 1-bstr 340 | "06", "4101", // 6 (partial-iv) => 1-bstr 341 | ), 342 | "expected only one of IV and partial IV", 343 | ), 344 | ( 345 | concat!( 346 | "a2", // 2-map 347 | "01", "63616263", // 1 (alg) => "abc" 348 | "07", "82", // 7 (sig) => 2-array 349 | "63616263", // tstr (invalid) 350 | "83", "40", "a0", "43010203", // [3-arr for COSE_Signature] 351 | ), 352 | "array or bstr value", 353 | ), 354 | ]; 355 | for (header_data, err_msg) in tests.iter() { 356 | let data = hex::decode(header_data).unwrap(); 357 | let result = Header::from_slice(&data); 358 | expect_err(result, err_msg); 359 | } 360 | } 361 | 362 | #[test] 363 | fn test_header_decode_dup_fail() { 364 | let tests = [ 365 | ( 366 | concat!( 367 | "a3", // 3-map 368 | "01", "01", // 1 (alg) => A128GCM 369 | "1866", "1867", // 66 => 67 370 | "1866", "1847", // 66 => 47 371 | ), 372 | "duplicate map key", 373 | ), 374 | ( 375 | concat!( 376 | "a3", // 3-map 377 | "01", "01", // 1 (alg) => A128GCM 378 | "1866", "1867", // 66 => 67 379 | "01", "01", // 1 (alg) => A128GCM (duplicate label) 380 | ), 381 | "duplicate map key", 382 | ), 383 | ]; 384 | for (header_data, err_msg) in tests.iter() { 385 | let data = hex::decode(header_data).unwrap(); 386 | let result = Header::from_slice(&data); 387 | expect_err(result, err_msg); 388 | } 389 | } 390 | 391 | #[test] 392 | fn test_header_encode_dup_fail() { 393 | let tests = vec![ 394 | Header { 395 | alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)), 396 | crit: vec![RegisteredLabel::Assigned(iana::HeaderParameter::Alg)], 397 | content_type: Some(ContentType::Assigned(iana::CoapContentFormat::CoseEncrypt0)), 398 | key_id: vec![1, 2, 3], 399 | iv: vec![1, 2, 3], 400 | rest: vec![ 401 | (Label::Int(0x46), Value::from(0x47)), 402 | (Label::Int(0x46), Value::from(0x67)), 403 | ], 404 | ..Default::default() 405 | }, 406 | HeaderBuilder::new() 407 | .text_value("doop".to_owned(), Value::from(1)) 408 | .text_value("doop".to_owned(), Value::from(2)) 409 | .build(), 410 | ]; 411 | for header in tests { 412 | let result = header.clone().to_vec(); 413 | expect_err(result, "duplicate map key"); 414 | } 415 | } 416 | 417 | #[test] 418 | fn test_header_builder() { 419 | let tests = vec![ 420 | ( 421 | HeaderBuilder::new().build(), 422 | Header { 423 | ..Default::default() 424 | }, 425 | ), 426 | ( 427 | HeaderBuilder::new() 428 | .algorithm(iana::Algorithm::A128GCM) 429 | .add_critical(iana::HeaderParameter::Alg) 430 | .add_critical_label(RegisteredLabel::Text("abc".to_owned())) 431 | .content_format(iana::CoapContentFormat::CoseEncrypt0) 432 | .key_id(vec![1, 2, 3]) 433 | .partial_iv(vec![4, 5, 6]) // removed by .iv() call 434 | .iv(vec![1, 2, 3]) 435 | .value(0x46, Value::from(0x47)) 436 | .value(0x66, Value::from(0x67)) 437 | .build(), 438 | Header { 439 | alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)), 440 | crit: vec![ 441 | RegisteredLabel::Assigned(iana::HeaderParameter::Alg), 442 | RegisteredLabel::Text("abc".to_owned()), 443 | ], 444 | content_type: Some(ContentType::Assigned(iana::CoapContentFormat::CoseEncrypt0)), 445 | key_id: vec![1, 2, 3], 446 | iv: vec![1, 2, 3], 447 | rest: vec![ 448 | (Label::Int(0x46), Value::from(0x47)), 449 | (Label::Int(0x66), Value::from(0x67)), 450 | ], 451 | ..Default::default() 452 | }, 453 | ), 454 | ( 455 | HeaderBuilder::new() 456 | .algorithm(iana::Algorithm::A128GCM) 457 | .add_critical(iana::HeaderParameter::Alg) 458 | .add_critical_label(RegisteredLabel::Text("abc".to_owned())) 459 | .content_type("type/subtype".to_owned()) 460 | .key_id(vec![1, 2, 3]) 461 | .iv(vec![1, 2, 3]) // removed by .partial_iv() call 462 | .partial_iv(vec![4, 5, 6]) 463 | .build(), 464 | Header { 465 | alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)), 466 | crit: vec![ 467 | RegisteredLabel::Assigned(iana::HeaderParameter::Alg), 468 | RegisteredLabel::Text("abc".to_owned()), 469 | ], 470 | content_type: Some(ContentType::Text("type/subtype".to_owned())), 471 | key_id: vec![1, 2, 3], 472 | partial_iv: vec![4, 5, 6], 473 | ..Default::default() 474 | }, 475 | ), 476 | ]; 477 | for (got, want) in tests { 478 | assert_eq!(got, want); 479 | } 480 | } 481 | 482 | #[test] 483 | #[should_panic] 484 | fn test_header_builder_core_param_panic() { 485 | // Attempting to set a core header parameter (in range [1,7]) via `.param()` panics. 486 | let _hdr = HeaderBuilder::new().value(1, Value::Null).build(); 487 | } 488 | -------------------------------------------------------------------------------- /src/iana/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | use super::*; 18 | 19 | #[test] 20 | fn test_algorithm_conversion() { 21 | assert_eq!(Some(Algorithm::ES256), Algorithm::from_i64(-7)); 22 | assert_eq!(Some(Algorithm::A128GCM), Algorithm::from_i64(1)); 23 | assert_eq!(Algorithm::A128GCM as i64, 1); 24 | assert_eq!(None, Algorithm::from_i64(8)); 25 | assert_eq!(None, Algorithm::from_i64(-65538)); 26 | } 27 | 28 | #[test] 29 | fn test_header_param_private_range() { 30 | assert!(!HeaderParameter::is_private(1)); 31 | assert!(HeaderParameter::is_private(-70_000)); 32 | } 33 | 34 | #[test] 35 | fn test_elliptic_curve_private_range() { 36 | assert!(!EllipticCurve::is_private(1)); 37 | assert!(EllipticCurve::is_private(-70_000)); 38 | } 39 | -------------------------------------------------------------------------------- /src/key/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! COSE_Key functionality. 18 | 19 | use crate::{ 20 | cbor::value::Value, 21 | common::{AsCborValue, CborOrdering}, 22 | iana, 23 | iana::EnumI64, 24 | util::{to_cbor_array, ValueTryAs}, 25 | Algorithm, CoseError, Label, Result, 26 | }; 27 | use alloc::{collections::BTreeSet, vec, vec::Vec}; 28 | 29 | #[cfg(test)] 30 | mod tests; 31 | 32 | /// Key type. 33 | pub type KeyType = crate::RegisteredLabel; 34 | 35 | impl Default for KeyType { 36 | fn default() -> Self { 37 | KeyType::Assigned(iana::KeyType::Reserved) 38 | } 39 | } 40 | 41 | /// Key operation. 42 | pub type KeyOperation = crate::RegisteredLabel; 43 | 44 | /// A collection of [`CoseKey`] objects. 45 | #[derive(Clone, Debug, Default, PartialEq)] 46 | pub struct CoseKeySet(pub Vec); 47 | 48 | impl crate::CborSerializable for CoseKeySet {} 49 | 50 | impl AsCborValue for CoseKeySet { 51 | fn from_cbor_value(value: Value) -> Result { 52 | Ok(Self( 53 | value.try_as_array_then_convert(CoseKey::from_cbor_value)?, 54 | )) 55 | } 56 | 57 | fn to_cbor_value(self) -> Result { 58 | to_cbor_array(self.0) 59 | } 60 | } 61 | 62 | /// Structure representing a cryptographic key. 63 | /// 64 | /// ```cddl 65 | /// COSE_Key = { 66 | /// 1 => tstr / int, ; kty 67 | /// ? 2 => bstr, ; kid 68 | /// ? 3 => tstr / int, ; alg 69 | /// ? 4 => [+ (tstr / int) ], ; key_ops 70 | /// ? 5 => bstr, ; Base IV 71 | /// * label => values 72 | /// } 73 | /// ``` 74 | #[derive(Clone, Debug, Default, PartialEq)] 75 | pub struct CoseKey { 76 | /// Key type identification. 77 | pub kty: KeyType, 78 | /// Key identification. 79 | pub key_id: Vec, 80 | /// Key use restriction to this algorithm. 81 | pub alg: Option, 82 | /// Restrict set of possible operations. 83 | pub key_ops: BTreeSet, 84 | /// Base IV to be xor-ed with partial IVs. 85 | pub base_iv: Vec, 86 | /// Any additional parameter (label,value) pairs. If duplicate labels are present, 87 | /// CBOR-encoding will fail. 88 | pub params: Vec<(Label, Value)>, 89 | } 90 | 91 | impl CoseKey { 92 | /// Re-order the contents of the key so that the contents will be emitted in one of the standard 93 | /// CBOR sorted orders. 94 | pub fn canonicalize(&mut self, ordering: CborOrdering) { 95 | // The keys that are represented as named fields CBOR-encode as single bytes 0x01 - 0x05, 96 | // which sort before any other CBOR values (other than 0x00) in either sorting scheme: 97 | // - In length-first sorting, a single byte sorts before anything multi-byte and 1-5 sorts 98 | // before any other value. 99 | // - In encoded-lexicographic sorting, there are no valid CBOR-encoded single values that 100 | // start with a byte in the range 0x01 - 0x05 other than the values 1-5. 101 | // So we only need to sort the `params`. 102 | match ordering { 103 | CborOrdering::Lexicographic => self.params.sort_by(|l, r| l.0.cmp(&r.0)), 104 | CborOrdering::LengthFirstLexicographic => { 105 | self.params.sort_by(|l, r| l.0.cmp_canonical(&r.0)) 106 | } 107 | } 108 | } 109 | } 110 | 111 | impl crate::CborSerializable for CoseKey {} 112 | 113 | const KTY: Label = Label::Int(iana::KeyParameter::Kty as i64); 114 | const KID: Label = Label::Int(iana::KeyParameter::Kid as i64); 115 | const ALG: Label = Label::Int(iana::KeyParameter::Alg as i64); 116 | const KEY_OPS: Label = Label::Int(iana::KeyParameter::KeyOps as i64); 117 | const BASE_IV: Label = Label::Int(iana::KeyParameter::BaseIv as i64); 118 | 119 | impl AsCborValue for CoseKey { 120 | fn from_cbor_value(value: Value) -> Result { 121 | let m = value.try_as_map()?; 122 | let mut key = Self::default(); 123 | let mut seen = BTreeSet::new(); 124 | for (l, value) in m.into_iter() { 125 | // The `ciborium` CBOR library does not police duplicate map keys. 126 | // RFC 8152 section 14 requires that COSE does police duplicates, so do it here. 127 | let label = Label::from_cbor_value(l)?; 128 | if seen.contains(&label) { 129 | return Err(CoseError::DuplicateMapKey); 130 | } 131 | seen.insert(label.clone()); 132 | match label { 133 | KTY => key.kty = KeyType::from_cbor_value(value)?, 134 | 135 | KID => { 136 | key.key_id = value.try_as_nonempty_bytes()?; 137 | } 138 | 139 | ALG => key.alg = Some(Algorithm::from_cbor_value(value)?), 140 | 141 | KEY_OPS => { 142 | let key_ops = value.try_as_array()?; 143 | for key_op in key_ops.into_iter() { 144 | if !key.key_ops.insert(KeyOperation::from_cbor_value(key_op)?) { 145 | return Err(CoseError::UnexpectedItem( 146 | "repeated array entry", 147 | "unique array label", 148 | )); 149 | } 150 | } 151 | if key.key_ops.is_empty() { 152 | return Err(CoseError::UnexpectedItem("empty array", "non-empty array")); 153 | } 154 | } 155 | 156 | BASE_IV => { 157 | key.base_iv = value.try_as_nonempty_bytes()?; 158 | } 159 | 160 | label => key.params.push((label, value)), 161 | } 162 | } 163 | // Check that key type has been set. 164 | if key.kty == KeyType::Assigned(iana::KeyType::Reserved) { 165 | return Err(CoseError::UnexpectedItem( 166 | "no kty label", 167 | "mandatory kty label", 168 | )); 169 | } 170 | 171 | Ok(key) 172 | } 173 | 174 | fn to_cbor_value(self) -> Result { 175 | let mut map: Vec<(Value, Value)> = vec![(KTY.to_cbor_value()?, self.kty.to_cbor_value()?)]; 176 | if !self.key_id.is_empty() { 177 | map.push((KID.to_cbor_value()?, Value::Bytes(self.key_id))); 178 | } 179 | if let Some(alg) = self.alg { 180 | map.push((ALG.to_cbor_value()?, alg.to_cbor_value()?)); 181 | } 182 | if !self.key_ops.is_empty() { 183 | map.push((KEY_OPS.to_cbor_value()?, to_cbor_array(self.key_ops)?)); 184 | } 185 | if !self.base_iv.is_empty() { 186 | map.push((BASE_IV.to_cbor_value()?, Value::Bytes(self.base_iv))); 187 | } 188 | let mut seen = BTreeSet::new(); 189 | for (label, value) in self.params { 190 | if seen.contains(&label) { 191 | return Err(CoseError::DuplicateMapKey); 192 | } 193 | seen.insert(label.clone()); 194 | map.push((label.to_cbor_value()?, value)); 195 | } 196 | Ok(Value::Map(map)) 197 | } 198 | } 199 | 200 | /// Builder for [`CoseKey`] objects. 201 | #[derive(Debug, Default)] 202 | pub struct CoseKeyBuilder(CoseKey); 203 | 204 | impl CoseKeyBuilder { 205 | builder! {CoseKey} 206 | builder_set! {kty: KeyType} 207 | builder_set! {key_id: Vec} 208 | builder_set! {base_iv: Vec} 209 | 210 | /// Constructor for an elliptic curve public key specified by `x` and `y` coordinates. 211 | pub fn new_ec2_pub_key(curve: iana::EllipticCurve, x: Vec, y: Vec) -> Self { 212 | Self(CoseKey { 213 | kty: KeyType::Assigned(iana::KeyType::EC2), 214 | params: vec![ 215 | ( 216 | Label::Int(iana::Ec2KeyParameter::Crv as i64), 217 | Value::from(curve as u64), 218 | ), 219 | (Label::Int(iana::Ec2KeyParameter::X as i64), Value::Bytes(x)), 220 | (Label::Int(iana::Ec2KeyParameter::Y as i64), Value::Bytes(y)), 221 | ], 222 | ..Default::default() 223 | }) 224 | } 225 | 226 | /// Constructor for an elliptic curve public key specified by `x` coordinate plus sign of `y` 227 | /// coordinate. 228 | pub fn new_ec2_pub_key_y_sign(curve: iana::EllipticCurve, x: Vec, y_sign: bool) -> Self { 229 | Self(CoseKey { 230 | kty: KeyType::Assigned(iana::KeyType::EC2), 231 | params: vec![ 232 | ( 233 | Label::Int(iana::Ec2KeyParameter::Crv as i64), 234 | Value::from(curve as u64), 235 | ), 236 | (Label::Int(iana::Ec2KeyParameter::X as i64), Value::Bytes(x)), 237 | ( 238 | Label::Int(iana::Ec2KeyParameter::Y as i64), 239 | Value::Bool(y_sign), 240 | ), 241 | ], 242 | ..Default::default() 243 | }) 244 | } 245 | 246 | /// Constructor for an elliptic curve private key specified by `d`, together with public `x` and 247 | /// `y` coordinates. 248 | pub fn new_ec2_priv_key( 249 | curve: iana::EllipticCurve, 250 | x: Vec, 251 | y: Vec, 252 | d: Vec, 253 | ) -> Self { 254 | let mut builder = Self::new_ec2_pub_key(curve, x, y); 255 | builder 256 | .0 257 | .params 258 | .push((Label::Int(iana::Ec2KeyParameter::D as i64), Value::Bytes(d))); 259 | builder 260 | } 261 | 262 | /// Constructor for a symmetric key specified by `k`. 263 | pub fn new_symmetric_key(k: Vec) -> Self { 264 | Self(CoseKey { 265 | kty: KeyType::Assigned(iana::KeyType::Symmetric), 266 | params: vec![( 267 | Label::Int(iana::SymmetricKeyParameter::K as i64), 268 | Value::Bytes(k), 269 | )], 270 | ..Default::default() 271 | }) 272 | } 273 | 274 | /// Constructor for a octet keypair key. 275 | pub fn new_okp_key() -> Self { 276 | Self(CoseKey { 277 | kty: KeyType::Assigned(iana::KeyType::OKP), 278 | ..Default::default() 279 | }) 280 | } 281 | 282 | /// Set the key type. 283 | #[must_use] 284 | pub fn key_type(mut self, key_type: iana::KeyType) -> Self { 285 | self.0.kty = KeyType::Assigned(key_type); 286 | self 287 | } 288 | 289 | /// Set the algorithm. 290 | #[must_use] 291 | pub fn algorithm(mut self, alg: iana::Algorithm) -> Self { 292 | self.0.alg = Some(Algorithm::Assigned(alg)); 293 | self 294 | } 295 | 296 | /// Add a key operation. 297 | #[must_use] 298 | pub fn add_key_op(mut self, op: iana::KeyOperation) -> Self { 299 | self.0.key_ops.insert(KeyOperation::Assigned(op)); 300 | self 301 | } 302 | 303 | /// Set a parameter value. 304 | /// 305 | /// # Panics 306 | /// 307 | /// This function will panic if it used to set a parameter label from the [`iana::KeyParameter`] 308 | /// range. 309 | #[must_use] 310 | pub fn param(mut self, label: i64, value: Value) -> Self { 311 | if iana::KeyParameter::from_i64(label).is_some() { 312 | panic!("param() method used to set KeyParameter"); // safe: invalid input 313 | } 314 | self.0.params.push((Label::Int(label), value)); 315 | self 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! Set of types for supporting [CBOR Object Signing and Encryption (COSE)][COSE]. 18 | //! 19 | //! Builds on the [`ciborium`](https://docs.rs/ciborium) crate for underlying [CBOR][CBOR] support. 20 | //! 21 | //! ## Usage 22 | //! 23 | //! ``` 24 | //! # #[derive(Copy, Clone)] 25 | //! # struct FakeSigner {} 26 | //! # impl FakeSigner { 27 | //! # fn sign(&self, data: &[u8]) -> Vec { 28 | //! # data.to_vec() 29 | //! # } 30 | //! # fn verify(&self, sig: &[u8], data: &[u8]) -> Result<(), String> { 31 | //! # if sig != self.sign(data) { 32 | //! # Err("failed to verify".to_owned()) 33 | //! # } else { 34 | //! # Ok(()) 35 | //! # } 36 | //! # } 37 | //! # } 38 | //! # let signer = FakeSigner {}; 39 | //! # let verifier = signer; 40 | //! use coset::{iana, CborSerializable}; 41 | //! 42 | //! // Inputs. 43 | //! let pt = b"This is the content"; 44 | //! let aad = b"this is additional data"; 45 | //! 46 | //! // Build a `CoseSign1` object. 47 | //! let protected = coset::HeaderBuilder::new() 48 | //! .algorithm(iana::Algorithm::ES256) 49 | //! .key_id(b"11".to_vec()) 50 | //! .build(); 51 | //! let sign1 = coset::CoseSign1Builder::new() 52 | //! .protected(protected) 53 | //! .payload(pt.to_vec()) 54 | //! .create_signature(aad, |pt| signer.sign(pt)) // closure to do sign operation 55 | //! .build(); 56 | //! 57 | //! // Serialize to bytes. 58 | //! let sign1_data = sign1.to_vec().unwrap(); 59 | //! println!( 60 | //! "'{}' + '{}' => {}", 61 | //! String::from_utf8_lossy(pt), 62 | //! String::from_utf8_lossy(aad), 63 | //! hex::encode(&sign1_data) 64 | //! ); 65 | //! 66 | //! // At the receiving end, deserialize the bytes back to a `CoseSign1` object. 67 | //! let mut sign1 = coset::CoseSign1::from_slice(&sign1_data).unwrap(); 68 | //! 69 | //! // At this point, real code would validate the protected headers. 70 | //! 71 | //! // Check the signature, which needs to have the same `aad` provided, by 72 | //! // providing a closure that can do the verify operation. 73 | //! let result = sign1.verify_signature(aad, |sig, data| verifier.verify(sig, data)); 74 | //! println!("Signature verified: {:?}.", result); 75 | //! assert!(result.is_ok()); 76 | //! 77 | //! // Changing an unprotected header leaves the signature valid. 78 | //! sign1.unprotected.content_type = Some(coset::ContentType::Text("text/plain".to_owned())); 79 | //! assert!(sign1 80 | //! .verify_signature(aad, |sig, data| verifier.verify(sig, data)) 81 | //! .is_ok()); 82 | //! 83 | //! // Providing a different `aad` means the signature won't validate. 84 | //! assert!(sign1 85 | //! .verify_signature(b"not aad", |sig, data| verifier.verify(sig, data)) 86 | //! .is_err()); 87 | //! 88 | //! // Changing a protected header invalidates the signature. 89 | //! sign1.protected.original_data = None; 90 | //! sign1.protected.header.content_type = Some(coset::ContentType::Text("text/plain".to_owned())); 91 | //! assert!(sign1 92 | //! .verify_signature(aad, |sig, data| verifier.verify(sig, data)) 93 | //! .is_err()); 94 | //! ``` 95 | //! 96 | //! [COSE]: https://tools.ietf.org/html/rfc8152 97 | //! [CBOR]: https://tools.ietf.org/html/rfc7049 98 | 99 | #![cfg_attr(not(feature = "std"), no_std)] 100 | #![deny(rustdoc::broken_intra_doc_links)] 101 | extern crate alloc; 102 | 103 | /// Re-export of the `ciborium` crate used for underlying CBOR encoding. 104 | pub use ciborium as cbor; 105 | 106 | #[macro_use] 107 | pub(crate) mod util; 108 | 109 | pub mod cwt; 110 | #[macro_use] 111 | pub mod iana; 112 | 113 | mod common; 114 | pub use common::*; 115 | mod context; 116 | pub use context::*; 117 | mod encrypt; 118 | pub use encrypt::*; 119 | mod header; 120 | pub use header::*; 121 | mod key; 122 | pub use key::*; 123 | mod mac; 124 | pub use mac::*; 125 | mod sign; 126 | pub use sign::*; 127 | -------------------------------------------------------------------------------- /src/mac/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! COSE_Mac functionality. 18 | 19 | use crate::{ 20 | cbor, 21 | cbor::value::Value, 22 | common::AsCborValue, 23 | iana, 24 | util::{cbor_type_error, to_cbor_array, ValueTryAs}, 25 | CoseError, CoseRecipient, Header, ProtectedHeader, Result, 26 | }; 27 | use alloc::{borrow::ToOwned, vec, vec::Vec}; 28 | 29 | #[cfg(test)] 30 | mod tests; 31 | 32 | /// Structure representing a message with authentication code (MAC). 33 | /// 34 | /// ```cddl 35 | /// COSE_Mac = [ 36 | /// Headers, 37 | /// payload : bstr / nil, 38 | /// tag : bstr, 39 | /// recipients :[+COSE_recipient] 40 | /// ] 41 | /// ``` 42 | #[derive(Clone, Debug, Default, PartialEq)] 43 | pub struct CoseMac { 44 | pub protected: ProtectedHeader, 45 | pub unprotected: Header, 46 | pub payload: Option>, 47 | pub tag: Vec, 48 | pub recipients: Vec, 49 | } 50 | 51 | impl crate::CborSerializable for CoseMac {} 52 | 53 | impl crate::TaggedCborSerializable for CoseMac { 54 | const TAG: u64 = iana::CborTag::CoseMac as u64; 55 | } 56 | 57 | impl AsCborValue for CoseMac { 58 | fn from_cbor_value(value: Value) -> Result { 59 | let mut a = value.try_as_array()?; 60 | if a.len() != 5 { 61 | return Err(CoseError::UnexpectedItem("array", "array with 5 items")); 62 | } 63 | 64 | // Remove array elements in reverse order to avoid shifts. 65 | let recipients = a 66 | .remove(4) 67 | .try_as_array_then_convert(CoseRecipient::from_cbor_value)?; 68 | 69 | Ok(Self { 70 | recipients, 71 | tag: a.remove(3).try_as_bytes()?, 72 | payload: match a.remove(2) { 73 | Value::Bytes(b) => Some(b), 74 | Value::Null => None, 75 | v => return cbor_type_error(&v, "bstr"), 76 | }, 77 | unprotected: Header::from_cbor_value(a.remove(1))?, 78 | protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?, 79 | }) 80 | } 81 | 82 | fn to_cbor_value(self) -> Result { 83 | Ok(Value::Array(vec![ 84 | self.protected.cbor_bstr()?, 85 | self.unprotected.to_cbor_value()?, 86 | match self.payload { 87 | None => Value::Null, 88 | Some(b) => Value::Bytes(b), 89 | }, 90 | Value::Bytes(self.tag), 91 | to_cbor_array(self.recipients)?, 92 | ])) 93 | } 94 | } 95 | 96 | impl CoseMac { 97 | /// Verify the `tag` value using the provided `mac` function, feeding it 98 | /// the `tag` value and the combined to-be-MACed data (in that order). 99 | /// 100 | /// # Panics 101 | /// 102 | /// This function will panic if the `payload` has not been set. 103 | pub fn verify_tag(&self, external_aad: &[u8], verify: F) -> Result<(), E> 104 | where 105 | F: FnOnce(&[u8], &[u8]) -> Result<(), E>, 106 | { 107 | let tbm = self.tbm(external_aad); 108 | verify(&self.tag, &tbm) 109 | } 110 | 111 | /// Construct the to-be-MAC-ed data for this object. Any protected header values should be set 112 | /// before using this method, as should the `payload`. 113 | /// 114 | /// # Panics 115 | /// 116 | /// This function will panic if the `payload` has not been set. 117 | fn tbm(&self, external_aad: &[u8]) -> Vec { 118 | mac_structure_data( 119 | MacContext::CoseMac, 120 | self.protected.clone(), 121 | external_aad, 122 | self.payload.as_ref().expect("payload missing"), // safe: documented 123 | ) 124 | } 125 | } 126 | 127 | /// Builder for [`CoseMac`] objects. 128 | #[derive(Debug, Default)] 129 | pub struct CoseMacBuilder(CoseMac); 130 | 131 | impl CoseMacBuilder { 132 | builder! {CoseMac} 133 | builder_set_protected! {protected} 134 | builder_set! {unprotected: Header} 135 | builder_set! {tag: Vec} 136 | builder_set_optional! {payload: Vec} 137 | 138 | /// Add a [`CoseRecipient`]. 139 | #[must_use] 140 | pub fn add_recipient(mut self, recipient: CoseRecipient) -> Self { 141 | self.0.recipients.push(recipient); 142 | self 143 | } 144 | 145 | /// Calculate the tag value, using `mac`. Any protected header values should be set 146 | /// before using this method, as should the `payload`. 147 | /// 148 | /// # Panics 149 | /// 150 | /// This function will panic if the `payload` has not been set. 151 | #[must_use] 152 | pub fn create_tag(self, external_aad: &[u8], create: F) -> Self 153 | where 154 | F: FnOnce(&[u8]) -> Vec, 155 | { 156 | let tbm = self.0.tbm(external_aad); 157 | self.tag(create(&tbm)) 158 | } 159 | 160 | /// Calculate the tag value, using `mac`. Any protected header values should be set 161 | /// before using this method, as should the `payload`. 162 | /// 163 | /// # Panics 164 | /// 165 | /// This function will panic if the `payload` has not been set. 166 | pub fn try_create_tag(self, external_aad: &[u8], create: F) -> Result 167 | where 168 | F: FnOnce(&[u8]) -> Result, E>, 169 | { 170 | let tbm = self.0.tbm(external_aad); 171 | Ok(self.tag(create(&tbm)?)) 172 | } 173 | } 174 | 175 | /// Structure representing a message with authentication code (MAC) 176 | /// where the relevant key is implicit. 177 | /// 178 | /// ```cddl 179 | /// COSE_Mac0 = [ 180 | /// Headers, 181 | /// payload : bstr / nil, 182 | /// tag : bstr, 183 | /// ] 184 | /// ``` 185 | #[derive(Clone, Debug, Default, PartialEq)] 186 | pub struct CoseMac0 { 187 | pub protected: ProtectedHeader, 188 | pub unprotected: Header, 189 | pub payload: Option>, 190 | pub tag: Vec, 191 | } 192 | 193 | impl crate::CborSerializable for CoseMac0 {} 194 | 195 | impl crate::TaggedCborSerializable for CoseMac0 { 196 | const TAG: u64 = iana::CborTag::CoseMac0 as u64; 197 | } 198 | 199 | impl AsCborValue for CoseMac0 { 200 | fn from_cbor_value(value: Value) -> Result { 201 | let mut a = value.try_as_array()?; 202 | if a.len() != 4 { 203 | return Err(CoseError::UnexpectedItem("array", "array with 4 items")); 204 | } 205 | 206 | // Remove array elements in reverse order to avoid shifts. 207 | Ok(Self { 208 | tag: a.remove(3).try_as_bytes()?, 209 | payload: match a.remove(2) { 210 | Value::Bytes(b) => Some(b), 211 | Value::Null => None, 212 | v => return cbor_type_error(&v, "bstr"), 213 | }, 214 | unprotected: Header::from_cbor_value(a.remove(1))?, 215 | protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?, 216 | }) 217 | } 218 | 219 | fn to_cbor_value(self) -> Result { 220 | Ok(Value::Array(vec![ 221 | self.protected.cbor_bstr()?, 222 | self.unprotected.to_cbor_value()?, 223 | match self.payload { 224 | None => Value::Null, 225 | Some(b) => Value::Bytes(b), 226 | }, 227 | Value::Bytes(self.tag), 228 | ])) 229 | } 230 | } 231 | 232 | impl CoseMac0 { 233 | /// Verify the `tag` value using the provided `mac` function, feeding it 234 | /// the `tag` value and the combined to-be-MACed data (in that order). 235 | /// 236 | /// # Panics 237 | /// 238 | /// This function will panic if the `payload` has not been set. 239 | pub fn verify_tag(&self, external_aad: &[u8], verify: F) -> Result<(), E> 240 | where 241 | F: FnOnce(&[u8], &[u8]) -> Result<(), E>, 242 | { 243 | let tbm = self.tbm(external_aad); 244 | verify(&self.tag, &tbm) 245 | } 246 | 247 | /// Construct the to-be-MAC-ed data for this object. Any protected header values should be set 248 | /// before using this method, as should the `payload`. 249 | /// 250 | /// # Panics 251 | /// 252 | /// This function will panic if the `payload` has not been set. 253 | fn tbm(&self, external_aad: &[u8]) -> Vec { 254 | mac_structure_data( 255 | MacContext::CoseMac0, 256 | self.protected.clone(), 257 | external_aad, 258 | self.payload.as_ref().expect("payload missing"), // safe: documented 259 | ) 260 | } 261 | } 262 | 263 | /// Builder for [`CoseMac0`] objects. 264 | #[derive(Debug, Default)] 265 | pub struct CoseMac0Builder(CoseMac0); 266 | 267 | impl CoseMac0Builder { 268 | builder! {CoseMac0} 269 | builder_set_protected! {protected} 270 | builder_set! {unprotected: Header} 271 | builder_set! {tag: Vec} 272 | builder_set_optional! {payload: Vec} 273 | 274 | /// Calculate the tag value, using `mac`. Any protected header values should be set 275 | /// before using this method, as should the `payload`. 276 | /// 277 | /// # Panics 278 | /// 279 | /// This function will panic if the `payload` has not been set. 280 | #[must_use] 281 | pub fn create_tag(self, external_aad: &[u8], create: F) -> Self 282 | where 283 | F: FnOnce(&[u8]) -> Vec, 284 | { 285 | let tbm = self.0.tbm(external_aad); 286 | self.tag(create(&tbm)) 287 | } 288 | 289 | /// Calculate the tag value, using `mac`. Any protected header values should be set 290 | /// before using this method, as should the `payload`. 291 | /// 292 | /// # Panics 293 | /// 294 | /// This function will panic if the `payload` has not been set. 295 | pub fn try_create_tag(self, external_aad: &[u8], create: F) -> Result 296 | where 297 | F: FnOnce(&[u8]) -> Result, E>, 298 | { 299 | let tbm = self.0.tbm(external_aad); 300 | Ok(self.tag(create(&tbm)?)) 301 | } 302 | } 303 | 304 | /// Possible MAC contexts. 305 | #[derive(Clone, Copy, Debug)] 306 | pub enum MacContext { 307 | CoseMac, 308 | CoseMac0, 309 | } 310 | 311 | impl MacContext { 312 | /// Return the context string as per RFC 8152 section 6.3. 313 | fn text(&self) -> &'static str { 314 | match self { 315 | MacContext::CoseMac => "MAC", 316 | MacContext::CoseMac0 => "MAC0", 317 | } 318 | } 319 | } 320 | 321 | /// Create a binary blob that will be signed. 322 | // 323 | /// ```cddl 324 | /// MAC_structure = [ 325 | /// context : "MAC" / "MAC0", 326 | /// protected : empty_or_serialized_map, 327 | /// external_aad : bstr, 328 | /// payload : bstr 329 | /// ] 330 | /// ``` 331 | pub fn mac_structure_data( 332 | context: MacContext, 333 | protected: ProtectedHeader, 334 | external_aad: &[u8], 335 | payload: &[u8], 336 | ) -> Vec { 337 | let arr = vec![ 338 | Value::Text(context.text().to_owned()), 339 | protected.cbor_bstr().expect("failed to serialize header"), // safe: always serializable 340 | Value::Bytes(external_aad.to_vec()), 341 | Value::Bytes(payload.to_vec()), 342 | ]; 343 | 344 | let mut data = Vec::new(); 345 | cbor::ser::into_writer(&Value::Array(arr), &mut data).unwrap(); // safe: always serializable 346 | data 347 | } 348 | -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | //! Common internal utilities. 18 | 19 | use crate::{ 20 | cbor::value::{Integer, Value}, 21 | common::AsCborValue, 22 | CoseError, Result, 23 | }; 24 | use alloc::{boxed::Box, string::String, vec::Vec}; 25 | 26 | #[cfg(test)] 27 | mod tests; 28 | 29 | /// Return an error indicating that an unexpected CBOR type was encountered. 30 | pub(crate) fn cbor_type_error(value: &Value, want: &'static str) -> Result { 31 | let got = match value { 32 | Value::Integer(_) => "int", 33 | Value::Bytes(_) => "bstr", 34 | Value::Float(_) => "float", 35 | Value::Text(_) => "tstr", 36 | Value::Bool(_) => "bool", 37 | Value::Null => "nul", 38 | Value::Tag(_, _) => "tag", 39 | Value::Array(_) => "array", 40 | Value::Map(_) => "map", 41 | _ => "other", 42 | }; 43 | Err(CoseError::UnexpectedItem(got, want)) 44 | } 45 | 46 | /// Trait which augments the [`Value`] type with methods for convenient conversions to contained 47 | /// types which throw a [`CoseError`] if the Value is not of the expected type. 48 | pub(crate) trait ValueTryAs 49 | where 50 | Self: Sized, 51 | { 52 | /// Extractor for [`Value::Integer`] 53 | fn try_as_integer(self) -> Result; 54 | 55 | /// Extractor for [`Value::Bytes`] 56 | fn try_as_bytes(self) -> Result>; 57 | 58 | /// Extractor for [`Value::Bytes`] which also throws an error if the byte string is zero length 59 | fn try_as_nonempty_bytes(self) -> Result>; 60 | 61 | /// Extractor for [`Value::Array`] 62 | fn try_as_array(self) -> Result>; 63 | 64 | /// Extractor for [`Value::Array`] which applies `f` to each item to build a new [`Vec`] 65 | fn try_as_array_then_convert(self, f: F) -> Result> 66 | where 67 | F: Fn(Value) -> Result; 68 | 69 | /// Extractor for [`Value::Map`] 70 | fn try_as_map(self) -> Result>; 71 | 72 | /// Extractor for [`Value::Tag`] 73 | fn try_as_tag(self) -> Result<(u64, Box)>; 74 | 75 | /// Extractor for [`Value::Text`] 76 | fn try_as_string(self) -> Result; 77 | } 78 | 79 | impl ValueTryAs for Value { 80 | fn try_as_integer(self) -> Result { 81 | if let Value::Integer(i) = self { 82 | Ok(i) 83 | } else { 84 | cbor_type_error(&self, "int") 85 | } 86 | } 87 | 88 | fn try_as_bytes(self) -> Result> { 89 | if let Value::Bytes(b) = self { 90 | Ok(b) 91 | } else { 92 | cbor_type_error(&self, "bstr") 93 | } 94 | } 95 | 96 | fn try_as_nonempty_bytes(self) -> Result> { 97 | let v = self.try_as_bytes()?; 98 | if v.is_empty() { 99 | return Err(CoseError::UnexpectedItem("empty bstr", "non-empty bstr")); 100 | } 101 | Ok(v) 102 | } 103 | 104 | fn try_as_array(self) -> Result> { 105 | if let Value::Array(a) = self { 106 | Ok(a) 107 | } else { 108 | cbor_type_error(&self, "array") 109 | } 110 | } 111 | 112 | fn try_as_array_then_convert(self, f: F) -> Result> 113 | where 114 | F: Fn(Value) -> Result, 115 | { 116 | self.try_as_array()? 117 | .into_iter() 118 | .map(f) 119 | .collect::, _>>() 120 | } 121 | 122 | fn try_as_map(self) -> Result> { 123 | if let Value::Map(a) = self { 124 | Ok(a) 125 | } else { 126 | cbor_type_error(&self, "map") 127 | } 128 | } 129 | 130 | fn try_as_tag(self) -> Result<(u64, Box)> { 131 | if let Value::Tag(a, v) = self { 132 | Ok((a, v)) 133 | } else { 134 | cbor_type_error(&self, "tag") 135 | } 136 | } 137 | 138 | fn try_as_string(self) -> Result { 139 | if let Value::Text(s) = self { 140 | Ok(s) 141 | } else { 142 | cbor_type_error(&self, "tstr") 143 | } 144 | } 145 | } 146 | 147 | /// Convert each item of an iterator to CBOR, and wrap the lot in 148 | /// a [`Value::Array`] 149 | pub fn to_cbor_array(c: C) -> Result 150 | where 151 | C: IntoIterator, 152 | C::Item: AsCborValue, 153 | { 154 | Ok(Value::Array( 155 | c.into_iter() 156 | .map(|e| e.to_cbor_value()) 157 | .collect::, _>>()?, 158 | )) 159 | } 160 | 161 | /// Check for an expected error. 162 | #[cfg(test)] 163 | pub fn expect_err( 164 | result: Result, 165 | err_msg: &str, 166 | ) { 167 | #[cfg(not(feature = "std"))] 168 | use alloc::format; 169 | match result { 170 | Ok(_) => { 171 | assert!( 172 | result.is_err(), 173 | "expected error containing '{}', got success {:?}", 174 | err_msg, 175 | result 176 | ); 177 | } 178 | Err(err) => { 179 | assert!( 180 | format!("{:?}", err).contains(err_msg), 181 | "unexpected error {:?}, doesn't contain '{}' (Debug impl)", 182 | err, 183 | err_msg 184 | ); 185 | assert!( 186 | format!("{}", err).contains(err_msg), 187 | "unexpected error {:?}, doesn't contain '{}' (Display impl)", 188 | err, 189 | err_msg 190 | ); 191 | } 192 | } 193 | } 194 | 195 | // Macros to reduce boilerplate when creating `CoseSomethingBuilder` structures. 196 | 197 | /// Add `new()` and `build()` methods to the builder. 198 | macro_rules! builder { 199 | ( $otype: ty ) => { 200 | /// Constructor for builder. 201 | pub fn new() -> Self { 202 | Self(<$otype>::default()) 203 | } 204 | /// Build the completed object. 205 | pub fn build(self) -> $otype { 206 | self.0 207 | } 208 | }; 209 | } 210 | 211 | /// Add a setter function for a field to the builder. 212 | macro_rules! builder_set { 213 | ( $name:ident: $ftype:ty ) => { 214 | /// Set the associated field. 215 | #[must_use] 216 | pub fn $name(mut self, $name: $ftype) -> Self { 217 | self.0.$name = $name; 218 | self 219 | } 220 | }; 221 | } 222 | 223 | /// Add a setter function for an optional field to the builder. 224 | macro_rules! builder_set_optional { 225 | ( $name:ident: $ftype:ty ) => { 226 | /// Set the associated field. 227 | #[must_use] 228 | pub fn $name(mut self, $name: $ftype) -> Self { 229 | self.0.$name = Some($name); 230 | self 231 | } 232 | }; 233 | } 234 | 235 | /// Add a setter function that fills out a `ProtectedHeader` from `Header` contents. 236 | macro_rules! builder_set_protected { 237 | ( $name:ident ) => { 238 | /// Set the associated field. 239 | #[must_use] 240 | pub fn $name(mut self, hdr: $crate::Header) -> Self { 241 | self.0.$name = $crate::ProtectedHeader { 242 | original_data: None, 243 | header: hdr, 244 | }; 245 | self 246 | } 247 | }; 248 | } 249 | -------------------------------------------------------------------------------- /src/util/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Google LLC 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | // 15 | //////////////////////////////////////////////////////////////////////////////// 16 | 17 | use super::*; 18 | use crate::{cbor::value::Value, util::expect_err}; 19 | use alloc::{borrow::ToOwned, boxed::Box, vec}; 20 | 21 | #[test] 22 | fn test_cbor_type_error() { 23 | let cases = vec![ 24 | (Value::Null, "nul"), 25 | (Value::Bool(true), "bool"), 26 | (Value::Bool(false), "bool"), 27 | (Value::from(128), "int"), 28 | (Value::from(-1), "int"), 29 | (Value::Bytes(vec![1, 2]), "bstr"), 30 | (Value::Text("string".to_owned()), "tstr"), 31 | (Value::Array(vec![Value::from(0)]), "array"), 32 | (Value::Map(vec![]), "map"), 33 | (Value::Tag(1, Box::new(Value::from(0))), "tag"), 34 | (Value::Float(1.054571817), "float"), 35 | ]; 36 | for (val, want) in cases { 37 | let e = cbor_type_error::<()>(&val, "a"); 38 | expect_err(e, want); 39 | } 40 | } 41 | 42 | #[test] 43 | #[should_panic] 44 | fn test_expect_err_but_ok() { 45 | let result: Result = Ok(42); 46 | expect_err(result, "absent text"); 47 | } 48 | 49 | #[test] 50 | #[should_panic] 51 | fn test_expect_err_wrong_msg() { 52 | let err = cbor_type_error::<()>(&Value::Bool(true), "a"); 53 | expect_err(err, "incorrect text"); 54 | } 55 | 56 | #[test] 57 | #[should_panic] 58 | fn test_expect_err_wrong_display_msg() { 59 | // Error type where `Debug` shows the message but `Display` doesn't 60 | #[allow(dead_code)] 61 | #[derive(Debug)] 62 | struct Error(&'static str); 63 | impl core::fmt::Display for Error { 64 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 65 | write!(f, "other") 66 | } 67 | } 68 | 69 | let err: Result = Err(Error("text")); 70 | // The expected text appears in the `Debug` output but not the `Display` output. 71 | expect_err(err, "text"); 72 | } 73 | --------------------------------------------------------------------------------