diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..82141e1 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,25 @@ +codecov: + require_ci_to_pass: yes + +coverage: + precision: 2 + round: down + range: "70...100" + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: false + +ignore: + - "**/build.rs" + - "**/benches/" + - "**/tests/" + - "**/codegen/" + - "**/*_test.rs" + - "**/*tests.rs" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e5da331 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,139 @@ +name: CI +on: + pull_request: + paths-ignore: + - README.md + push: + branches: main + paths-ignore: + - README.md + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - beta + - nightly-2020-10-19 + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + components: rustfmt + override: true + - run: cargo build --release --all + + test: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - stable + - beta + - nightly-2020-10-19 + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + components: rustfmt + override: true + - run: cargo test --all -- --nocapture + + formatting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-go@v2 + - run: go get github.com/campoy/embedmd + - uses: actions/setup-ruby@v1 + - run: gem install mdl + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly-2020-10-19 + override: true + components: rustfmt + - run: cargo fmt --all -- --check + - run: scripts/check-format.sh + + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly-2020-10-19 + override: true + components: rustfmt, clippy + - run: cargo clippy --all-features --all-targets + + doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + - run: cargo doc --no-deps --document-private-items + + udeps: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly-2020-10-19 + override: true + components: rustfmt + - uses: actions-rs/install@v0.1.2 + with: + crate: cargo-udeps + use-tool-cache: true + version: 0.1.15 + - run: cargo udeps + + deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly-2020-10-19 + override: true + components: rustfmt + - run: cargo install --locked --version 0.8.5 cargo-deny + - run: cargo deny check + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: nightly-2020-10-19 + override: true + components: rustfmt + - uses: actions-rs/install@v0.1.2 + with: + crate: cargo-tarpaulin + use-tool-cache: true + - run: cargo tarpaulin --verbose --ignore-tests --all-features --timeout=600 --out Xml + - name: Upload to codecov.io + run: | + bash <(curl -s https://codecov.io/bash) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.mdl-style.rb b/.mdl-style.rb new file mode 100644 index 0000000..f10cb94 --- /dev/null +++ b/.mdl-style.rb @@ -0,0 +1,18 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +all +rule 'MD013', :line_length => 120 +rule 'MD007', :indent => 4 +exclude_rule 'MD031' # embedmd markers are next to fenced code blocks +exclude_rule 'MD033' # allow inline HTML, esp. diff --git a/.mdlrc b/.mdlrc new file mode 100644 index 0000000..20c534f --- /dev/null +++ b/.mdlrc @@ -0,0 +1 @@ +style "#{File.dirname(__FILE__)}/.mdl-style.rb" \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..66ca745 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,9 @@ +# See https://github.com/rust-lang/rustfmt/blob/master/Configurations.md +comment_width = 100 +format_code_in_doc_comments = true +max_width = 100 +normalize_doc_attributes = true +wrap_comments = true +merge_imports = true +imports_layout = "mixed" +edition = "2018" diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..20370f0 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,7 @@ +# This is the list of significant contributors. +# +# This does not necessarily list everyone who has contributed code, +# especially since many employees of one corporation may be contributing. +# To see the full list of contributors, see the revision history in +# source control. +Google LLC \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..654a071 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution; +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. + +## Community Guidelines + +This project follows [Google's Open Source Community +Guidelines](https://opensource.google/conduct/). diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0c6a18f --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,94 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "coset" +version = "0.1.0" +dependencies = [ + "hex", + "maplit", + "serde", + "serde_cbor", +] + +[[package]] +name = "half" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62aca2aba2d62b4a7f5b33f3712cb1b0692779a56fb510499d5c0aa594daeaf3" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "proc-macro2" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd761ff957cb2a45fbb9ab3da6512de9de55872866160b23c25f1a841e99d29f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_cbor" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e18acfa2f90e8b735b2836ab8d538de304cbb6729a7360729ea5a895d15a622" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.124" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1800f7693e94e186f5e25a28291ae1570da908aff7d97a095dec1e56ff99069b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6e0333c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "coset" +version = "0.1.0" +authors = ["David Drysdale "] +edition = "2018" +license = "Apache-2.0" +description = "Set of types for supporting COSE" +keywords = ["cryptography", "cose"] +categories = ["cryptography"] + +[dependencies] +maplit = "^1.0" +serde_cbor = "^0.11.1" +serde = { version = "^1.0.123", features = ["derive"] } + +[dev-dependencies] +hex = "^0.4.2" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..facc365 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ +# COSET + +This repository holds a set of Rust types for working with CBOR Object Signing and Encryption (COSE) objects, as defined +in RFC 8152. It builds on the core CBOR parsing functionality from the [`serde_cbor` crate](https://docs.rs/serde_cbor). + +**Thist repo is under construction** and so details of the API and the code may change without warning. + +## Disclaimer + +This is not an officially supported Google product. diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..849416c --- /dev/null +++ b/deny.toml @@ -0,0 +1,32 @@ +# Configuration used for dependency checking with cargo-deny. +# +# For further details on all configuration options see: +# https://embarkstudios.github.io/cargo-deny/checks/cfg.html +targets = [ + { triple = "x86_64-unknown-linux-gnu" }, + { triple = "x86_64-apple-darwin" }, + { triple = "x86_64-pc-windows-msvc" }, +] + +# Deny all advisories unless explicitly ignored. +[advisories] +vulnerability = "deny" +unmaintained = "deny" +yanked = "deny" +notice = "deny" +ignore = [] + +# Deny multiple versions unless explicitly skipped. +[bans] +multiple-versions = "deny" +wildcards = "allow" + +###################################### + +# List of allowed licenses. +[licenses] +allow = [ + "Apache-2.0", + "MIT", +] +copyleft = "deny" diff --git a/dependabot.yml b/dependabot.yml new file mode 100644 index 0000000..7d87c2f --- /dev/null +++ b/dependabot.yml @@ -0,0 +1,9 @@ +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "daily" diff --git a/scripts/check-format.sh b/scripts/check-format.sh new file mode 100755 index 0000000..9b9c5bb --- /dev/null +++ b/scripts/check-format.sh @@ -0,0 +1,109 @@ +#!/usr/bin/env bash +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Find code files. +CODE_FILES=() +while IFS= read -r -d $'\0'; do + CODE_FILES+=("$REPLY") +done < <(find . -not \( -path '*/target' -prune \) -and -name '*.rs' -print0) + +# Find markdown files. +MD_FILES=() +while IFS= read -r -d $'\0'; do + MD_FILES+=("$REPLY") +done < <(find . -not \( -path '*/target' -prune \) -and -not \( -path '*/wycheproof' -prune \) -and -name '*.md' -print0) + +# Check that source files have the Apache License header. +# Automatically skips generated files. +check_license() { + local path="$1" + + if head -1 "$path" | grep -iq -e 'generated' -e '::prost::message'; then + return 0 + fi + + if echo "$path" | grep -q "/proto/"; then + return 0 + fi + + # Look for "Apache License" on the file header + if ! head -10 "$path" | grep -q 'Apache License'; then + # Format: $path:$line:$message + echo "$path:1:license header not found" + return 1 + fi + return 0 +} + +# Check that any TODO markers in files have associated issue numbers +check_todo() { + local path="$1" + local result + result=$(grep --with-filename --line-number TODO "$path" | grep --invert-match --regexp='TODO(#[0-9][0-9]*)') + if [[ -n $result ]]; then + echo "TODO marker without issue number:" + echo "$result" + return 1 + fi + return 0 +} + +# Check that any calls that might panic have a comment noting why they're safe +check_panic() { + local path="$1" + if [[ $path =~ "test" || $path =~ "examples/" || $path =~ "rinkey/" || $path =~ "benches/" ]]; then + return 0 + fi + for needle in "panic!(" "unwrap(" "expect(" "unwrap_err(" "expect_err(" "unwrap_none(" "expect_none(" "unreachable!"; do + local result + result=$(grep --with-filename --line-number "$needle" "$path" | grep --invert-match --regexp='safe:') + if [[ -n $result ]]; then + echo "Un-annotated panic code:" + echo "$result" + return 1 + fi + done + return 0 +} + +errcount=0 +for f in "${CODE_FILES[@]}"; do + check_license "$f" + errcount=$((errcount + $?)) + # TODO: enable once GitHub issues are available + # check_todo "$f" + errcount=$((errcount + $?)) + check_panic "$f" + errcount=$((errcount + $?)) +done + +EMBEDMD="$(go env GOPATH)/bin/embedmd" +if [[ ! -x "$EMBEDMD" ]]; then + go get github.com/campoy/embedmd +fi +for f in "${MD_FILES[@]}"; do + "$EMBEDMD" -d "$f" + errcount=$((errcount + $?)) + # TODO: enable once GitHub issues are available + # check_todo "$f" + errcount=$((errcount + $?)) + mdl "$f" + errcount=$((errcount + $?)) +done + +if [ $errcount -gt 0 ]; then + echo "$errcount errors detected" + exit 1 +fi diff --git a/src/common/mod.rs b/src/common/mod.rs new file mode 100644 index 0000000..e9f1b13 --- /dev/null +++ b/src/common/mod.rs @@ -0,0 +1,249 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +//! Common types. + +use crate::{ + iana, + iana::{EnumI128, WithPrivateRange}, + util::{cbor_type_error, AsCborValue}, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer}; +use serde_cbor as cbor; +use std::cmp::Ordering; + +#[cfg(test)] +mod tests; + +/// Extension trait that adds serialization/deserialization methods. +pub trait CborSerializable: Serialize + DeserializeOwned { + /// Create an object instance by reading serialized CBOR data from [`std::io::Read`] instance. + fn from_reader(reader: R) -> cbor::Result { + cbor::from_reader::(reader) + } + + /// Create an object instance from serialized CBOR data in a slice. + fn from_slice(slice: &[u8]) -> cbor::Result { + cbor::from_slice::(slice) + } + + /// Serialize this object to a vector. + fn to_vec(&self) -> cbor::Result> { + cbor::to_vec(self) + } + + /// Serialize this object to a [`std::io::Write`] instance. + fn to_writer(&self, writer: W) -> cbor::Result<()> { + cbor::to_writer(writer, self) + } +} + +/// Algorithm identifier. +pub type Algorithm = crate::RegisteredLabelWithPrivate; + +/// A COSE label may be either a signed integer value or a string. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Label { + Int(i128), + Text(String), +} + +impl CborSerializable for Label {} + +/// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected. +/// +/// Note that this uses the ordering given by RFC 8949 section 4.2.1 (lexicographic ordering of +/// encoded form), which is *different* from the canonical ordering defined in RFC 7049 section 3.9 +/// (where the primary sorting criterion is the length of the encoded form) +impl Ord for Label { + fn cmp(&self, other: &Self) -> Ordering { + self.to_cbor_value().cmp(&other.to_cbor_value()) + } +} +impl PartialOrd for Label { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl AsCborValue for Label { + fn from_cbor_value(value: cbor::Value) -> Result { + match value { + cbor::Value::Integer(i) => Ok(Label::Int(i)), + cbor::Value::Text(t) => Ok(Label::Text(t)), + v => cbor_type_error(&v, &"int/tstr"), + } + } + fn to_cbor_value(&self) -> cbor::Value { + match self { + Label::Int(i) => cbor::Value::Integer(*i as i128), + Label::Text(t) => cbor::Value::Text(t.clone()), + } + } +} + +impl Serialize for Label { + fn serialize(&self, serializer: S) -> Result { + match self { + Label::Int(i) => serializer.serialize_i128(*i as i128), + Label::Text(t) => serializer.serialize_str(t), + } + } +} + +impl<'de> Deserialize<'de> for Label { + fn deserialize>(deserializer: D) -> Result { + Self::from_cbor_value(cbor::Value::deserialize(deserializer)?) + } +} + +/// A COSE label which can be either a signed integer value or a string, but +/// where the allowed integer values are governed by IANA. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RegisteredLabel { + Assigned(T), + Text(String), +} + +impl CborSerializable for RegisteredLabel {} + +/// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected. +/// +/// Note that this uses the ordering given by RFC 8949 section 4.2.1 (lexicographic ordering of +/// encoded form), which is *different* from the canonical ordering defined in RFC 7049 section 3.9 +/// (where the primary sorting criterion is the length of the encoded form) +impl Ord for RegisteredLabel { + fn cmp(&self, other: &Self) -> Ordering { + self.to_cbor_value().cmp(&other.to_cbor_value()) + } +} +impl PartialOrd for RegisteredLabel { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl AsCborValue for RegisteredLabel { + fn from_cbor_value(value: cbor::Value) -> Result { + match value { + cbor::Value::Integer(i) => { + if let Some(a) = T::from_i128(i) { + Ok(RegisteredLabel::Assigned(a)) + } else { + Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Signed(i as i64), + &"recognized IANA value", + )) + } + } + cbor::Value::Text(t) => Ok(RegisteredLabel::Text(t)), + v => cbor_type_error(&v, &"int/tstr"), + } + } + fn to_cbor_value(&self) -> cbor::Value { + match self { + RegisteredLabel::Assigned(e) => cbor::Value::Integer(e.to_i128()), + RegisteredLabel::Text(t) => cbor::Value::Text(t.clone()), + } + } +} + +impl Serialize for RegisteredLabel { + fn serialize(&self, serializer: S) -> Result { + match self { + RegisteredLabel::Assigned(i) => serializer.serialize_i128(i.to_i128()), + RegisteredLabel::Text(t) => serializer.serialize_str(t), + } + } +} + +impl<'de, T: EnumI128> Deserialize<'de> for RegisteredLabel { + fn deserialize>(deserializer: D) -> Result { + Self::from_cbor_value(cbor::Value::deserialize(deserializer)?) + } +} + +/// A COSE label which can be either a signed integer value or a string, and +/// where the allowed integer values are governed by IANA but include a private +/// use range. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum RegisteredLabelWithPrivate { + PrivateUse(i128), + Assigned(T), + Text(String), +} + +impl CborSerializable for RegisteredLabelWithPrivate {} + +/// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected. +/// +/// Note that this uses the ordering given by RFC 8949 section 4.2.1 (lexicographic ordering of +/// encoded form), which is *different* from the canonical ordering defined in RFC 7049 section 3.9 +/// (where the primary sorting criterion is the length of the encoded form) +impl Ord for RegisteredLabelWithPrivate { + fn cmp(&self, other: &Self) -> Ordering { + self.to_cbor_value().cmp(&other.to_cbor_value()) + } +} +impl PartialOrd for RegisteredLabelWithPrivate { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl AsCborValue for RegisteredLabelWithPrivate { + fn from_cbor_value(value: cbor::Value) -> Result { + match value { + cbor::Value::Integer(i) => { + if let Some(a) = T::from_i128(i) { + Ok(RegisteredLabelWithPrivate::Assigned(a)) + } else if T::is_private(i) { + Ok(RegisteredLabelWithPrivate::PrivateUse(i)) + } else { + Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Signed(i as i64), + &"value in IANA or private use range", + )) + } + } + cbor::Value::Text(t) => Ok(RegisteredLabelWithPrivate::Text(t)), + v => cbor_type_error(&v, &"int/tstr"), + } + } + fn to_cbor_value(&self) -> cbor::Value { + match self { + RegisteredLabelWithPrivate::PrivateUse(i) => cbor::Value::Integer(*i), + RegisteredLabelWithPrivate::Assigned(e) => cbor::Value::Integer(e.to_i128()), + RegisteredLabelWithPrivate::Text(t) => cbor::Value::Text(t.clone()), + } + } +} + +impl Serialize for RegisteredLabelWithPrivate { + fn serialize(&self, serializer: S) -> Result { + match self { + RegisteredLabelWithPrivate::PrivateUse(i) => serializer.serialize_i128(*i), + RegisteredLabelWithPrivate::Assigned(i) => serializer.serialize_i128(i.to_i128()), + RegisteredLabelWithPrivate::Text(t) => serializer.serialize_str(t), + } + } +} + +impl<'de, T: EnumI128 + WithPrivateRange> Deserialize<'de> for RegisteredLabelWithPrivate { + fn deserialize>(deserializer: D) -> Result { + Self::from_cbor_value(cbor::Value::deserialize(deserializer)?) + } +} diff --git a/src/common/tests.rs b/src/common/tests.rs new file mode 100644 index 0000000..eaa264f --- /dev/null +++ b/src/common/tests.rs @@ -0,0 +1,41 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +use super::*; + +#[test] +fn test_label_encode() { + let tests = vec![ + (Label::Int(2), "02"), + (Label::Text("abc".to_owned()), "63616263"), + ]; + + for (i, (label, label_data)) in tests.iter().enumerate() { + let got = label.to_vec().unwrap(); + assert_eq!(*label_data, hex::encode(&got), "case {}", i); + + let got = Label::from_slice(&got).unwrap(); + assert_eq!(*label, got); + + // Also exercise the `Read` / `Write` versions. + let mut got = vec![]; + label.to_writer(&mut got).unwrap(); + assert_eq!(*label_data, hex::encode(&got), "case {}", i); + + let got = Label::from_reader(std::io::Cursor::new(&got)).unwrap(); + assert_eq!(*label, got); + } +} diff --git a/src/iana/mod.rs b/src/iana/mod.rs new file mode 100644 index 0000000..017b56e --- /dev/null +++ b/src/iana/mod.rs @@ -0,0 +1,611 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +//! Enumerations for IANA-managed values, from https://www.iana.org/assignments/cose/cose.xhtml. + +use serde::{Deserialize, Serialize}; + +#[cfg(test)] +mod tests; + +/// Trait indicating an enum that can be constructed from `i128` values. +pub trait EnumI128: Sized { + fn from_i128(i: i128) -> Option; + fn to_i128(&self) -> i128; +} + +/// Trait indicating an enum with a range of private values. +pub trait WithPrivateRange { + fn is_private(i: i128) -> bool; +} + +/// Generate an enum with associated values, plus a `from_i128` method. +macro_rules! iana_registry { + ( $(#[$attr:meta])* $enum_name:ident {$($(#[$fattr:meta])* $name:ident: $val:expr,)* } ) => { + #[allow(non_camel_case_types)] + $(#[$attr])* + #[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] + pub enum $enum_name { + $($(#[$fattr])* $name = $val,)* + } + impl EnumI128 for $enum_name { + fn from_i128(i: i128) -> Option { + match i { + $(x if x == Self::$name as i128 => Some(Self::$name),)* + _ => None, + } + } + #[inline] + fn to_i128(&self) -> i128 { + *self as i128 + } + } + } +} + +iana_registry! { + /// IANA-registered COSE header parameters. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#header-parameters + /// as of 2021-03-19. + HeaderParameter { + /// Reserved + Reserved: 0, + /// Cryptographic algorithm to use + /// + /// Associated value of type int / tstr + Alg: 1, + /// Critical headers to be understood + /// + /// Associated value of type [+ label] + Crit: 2, + /// Content type of the payload + /// + /// Associated value of type tstr / uint + ContentType: 3, + /// Key identifier + /// + /// Associated value of type bstr + Kid: 4, + /// Full Initialization Vector + /// + /// Associated value of type bstr + Iv: 5, + /// Partial Initialization Vector + /// + /// Associated value of type bstr + PartialIv: 6, + /// CBOR-encoded signature structure + /// + /// Associated value of type COSE_Signature / [+ COSE_Signature ] + CounterSignature: 7, + /// Counter signature with implied signer and headers + /// + /// Associated value of type bstr + CounterSignature0: 9, + /// Identifies the context for the key identifier + /// + /// Associated value of type bstr + KidContext: 10, + /// An unordered bag of X.509 certificates + /// + /// Associated value of type COSE_X509 + X5Bag: 32, + /// An ordered chain of X.509 certificates + /// + /// Associated value of type COSE_X509 + X5Chain: 33, + /// Hash of an X.509 certificate + /// + /// Associated value of type COSE_CertHash + X5T: 34, + /// URI pointing to an X.509 certificate + /// + /// Associated value of type uri + X5U: 35, + /// Challenge Nonce + /// + /// Associated value of type bstr + CuphNonce: 256, + /// Public Key + /// + /// Associated value of type array + CuphOwnerPubKey: 257, + } +} + +/// Integer values for COSE header parameters below this value are reserved for private use. +pub const HEADER_PARAMETER_PRIVATE_USE_MAX: i128 = -65536; + +impl WithPrivateRange for HeaderParameter { + fn is_private(i: i128) -> bool { + i < HEADER_PARAMETER_PRIVATE_USE_MAX + } +} + +iana_registry! { + /// IANA-registered COSE header algorithm parameters. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#header-algorithm-parameters + /// as of 2021-03-19. + HeaderAlgorithmParameter { + /// Party V other provided information + /// + /// Associated value of type bstr + PartyVOther: -26, + /// Party V provided nonce + /// + /// Associated value of type bstr / int + PartyVNonce: -25, + /// Party V identity information + /// + /// Associated value of type bstr + PartyVIdentity: -24, + /// Party U other provided information + /// + /// Associated value of type bstr + PartyUOther: -23, + /// Party U provided nonce + /// + /// Associated value of type bstr / int + PartyUNonce: -22, + /// Party U identity information + /// + /// Associated value of type bstr + PartyUIdentity: -21, + /// Random salt + /// + /// Associated value of type bstr + Salt: -20, + /// Static public key identifier for the sender + /// + /// Associated value of type bstr + StaticKeyId: -3, + /// Static public key for the sender + /// + /// Associated value of type COSE_Key + StaticKey: -2, + /// Ephemeral public key for the sender + /// + /// Associated value of type COSE_Key + EphemeralKey: -1, + } +} + +iana_registry! { + /// IANA-registered COSE algorithms. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#algorithms + /// as of 2021-03-19. + Algorithm { + /// RSASSA-PKCS1-v1_5 using SHA-1 + RS1: -65535, + /// WalnutDSA signature + WalnutDSA: -260, + /// RSASSA-PKCS1-v1_5 using SHA-512 + RS512: -259, + /// RSASSA-PKCS1-v1_5 using SHA-384 + RS384: -258, + /// RSASSA-PKCS1-v1_5 using SHA-256 + RS256: -257, + /// ECDSA using secp256k1 curve and SHA-256 + ES256K: -47, + /// HSS/LMS hash-based digital signature + HSS_LMS: -46, + /// SHAKE-256 512-bit Hash Value + SHAKE256: -45, + /// SHA-2 512-bit Hash + SHA_512: -44, + /// SHA-2 384-bit Hash + SHA_384: -43, + /// RSAES-OAEP w/ SHA-512 + RSAES_OAEP_SHA_512: -42, + /// RSAES-OAEP w/ SHA-256 + RSAES_OAEP_SHA_256: -41, + /// RSAES-OAEP w/ SHA-1 + RSAES_OAEP_RFC_8017_default: -40, + /// RSASSA-PSS w/ SHA-512 + PS512: -39, + /// RSASSA-PSS_SHA-384 + PS384: -38, + /// RSASSA-PSS w/ SHA-256 + PS256: -37, + /// ECDSA w/ SHA-512 + ES512: -36, + /// ECDSA w/ SHA-384 + ES384: -35, + /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 256-bit key + ECDH_SS_A256KW: -34, + /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 192-bit key + ECDH_SS_A192KW: -33, + /// ECDH SS w/ Concat KDF and AES Key Wrap w/ 128-bit key + ECDH_SS_A128KW: -32, + /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 256-bit key + ECDH_ES_A256KW: -31, + /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 192-bit key + ECDH_ES_A192KW: -30, + /// ECDH ES w/ Concat KDF and AES Key Wrap w/ 128-bit key + ECDH_ES_A128KW: -29, + /// ECDH SS w/ HKDF - generate key directly + ECDH_SS_HKDF_512: -28, + /// ECDH SS w/ HKDF - generate key directly + ECDH_SS_HKDF_256: -27, + /// ECDH ES w/ HKDF - generate key directly + ECDH_ES_HKDF_512: -26, + /// ECDH ES w/ HKDF - generate key directly + ECDH_ES_HKDF_256: -25, + /// SHAKE-128 256-bit Hash Value + SHAKE128: -18, + /// SHA-2 512-bit Hash truncated to 256-bits + SHA_512_256: -17, + /// SHA-2 256-bit Hash + SHA_256: -16, + /// SHA-2 256-bit Hash truncated to 64-bits + SHA_256_64: -15, + /// SHA-1 Hash + SHA_1: -14, + /// Shared secret w/ AES-MAC 256-bit key + Direct_HKDF_AES_256: -13, + /// Shared secret w/ AES-MAC 128-bit key + Direct_HKDF_AES_128: -12, + /// Shared secret w/ HKDF and SHA-512 + Direct_HKDF_SHA_512: -11, + /// Shared secret w/ HKDF and SHA-256 + Direct_HKDF_SHA_256: -10, + /// EdDSA + EdDSA: -8, + /// ECDSA w/ SHA-256 + ES256: -7, + /// Direct use of CEK + Direct: -6, + /// AES Key Wrap w/ 256-bit key + A256KW: -5, + /// AES Key Wrap w/ 192-bit key + A192KW: -4, + /// AES Key Wrap w/ 128-bit key + A128KW: -3, + /// AES-GCM mode w/ 128-bit key, 128-bit tag + A128GCM: 1, + /// AES-GCM mode w/ 192-bit key, 128-bit tag + A192GCM: 2, + /// AES-GCM mode w/ 256-bit key, 128-bit tag + A256GCM: 3, + /// HMAC w/ SHA-256 truncated to 64 bits + HMAC_256_64: 4, + /// HMAC w/ SHA-256 + HMAC_256_256: 5, + /// HMAC w/ SHA-384 + HMAC_384_384: 6, + /// HMAC w/ SHA-512 + HMAC_512_512: 7, + /// AES-CCM mode 128-bit key, 64-bit tag, 13-byte nonce + AES_CCM_16_64_128: 10, + /// AES-CCM mode 256-bit key, 64-bit tag, 13-byte nonce + AES_CCM_16_64_256: 11, + /// AES-CCM mode 128-bit key, 64-bit tag, 7-byte nonce + AES_CCM_64_64_128: 12, + /// AES-CCM mode 256-bit key, 64-bit tag, 7-byte nonce + AES_CCM_64_64_256: 13, + /// AES-MAC 128-bit key, 64-bit tag + AES_MAC_128_64: 14, + /// AES-MAC 256-bit key, 64-bit tag + AES_MAC_256_64: 15, + /// ChaCha20/Poly1305 w/ 256-bit key, 128-bit tag + ChaCha20Poly1305: 24, + /// AES-MAC 128-bit key, 128-bit tag + AES_MAC_128_128: 25, + /// AES-MAC 256-bit key, 128-bit tag + AES_MAC_256_128: 26, + /// AES-CCM mode 128-bit key, 128-bit tag, 13-byte nonce + AES_CCM_16_128_128: 30, + /// AES-CCM mode 256-bit key, 128-bit tag, 13-byte nonce + AES_CCM_16_128_256: 31, + /// AES-CCM mode 128-bit key, 128-bit tag, 7-byte nonce + AES_CCM_64_128_128: 32, + /// AES-CCM mode 256-bit key, 128-bit tag, 7-byte nonce + AES_CCM_64_128_256: 33, + /// For doing IV generation for symmetric algorithms. + IV_GENERATION: 34, + } +} + +/// Integer values for COSE algorithms below this value are reserved for private use. +pub const ALGORITHM_PRIVATE_USE_MAX: i128 = -65536; + +impl WithPrivateRange for Algorithm { + fn is_private(i: i128) -> bool { + i < ALGORITHM_PRIVATE_USE_MAX + } +} + +iana_registry! { + /// IANA-registered COSE common key parameters. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-common-parameters + /// as of 2021-03-19. + KeyParameter { + /// Reserved value. + Reserved: 0, + /// Identification of the key type + /// + /// Associated value of type tstr / int + Kty: 1, + /// Key identification value - match to kid in message + /// + /// Associated value of type bstr + Kid: 2, + /// Key usage restriction to this algorithm + /// + /// Associated value of type tstr / int + Alg: 3, + /// Restrict set of permissible operations + /// + /// Associated value of type [+ (tstr / int)] + KeyOps: 4, + /// Base IV to be XORed with Partial IVs + /// + /// Associated value of type bstr + BaseIv: 5, + } +} + +iana_registry! { + /// IANA-registered COSE key parameters for keys of type [`KeyType::OKP`]. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters + /// as of 2021-03-19. + OkpKeyParameter { + /// EC identifier - Taken from the "COSE Elliptic Curves" registry + /// + /// Associated value of type tstr / int + Crv: -1, + /// x-coordinate + /// + /// Associated value of type bstr + X: -2, + /// Private key + /// + /// Associated value of type bstr + D: -4, + } +} + +iana_registry! { + /// IANA-registered COSE key parameters for keys of type [`KeyType::EC2`]. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters + /// as of 2021-03-19. + Ec2KeyParameter { + /// EC identifier - Taken from the "COSE Elliptic Curves" registry + /// + /// Associated value of type tstr / int + Crv: -1, + /// Public Key + /// + /// Associated value of type bstr + X: -2, + /// y-coordinate + /// + /// Associated value of type bstr / bool + Y: -3, + /// Private key + /// + /// Associated value of type bstr + D: -4, + } +} + +iana_registry! { + /// IANA-registered COSE key parameters for keys of type [`KeyType::RSA`]. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters + /// as of 2021-03-19. + RsaKeyParameter { + /// The RSA modulus n + /// + /// Associated value of type bstr + N: -1, + /// The RSA public exponent e + /// + /// Associated value of type bstr + E: -2, + /// The RSA private exponent d + /// + /// Associated value of type bstr + D: -3, + /// The prime factor p of n + /// + /// Associated value of type bstr + P: -4, + /// The prime factor q of n + /// + /// Associated value of type bstr + Q: -5, + /// dP is d mod (p - 1) + /// + /// Associated value of type bstr + DP: -6, + /// dQ is d mod (q - 1) + /// + /// Associated value of type bstr + DQ: -7, + /// qInv is the CRT coefficient q^(-1) mod p + /// + /// Associated value of type bstr + QInv: -8, + /// Other prime infos, an array + /// + /// Associated value of type array + Other: -9, + /// a prime factor r_i of n, where i >= 3 + /// + /// Associated value of type bstr + RI: -10, + /// d_i = d mod (r_i - 1) + /// + /// Associated value of type bstr + DI: -11, + /// The CRT coefficient t_i = (r_1 * r_2 * ... * r_(i-1))^(-1) mod r_i + /// + /// Associated value of type bstr + TI: -12, + } +} + +iana_registry! { + /// IANA-registered COSE key parameters for keys of type [`KeyType::Symmetric`]. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters + /// as of 2021-03-19. + SymmetricKeyParameter { + /// Key Value + /// + /// Associated value of type bstr + K: -1, + } +} + +iana_registry! { + /// IANA-registered COSE key parameters for keys of type [`KeyType::HSS_LMS`]. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters + /// as of 2021-03-19. + HssLmsKeyParameter { + /// Public key for HSS/LMS hash-based digital signature + /// + /// Associated value of type bstr + Pub: -1, + } +} + +iana_registry! { + /// IANA-registered COSE key parameters for keys of type [`KeyType::WalnutDSA`]. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type-parameters + /// as of 2021-03-19. + WalnutDsaKeyParameter { + /// Group and Matrix (NxN) size + /// + /// Associated value of type uint + N: -1, + /// Finite field F_q + /// + /// Associated value of type uint + Q: -2, + /// List of T-values, enties in F_q + /// + /// Associated value of type array of uint + TValues: -3, + /// NxN Matrix of enties in F_q in column-major form + /// + /// Associated value of type array of array of uint + Matrix1: -4, + /// Permutation associated with matrix 1 + /// + /// Associated value of type array of uint + Permutation1: -5, + /// NxN Matrix of enties in F_q in column-major form + /// + /// Associated value of type array of array of uint + Matrix2: -6, + } +} + +iana_registry! { + /// IANA-registered COSE key types. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#key-type + /// as of 2021-03-19. + KeyType { + /// This value is reserved + Reserved: 0, + /// Octet Key Pair + OKP: 1, + /// Elliptic Curve Keys w/ x- and y-coordinate pair + EC2: 2, + /// RSA Key + RSA: 3, + /// Symmetric Keys + Symmetric: 4, + /// Public key for HSS/LMS hash-based digital signature + HSS_LMS: 5, + /// WalnutDSA public key + WalnutDSA: 6, + } +} + +iana_registry! { + /// IANA-registered COSE elliptic curves. + /// + /// From IANA registry https://www.iana.org/assignments/cose/cose.xhtml#elliptic-curves + /// as of 2021-03-19. + EllipticCurve { + Reserved: 0, + /// EC2: NIST P-256 also known as secp256r1 + P_256: 1, + /// EC2: NIST P-384 also known as secp384r1 + P_384: 2, + /// EC2: NIST P-521 also known as secp521r1 + P_521: 3, + /// OKP: X25519 for use w/ ECDH only + X25519: 4, + /// OKP: X448 for use w/ ECDH only + X448: 5, + /// OKP: Ed25519 for use w/ EdDSA only + Ed25519: 6, + /// OKP: Ed448 for use w/ EdDSA only + Ed448: 7, + /// EC2: SECG secp256k1 curve + Secp256k1: 8, + } +} + +/// Integer values for COSE elliptic curves below this value are reserved for private use. +pub const ELLIPTIC_CURVE_PRIVATE_USE_MAX: i128 = -65536; + +impl WithPrivateRange for EllipticCurve { + fn is_private(i: i128) -> bool { + i < ELLIPTIC_CURVE_PRIVATE_USE_MAX + } +} + +iana_registry! { + /// Key operation values. + /// + /// See RFC 8152 section 7.1 table 4. + KeyOperation { + /// Key is used to create signatures. Requires private key fields. + Sign: 1, + /// Key is used for verification of signatures. + Verify: 2, + /// Key is used for key transport encryption. + Encrypt: 3, + /// Key is used for key transport decryption. Requires private key fields. + Decrypt: 4, + /// Key is used for key wrap encryption. + WrapKey: 5, + /// Key is used for key wrap decryption. Requires private key fields. + UnwrapKey: 6, + /// Key is used for deriving keys. Requires private key fields. + DeriveKey: 7, + /// Key is used for deriving bits not to be used as a key. Requires private key fields. + DeriveBits: 8, + /// Key is used for creating MACs. + MacCreate: 9, + /// Key is used for validating MACs. + MacVerify: 10, + } +} diff --git a/src/iana/tests.rs b/src/iana/tests.rs new file mode 100644 index 0000000..56ae5a2 --- /dev/null +++ b/src/iana/tests.rs @@ -0,0 +1,38 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +use super::*; + +#[test] +fn test_algorithm_conversion() { + assert_eq!(Some(Algorithm::ES256), Algorithm::from_i128(-7)); + assert_eq!(Some(Algorithm::A128GCM), Algorithm::from_i128(1)); + assert_eq!(Algorithm::A128GCM as i128, 1); + assert_eq!(None, Algorithm::from_i128(0)); + assert_eq!(None, Algorithm::from_i128(-65538)); +} + +#[test] +fn test_header_param_private_range() { + assert_eq!(HeaderParameter::is_private(1), false); + assert_eq!(HeaderParameter::is_private(-70_000), true); +} + +#[test] +fn test_elliptic_curve_private_range() { + assert_eq!(EllipticCurve::is_private(1), false); + assert_eq!(EllipticCurve::is_private(-70_000), true); +} diff --git a/src/key/mod.rs b/src/key/mod.rs new file mode 100644 index 0000000..f985011 --- /dev/null +++ b/src/key/mod.rs @@ -0,0 +1,316 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +//! COSE_Key functionality. + +use crate::{ + iana, + iana::EnumI128, + util::{cbor_type_error, AsCborValue}, + Algorithm, Label, +}; +use maplit::btreemap; +use serde::{de::Unexpected, Deserialize, Serialize, Serializer}; +use serde_cbor as cbor; +use std::collections::{btree_map::Entry, BTreeMap, BTreeSet}; + +#[cfg(test)] +mod tests; + +/// Key type. +type KeyType = crate::RegisteredLabel; + +impl Default for KeyType { + fn default() -> Self { + KeyType::Assigned(iana::KeyType::Reserved) + } +} + +/// Key operation. +type KeyOperation = crate::RegisteredLabel; + +/// Structure representing a cryptographic key. +/// +/// ```cddl +/// COSE_Key = { +/// 1 => tstr / int, ; kty +/// ? 2 => bstr, ; kid +/// ? 3 => tstr / int, ; alg +/// ? 4 => [+ (tstr / int) ], ; key_ops +/// ? 5 => bstr, ; Base IV +/// * label => values +/// } +/// ``` +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct CoseKey { + /// Key type identification. + pub kty: KeyType, + /// Key identification. + pub kid: Vec, + /// Key use restriction to this algorithm. + pub alg: Option, + /// Restrict set of possible operations. + pub key_ops: BTreeSet, + /// Base IV to be xor-ed with partial IVs. + pub base_iv: Vec, + /// Any additional parameter values. + pub params: BTreeMap, +} + +/// A collection of [`CoseKey`] objects. +pub type CoseKeySet = Vec; + +impl crate::CborSerializable for CoseKey {} + +const KTY: cbor::Value = cbor::Value::Integer(iana::KeyParameter::Kty as i128); +const KID: cbor::Value = cbor::Value::Integer(iana::KeyParameter::Kid as i128); +const ALG: cbor::Value = cbor::Value::Integer(iana::KeyParameter::Alg as i128); +const KEY_OPS: cbor::Value = cbor::Value::Integer(iana::KeyParameter::KeyOps as i128); +const BASE_IV: cbor::Value = cbor::Value::Integer(iana::KeyParameter::BaseIv as i128); + +impl AsCborValue for CoseKey { + fn from_cbor_value(value: cbor::Value) -> Result { + let m = match value { + cbor::Value::Map(m) => m, + v => return cbor_type_error(&v, &"map"), + }; + + let mut key = Self::default(); + for (label, value) in m.into_iter() { + match label { + x if x == KTY => key.kty = KeyType::from_cbor_value(value)?, + + x if x == KID => match value { + cbor::Value::Bytes(v) => { + if v.is_empty() { + return Err(serde::de::Error::invalid_value( + Unexpected::Bytes(&v), + &"non-empty bstr", + )); + } + key.kid = v; + } + v => return cbor_type_error(&v, &"bstr value"), + }, + + x if x == ALG => key.alg = Some(Algorithm::from_cbor_value(value)?), + + x if x == KEY_OPS => match value { + cbor::Value::Array(key_ops) => { + for key_op in key_ops.into_iter() { + if !key.key_ops.insert(KeyOperation::from_cbor_value(key_op)?) { + return Err(serde::de::Error::invalid_value( + Unexpected::TupleVariant, + &"unique array label", + )); + } + } + if key.key_ops.is_empty() { + return Err(serde::de::Error::invalid_value( + Unexpected::TupleVariant, + &"non-empty array", + )); + } + } + v => return cbor_type_error(&v, &"array value"), + }, + + x if x == BASE_IV => match value { + cbor::Value::Bytes(v) => { + if v.is_empty() { + return Err(serde::de::Error::invalid_value( + Unexpected::Bytes(&v), + &"non-empty bstr", + )); + } + key.base_iv = v; + } + v => return cbor_type_error(&v, &"bstr value"), + }, + + l => { + let label = Label::from_cbor_value(l)?; + match key.params.entry(label) { + Entry::Occupied(_) => { + return Err(serde::de::Error::invalid_value( + Unexpected::StructVariant, + &"unique map label", + )); + } + Entry::Vacant(ve) => { + ve.insert(value); + } + } + } + } + } + // Check that key type has been set. + if key.kty == KeyType::Assigned(iana::KeyType::Reserved) { + return Err(serde::de::Error::invalid_value( + Unexpected::StructVariant, + &"mandatory kty label", + )); + } + + Ok(key) + } + + fn to_cbor_value(&self) -> cbor::Value { + let mut map = BTreeMap::::new(); + map.insert(KTY, self.kty.to_cbor_value()); + if !self.kid.is_empty() { + map.insert(KID, cbor::Value::Bytes(self.kid.to_vec())); + } + if let Some(alg) = &self.alg { + map.insert(ALG, alg.to_cbor_value()); + } + if !self.key_ops.is_empty() { + map.insert( + KEY_OPS, + cbor::Value::Array( + self.key_ops + .iter() + .map(|op| match op { + KeyOperation::Assigned(i) => cbor::Value::Integer(*i as i128), + KeyOperation::Text(t) => cbor::Value::Text(t.to_owned()), + }) + .collect(), + ), + ); + } + if !self.base_iv.is_empty() { + map.insert(BASE_IV, cbor::Value::Bytes(self.base_iv.to_vec())); + } + for (label, value) in &self.params { + map.insert(label.to_cbor_value(), value.clone()); + } + cbor::Value::Map(map) + } +} + +impl Serialize for CoseKey { + fn serialize(&self, serializer: S) -> Result { + self.to_cbor_value().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for CoseKey { + fn deserialize>(deserializer: D) -> Result { + Self::from_cbor_value(cbor::Value::deserialize(deserializer)?) + } +} + +/// Builder for [`CoseKey`] objects. +pub struct CoseKeyBuilder(CoseKey); + +impl CoseKeyBuilder { + /// Constructor for an elliptic curve public key specified by `x` and `y` coordinates. + pub fn new_ec2_pub_key(curve: iana::EllipticCurve, x: Vec, y: Vec) -> Self { + Self(CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: btreemap! { + Label::Int(iana::Ec2KeyParameter::Crv as i128) => cbor::Value::Integer(curve as i128), + Label::Int(iana::Ec2KeyParameter::X as i128) => cbor::Value::Bytes(x), + Label::Int(iana::Ec2KeyParameter::Y as i128) => cbor::Value::Bytes(y), + }, + ..Default::default() + }) + } + + /// Constructor for an elliptic curve public key specified by `x` coordinate plus sign of `y` + /// coordinate. + pub fn new_ec2_pub_key_y_sign(curve: iana::EllipticCurve, x: Vec, y_sign: bool) -> Self { + Self(CoseKey { + kty: KeyType::Assigned(iana::KeyType::EC2), + params: btreemap! { + Label::Int(iana::Ec2KeyParameter::Crv as i128) => cbor::Value::Integer(curve as i128), + Label::Int(iana::Ec2KeyParameter::X as i128) => cbor::Value::Bytes(x), + Label::Int(iana::Ec2KeyParameter::Y as i128) => cbor::Value::Bool(y_sign), + }, + ..Default::default() + }) + } + + /// Constructor for an elliptic curve private key specified by `d`, together with public `x` and + /// `y` coordinates. + pub fn new_ec2_priv_key( + curve: iana::EllipticCurve, + x: Vec, + y: Vec, + d: Vec, + ) -> Self { + let mut builder = Self::new_ec2_pub_key(curve, x, y); + builder.0.params.insert( + Label::Int(iana::Ec2KeyParameter::D as i128), + cbor::Value::Bytes(d), + ); + builder + } + + /// Constructor for a symmetric key specified by `k`. + pub fn new_symmetric_key(k: Vec) -> Self { + Self(CoseKey { + kty: KeyType::Assigned(iana::KeyType::Symmetric), + params: btreemap! { + Label::Int(iana::SymmetricKeyParameter::K as i128) => cbor::Value::Bytes(k), + }, + ..Default::default() + }) + } + + /// Set the key identifier. + pub fn key_id(mut self, kid: Vec) -> Self { + self.0.kid = kid; + self + } + + /// Set the algorithm. + pub fn algorithm(mut self, alg: iana::Algorithm) -> Self { + self.0.alg = Some(Algorithm::Assigned(alg)); + self + } + + /// Add a key operation. + pub fn add_key_op(mut self, op: iana::KeyOperation) -> Self { + self.0.key_ops.insert(KeyOperation::Assigned(op)); + self + } + + /// Set the base initialization vector. + pub fn base_iv(mut self, base_iv: Vec) -> Self { + self.0.base_iv = base_iv; + self + } + + /// Set a parameter value. + /// + /// # Panics + /// + /// This function will panic if it used to set a parameter label from the [`iana::KeyParameter`] + /// range. + pub fn param(mut self, label: i128, value: cbor::Value) -> Self { + if iana::KeyParameter::from_i128(label).is_some() { + panic!("param() method used to set KeyParameter"); // safe: invalid input + } + self.0.params.insert(Label::Int(label), value); + self + } + + /// Build the complete [`Key`] object. + pub fn build(self) -> CoseKey { + self.0 + } +} diff --git a/src/key/tests.rs b/src/key/tests.rs new file mode 100644 index 0000000..5e6c6d6 --- /dev/null +++ b/src/key/tests.rs @@ -0,0 +1,703 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +use super::*; +use crate::{iana, util::expect_err, CborSerializable}; +use maplit::{btreemap, btreeset}; +use serde_cbor as cbor; + +#[test] +fn test_cbor_sort() { + // Pairs of objects with the "smaller" first. + // Comparing `cbor::Value`s should give the same answer as comparing + // the encoded versions according to RFC 8949 section 4.2.1. + // This is *different* than the canonical ordering defined in + // RFC 7049 section 3.9, where the primary sorting criterion + // is the length of the encoded form. + let pairs = vec![ + ( + cbor::Value::Integer(0x1234), + cbor::Value::Text("a".to_owned()), + ), + ( + cbor::Value::Integer(0x1234), + cbor::Value::Text("ab".to_owned()), + ), + (cbor::Value::Integer(10), cbor::Value::Integer(-1)), + (cbor::Value::Integer(0x12), cbor::Value::Integer(0x1234)), + (cbor::Value::Integer(0x99), cbor::Value::Integer(0x1234)), + (cbor::Value::Integer(0x1234), cbor::Value::Integer(0x1235)), + ]; + for (left, right) in pairs.into_iter() { + let value_cmp = left.cmp(&right); + let left_data = cbor::to_vec(&left).unwrap(); + let right_data = cbor::to_vec(&right).unwrap(); + let data_cmp = left_data.cmp(&right_data); + + assert_eq!(value_cmp, std::cmp::Ordering::Less); + assert_eq!(data_cmp, std::cmp::Ordering::Less); + } +} + +#[test] +fn test_cose_key_encode() { + let tests = vec![ + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + kid: vec![1, 2, 3], + ..Default::default() + }, + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "02", "43", "010203" // 2 (kid) => 3-bstr + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + ..Default::default() + }, + concat!( + "a1", // 1-map + "01", "01", // 1 (kty) => OKP + ), + ), + ( + CoseKey { + kty: KeyType::Text("bc".to_owned()), + ..Default::default() + }, + concat!( + "a1", // 1-map + "01", "62", "6263" // 1 (kty) => "bc" + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + base_iv: vec![3, 2, 1], + ..Default::default() + }, + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "05", "43", "030201", // 5 (base_iv) => 3-bstr + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + alg: Some(Algorithm::Assigned(iana::Algorithm::ES256)), + ..Default::default() + }, + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "03", "26", // 3 (alg) => -7 + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + alg: Some(Algorithm::PrivateUse(-70_000)), + ..Default::default() + }, + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "03", "3a", "0001116f", // 3 (alg) => -70000 + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + alg: Some(Algorithm::Text("abc".to_owned())), + ..Default::default() + }, + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "03", "63", "616263", // 3 (alg) => "abc" + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + kid: vec![1, 2, 3], + key_ops: btreeset! { + KeyOperation::Assigned(iana::KeyOperation::Encrypt), + KeyOperation::Assigned(iana::KeyOperation::Decrypt), + KeyOperation::Text("abc".to_owned()), + }, + ..Default::default() + }, + concat!( + "a3", // 3-map + "01", "01", // 1 (kty) => OKP + "02", "43", "010203", // 2 (kid) => 3-bstr + "04", "83", "03", "04", "63616263", // 4 (key_ops) => 3-tuple [3,4,"abc"] + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + params: btreemap! { + Label::Int(0x46) => cbor::Value::Integer(0x47), + Label::Int(0x66) => cbor::Value::Integer(0x67), + }, + ..Default::default() + }, + concat!( + "a3", // 3-map + "01", "01", // 1 (kty) => OKP + "1846", "1847", // 46 => 47 (note canonical ordering) + "1866", "1867", // 66 => 67 + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + params: btreemap! { + Label::Text("a".to_owned()) => cbor::Value::Integer(0x67), + Label::Int(0x1234) => cbor::Value::Integer(0x47), + }, + ..Default::default() + }, + concat!( + "a3", // 3-map + "01", "01", // 1 (kty) => OKP + // note canonical ordering: lexicographic + "191234", "1847", // 0x1234 => 47 + "6161", "1867", // "a" => 67 + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::OKP), + params: btreemap! { + Label::Int(0x66) => cbor::Value::Integer(0x67), + Label::Text("a".to_owned()) => cbor::Value::Integer(0x47), + }, + ..Default::default() + }, + concat!( + "a3", // 3-map + "01", "01", // 1 (kty) => OKP + "1866", "1867", // 66 => 67 + "6161", "1847", // "a" => 47 + ), + ), + ( + CoseKeyBuilder::new_ec2_pub_key( + iana::EllipticCurve::P_256, + hex::decode("6b4ad240073b99cad65ab8417ce29c6844ad0ae77ce8b3f7e41233f5b9129465") + .unwrap(), + hex::decode("a7dc1c39391ab300f7b1787b6e569a031dd0750fe2509b880a41f06666fff785") + .unwrap(), + ) + .algorithm(iana::Algorithm::ES256) + .param(-70000, cbor::Value::Null) + .build(), + concat!( + "a60102032620012158206b4ad240073b", + "99cad65ab8417ce29c6844ad0ae77ce8", + "b3f7e41233f5b9129465225820a7dc1c", + "39391ab300f7b1787b6e569a031dd075", + "0fe2509b880a41f06666fff7853a0001", + "116ff6" + ), + ), + ]; + for (i, (key, key_data)) in tests.iter().enumerate() { + let got = cbor::ser::to_vec(&key).unwrap(); + assert_eq!(*key_data, hex::encode(&got), "case {}", i); + + let got = CoseKey::from_slice(&got).unwrap(); + assert_eq!(*key, got); + } + // Now combine all of the keys into a `CoseKeySet` + let keyset: CoseKeySet = tests.iter().map(|(l, _v)| l.clone()).collect(); + let mut keyset_data: Vec = vec![0x80u8 + (tests.len() as u8)]; // assumes fewer than 24 keys + for (_, key_data) in tests.iter() { + keyset_data.extend_from_slice(&hex::decode(key_data).unwrap()); + } + let got = cbor::ser::to_vec(&keyset).unwrap(); + assert_eq!(hex::encode(keyset_data), hex::encode(got)); +} + +#[test] +fn test_rfc8152_public_cose_key_decode() { + // Public keys from RFC8152 section 6.7.1. + // Note that map contents have been reordered into canonical order. + let tests = vec![ + ( + CoseKeyBuilder::new_ec2_pub_key( + iana::EllipticCurve::P_256, + hex::decode("65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d").unwrap(), + hex::decode("1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c").unwrap(), + ).key_id(b"meriadoc.brandybuck@buckland.example".to_vec()).build(), + concat!( + "a5", + "0102", + "0258246d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65", + "2001", + "21582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d", + "2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c", + ), + ), + ( + CoseKeyBuilder::new_ec2_pub_key( + iana::EllipticCurve::P_256, + hex::decode("bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff").unwrap(), + hex::decode("20138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e").unwrap(), + ).key_id(b"11".to_vec()).build(), + concat!("a5", + "0102", + "02423131", + "2001", + "215820bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff", + "22582020138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e", + ), + ), + ( + CoseKeyBuilder::new_ec2_pub_key( + iana::EllipticCurve::P_521, + hex::decode("0072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad").unwrap(), + hex::decode("01dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475").unwrap(), + ).key_id( + b"bilbo.baggins@hobbiton.example".to_vec()).build(), + concat!("a5", + "0102", + "02581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65", + "2003", + "2158420072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad", + "22584201dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475", + ), + ), + ( + CoseKeyBuilder::new_ec2_pub_key( + iana::EllipticCurve::P_256, + hex::decode("98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280").unwrap(), + hex::decode("f01400b089867804b8e9fc96c3932161f1934f4223069170d924b7e03bf822bb").unwrap(), + ).key_id(b"peregrin.took@tuckborough.example".to_vec()).build(), + concat!("a5", + "0102", + "025821706572656772696e2e746f6f6b407475636b626f726f7567682e6578616d706c65", + "2001", + "21582098f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280", + "225820f01400b089867804b8e9fc96c3932161f1934f4223069170d924b7e03bf822bb", + ) + ), + ]; + for (i, (key, key_data)) in tests.iter().enumerate() { + let got = cbor::ser::to_vec(&key).unwrap(); + assert_eq!(*key_data, hex::encode(&got), "case {}", i); + + let got = CoseKey::from_slice(&got).unwrap(); + assert_eq!(*key, got); + } + + // Now combine all of the keys into a `CoseKeySet` + let keyset: CoseKeySet = tests.iter().map(|(l, _v)| l.clone()).collect(); + let mut keyset_data: Vec = vec![0x80u8 + (tests.len() as u8)]; // assumes fewer than 24 keys + for (_, key_data) in tests.iter() { + keyset_data.extend_from_slice(&hex::decode(key_data).unwrap()); + } + let got = cbor::ser::to_vec(&keyset).unwrap(); + assert_eq!(hex::encode(keyset_data), hex::encode(got)); +} + +#[test] +fn test_rfc8152_private_cose_key_decode() { + // Private keys from RFC8152 section 6.7.2. + // Note that map contents have been reordered into canonical order. + let tests = vec![ + ( + CoseKeyBuilder::new_ec2_priv_key( + iana::EllipticCurve::P_256, + hex::decode("65eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d").unwrap(), + hex::decode("1e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c").unwrap(), + hex::decode("aff907c99f9ad3aae6c4cdf21122bce2bd68b5283e6907154ad911840fa208cf").unwrap(), + ).key_id(b"meriadoc.brandybuck@buckland.example".to_vec()).build(), + concat!( + "a6", + "0102", + "0258246d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65", + "2001", + "21582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d", + "2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c", + "235820aff907c99f9ad3aae6c4cdf21122bce2bd68b5283e6907154ad911840fa208cf", + ), + ), + ( + CoseKeyBuilder::new_ec2_priv_key( + iana::EllipticCurve::P_256, + hex::decode("bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff").unwrap(), + hex::decode("20138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e").unwrap(), + hex::decode("57c92077664146e876760c9520d054aa93c3afb04e306705db6090308507b4d3").unwrap(), + ).key_id(b"11".to_vec()).build(), + concat!("a6", + "0102", + "02423131", + "2001", + "215820bac5b11cad8f99f9c72b05cf4b9e26d244dc189f745228255a219a86d6a09eff", + "22582020138bf82dc1b6d562be0fa54ab7804a3a64b6d72ccfed6b6fb6ed28bbfc117e", + "23582057c92077664146e876760c9520d054aa93c3afb04e306705db6090308507b4d3", + ), + ), + ( + CoseKeyBuilder::new_ec2_priv_key( + iana::EllipticCurve::P_521, + hex::decode("0072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad").unwrap(), + hex::decode("01dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475").unwrap(), + hex::decode("00085138ddabf5ca975f5860f91a08e91d6d5f9a76ad4018766a476680b55cd339e8ab6c72b5facdb2a2a50ac25bd086647dd3e2e6e99e84ca2c3609fdf177feb26d").unwrap(), + ).key_id(b"bilbo.baggins@hobbiton.example".to_vec()).build(), + concat!("a6", + "0102", + "02581e62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65", + "2003", + "2158420072992cb3ac08ecf3e5c63dedec0d51a8c1f79ef2f82f94f3c737bf5de7986671eac625fe8257bbd0394644caaa3aaf8f27a4585fbbcad0f2457620085e5c8f42ad", + "22584201dca6947bce88bc5790485ac97427342bc35f887d86d65a089377e247e60baa55e4e8501e2ada5724ac51d6909008033ebc10ac999b9d7f5cc2519f3fe1ea1d9475", + "23584200085138ddabf5ca975f5860f91a08e91d6d5f9a76ad4018766a476680b55cd339e8ab6c72b5facdb2a2a50ac25bd086647dd3e2e6e99e84ca2c3609fdf177feb26d", + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::Symmetric), + kid: b"our-secret".to_vec(), + params: btreemap! { + Label::Int(iana::SymmetricKeyParameter::K as i128) => + cbor::Value::Bytes(hex::decode("849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188").unwrap()), + }, + ..Default::default() + }, + concat!("a3", + "0104", + "024a6f75722d736563726574", + "205820849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188", + ), + ), + ( + CoseKeyBuilder::new_ec2_priv_key( + iana::EllipticCurve::P_256, + hex::decode("98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280").unwrap(), + hex::decode("f01400b089867804b8e9fc96c3932161f1934f4223069170d924b7e03bf822bb").unwrap(), + hex::decode("02d1f7e6f26c43d4868d87ceb2353161740aacf1f7163647984b522a848df1c3").unwrap(), + ).key_id(b"peregrin.took@tuckborough.example".to_vec()).build(), + concat!("a6", + "0102", + "025821706572656772696e2e746f6f6b407475636b626f726f7567682e6578616d706c65", + "2001", + "21582098f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280", + "225820f01400b089867804b8e9fc96c3932161f1934f4223069170d924b7e03bf822bb", + "23582002d1f7e6f26c43d4868d87ceb2353161740aacf1f7163647984b522a848df1c3", + ) + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::Symmetric), + kid: b"our-secret2".to_vec(), + params: btreemap! { + Label::Int(iana::SymmetricKeyParameter::K as i128) => + cbor::Value::Bytes(hex::decode("849b5786457c1491be3a76dcea6c4271").unwrap()), + }, + ..Default::default() + }, + concat!("a3", + "0104", + "024b6f75722d73656372657432", + "2050849b5786457c1491be3a76dcea6c4271", + ), + ), + ( + CoseKey { + kty: KeyType::Assigned(iana::KeyType::Symmetric), + kid: b"018c0ae5-4d9b-471b-bfd6-eef314bc7037".to_vec(), + params: btreemap! { + Label::Int(iana::SymmetricKeyParameter::K as i128) => + cbor::Value::Bytes(hex::decode("849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188").unwrap()), + }, + ..Default::default() + }, + concat!("a3", + "0104", + "02582430313863306165352d346439622d343731622d626664362d656566333134626337303337", + "205820849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188", + ), + ), + ]; + for (i, (key, key_data)) in tests.iter().enumerate() { + let got = cbor::ser::to_vec(&key).unwrap(); + assert_eq!(*key_data, hex::encode(&got), "case {}", i); + + let got = CoseKey::from_slice(&got).unwrap(); + assert_eq!(*key, got); + } + + // Now combine all of the keys into a `CoseKeySet` + let keyset: CoseKeySet = tests.iter().map(|(l, _v)| l.clone()).collect(); + let mut keyset_data: Vec = vec![0x80u8 + (tests.len() as u8)]; // assumes fewer than 24 keys + for (_, key_data) in tests.iter() { + keyset_data.extend_from_slice(&hex::decode(key_data).unwrap()); + } + let got = cbor::ser::to_vec(&keyset).unwrap(); + assert_eq!(hex::encode(keyset_data), hex::encode(got)); +} + +#[test] +fn test_cose_key_decode_fail() { + let tests = vec![ + ( + concat!( + "82", // 2-tuple (invalid) + "01", "01", // 1 (kty) => OKP + "02", "43", "010203" // 2 (kid) => 3-bstr + ), + "expected map", + ), + ( + concat!( + "a2", // 2-map + "01", "11", // 1 (kty) => invalid value + "02", "43", "010203" // 2 (kid) => 3-bstr + ), + "expected recognized IANA value", + ), + ( + concat!( + "a2", // 2-map + "01", "4101", // 1 (kty) => 1-bstr (invalid value type) + "02", "43", "010203" // 2 (kid) => 3-bstr + ), + "expected int/tstr", + ), + ( + concat!( + "a1", // 1-map (no kty value) + "02", "41", "01", // 2 (kid) => 1-bstr + ), + "expected mandatory kty label", + ), + ( + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "02", "40", // 2 (kid) => 0-bstr + ), + "expected non-empty bstr", + ), + ( + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "02", "01", // 2 (kid) => int (invalid value type) + ), + "expected bstr", + ), + ( + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "03", "1899", // 3 (alg) => 0x99 + ), + "expected value in IANA or private use range", + ), + ( + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "03", "4101", // 3 (alg) => 1-bstr (invalid value type) + ), + "expected int/tstr", + ), + ( + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "04", "4101", // 4 (key_ops) => 1-bstr (invalid value type) + ), + "expected array", + ), + ( + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "04", "82", "03", "03", // 4 (key_ops) => 3-tuple [3,3] + ), + "expected unique array label", + ), + ( + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "04", "80", // 4 (key_ops) => 0-tuple [] + ), + "expected non-empty array", + ), + ( + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "04", "82", "03", "0b", // 4 (key_ops) => 3-tuple [3,11] + ), + "expected recognized IANA value", + ), + ( + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "05", "40", // 5 (base_iv) => 0-bstr + ), + "expected non-empty bstr", + ), + ( + concat!( + "a2", // 2-map + "01", "01", // 1 (kty) => OKP + "05", "01", // 5 (base_iv) => int (invalid value type) + ), + "expected bstr", + ), + ]; + for (key_data, err_msg) in tests.iter() { + let data = hex::decode(key_data).unwrap(); + let result = CoseKey::from_slice(&data); + expect_err(result, err_msg); + } +} + +// TODO: get serde_cbor to generate an error on duplicate keys in map +#[test] +#[ignore] +fn test_cose_key_decode_dup_fail() { + let tests = vec![ + ( + concat!( + "a3", // 3-map + "01", "01", // 1 (kty) => OKP + "1866", "1867", // 66 => 67 + "1866", "1847", // 66 => 47 + ), + "expected unique map label", + ), + ( + concat!( + "a3", // 3-map + "01", "01", // 1 (kty) => OKP + "02", "41", "01", // 2 (kid) => 1-bstr + "01", "01", // 1 (kty) => OKP (duplicate label) + ), + "expected unique map label", + ), + ]; + for (key_data, err_msg) in tests.iter() { + let data = hex::decode(key_data).unwrap(); + let result = CoseKey::from_slice(&data); + expect_err(result, err_msg); + } +} + +#[test] +fn test_key_builder() { + let tests = vec![ + ( + CoseKeyBuilder::new_symmetric_key(vec![1, 2, 3]).build(), + CoseKey { + kty: KeyType::Assigned(iana::KeyType::Symmetric), + params: btreemap! { + Label::Int(iana::SymmetricKeyParameter::K as i128) => + cbor::Value::Bytes(vec![1,2,3]), + }, + ..Default::default() + }, + ), + ( + CoseKeyBuilder::new_symmetric_key(vec![1, 2, 3]) + .algorithm(iana::Algorithm::A128GCM) + .build(), + CoseKey { + kty: KeyType::Assigned(iana::KeyType::Symmetric), + alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)), + params: btreemap! { + Label::Int(iana::SymmetricKeyParameter::K as i128) => + cbor::Value::Bytes(vec![1,2,3]), + }, + ..Default::default() + }, + ), + ( + CoseKeyBuilder::new_symmetric_key(vec![1, 2, 3]) + .key_id(vec![4, 5]) + .build(), + CoseKey { + kty: KeyType::Assigned(iana::KeyType::Symmetric), + kid: vec![4, 5], + params: btreemap! { + Label::Int(iana::SymmetricKeyParameter::K as i128) => + cbor::Value::Bytes(vec![1,2,3]), + }, + ..Default::default() + }, + ), + ( + CoseKeyBuilder::new_symmetric_key(vec![1, 2, 3]) + .add_key_op(iana::KeyOperation::Encrypt) + .add_key_op(iana::KeyOperation::Decrypt) + .build(), + CoseKey { + kty: KeyType::Assigned(iana::KeyType::Symmetric), + key_ops: btreeset! { + KeyOperation::Assigned(iana::KeyOperation::Encrypt), + KeyOperation::Assigned(iana::KeyOperation::Decrypt), + }, + params: btreemap! { + Label::Int(iana::SymmetricKeyParameter::K as i128) => + cbor::Value::Bytes(vec![1,2,3]), + }, + ..Default::default() + }, + ), + ( + CoseKeyBuilder::new_symmetric_key(vec![1, 2, 3]) + .base_iv(vec![4, 5]) + .build(), + CoseKey { + kty: KeyType::Assigned(iana::KeyType::Symmetric), + base_iv: vec![4, 5], + params: btreemap! { + Label::Int(iana::SymmetricKeyParameter::K as i128) => + cbor::Value::Bytes(vec![1,2,3]), + }, + ..Default::default() + }, + ), + ]; + for (got, want) in tests { + assert_eq!(got, want); + } +} + +#[test] +#[should_panic] +fn test_key_builder_core_param_panic() { + // Attempting to set a core `KeyParameter` (in range [1,5]) via `.param()` panics. + let _key = + CoseKeyBuilder::new_ec2_pub_key(iana::EllipticCurve::P_256, vec![1, 2, 3], vec![2, 3, 4]) + .param(1, cbor::Value::Null) + .build(); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a520b64 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,28 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +//! Set of types for supporting CBOR Object Signing and Encryption (COSE). + +#![deny(broken_intra_doc_links)] + +pub(crate) mod util; + +pub mod iana; + +mod common; +pub use common::*; +mod key; +pub use key::*; diff --git a/src/util/mod.rs b/src/util/mod.rs new file mode 100644 index 0000000..a165ca6 --- /dev/null +++ b/src/util/mod.rs @@ -0,0 +1,63 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +//! Common internal utilities. + +use serde_cbor as cbor; + +#[cfg(test)] +mod tests; + +/// Map a [`cbor::Value`] into a serde type error. +pub(crate) fn cbor_type_error(v: &cbor::Value, msg: &M) -> Result +where + M: serde::de::Expected, + E: serde::de::Error, +{ + Err(serde::de::Error::invalid_type( + match v { + cbor::Value::Integer(i) => serde::de::Unexpected::Signed(*i as i64), + cbor::Value::Text(t) => serde::de::Unexpected::Str(&t), + cbor::Value::Null => serde::de::Unexpected::Unit, + cbor::Value::Bool(b) => serde::de::Unexpected::Bool(*b), + cbor::Value::Float(f) => serde::de::Unexpected::Float(*f), + cbor::Value::Bytes(b) => serde::de::Unexpected::Bytes(b), + cbor::Value::Array(_) => serde::de::Unexpected::TupleVariant, + cbor::Value::Map(_) => serde::de::Unexpected::StructVariant, + _ => serde::de::Unexpected::Other("invalid type"), + }, + msg, + )) +} + +/// Trait for types that can be converted to/from a [`cbor::Value`] +pub(crate) trait AsCborValue: Sized { + fn from_cbor_value(value: cbor::Value) -> Result; + fn to_cbor_value(&self) -> cbor::Value; +} + +/// Check for an expected error. +#[cfg(test)] +pub fn expect_err(result: Result, err_msg: &str) { + assert!(result.is_err(), "expected error containing '{}'", err_msg); + let err = result.err(); + assert!( + format!("{:?}", err).contains(err_msg), + "unexpected error {:?}, doesn't contain '{}'", + err, + err_msg + ); +} diff --git a/src/util/tests.rs b/src/util/tests.rs new file mode 100644 index 0000000..aeefa93 --- /dev/null +++ b/src/util/tests.rs @@ -0,0 +1,52 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +//////////////////////////////////////////////////////////////////////////////// + +use super::*; +use crate::util::expect_err; +use maplit::btreemap; +use serde::de::value::Error; +use serde_cbor as cbor; + +#[test] +fn test_cbor_type_error() { + let val = cbor::Value::Null; + let e = cbor_type_error::<(), _, Error>(&val, &"a"); + expect_err(e, "unit value"); + let val = cbor::Value::Bool(true); + let e = cbor_type_error::<(), _, Error>(&val, &"a"); + expect_err(e, "boolean"); + let val = cbor::Value::Integer(128); + let e = cbor_type_error::<(), _, Error>(&val, &"a"); + expect_err(e, "integer"); + let val = cbor::Value::Float(64.0); + let e = cbor_type_error::<(), _, Error>(&val, &"a"); + expect_err(e, "float"); + let val = cbor::Value::Bytes(vec![1, 2]); + let e = cbor_type_error::<(), _, Error>(&val, &"a"); + expect_err(e, "byte array"); + let val = cbor::Value::Text("string".to_owned()); + let e = cbor_type_error::<(), _, Error>(&val, &"a"); + expect_err(e, "string"); + let val = cbor::Value::Array(vec![cbor::Value::Null]); + let e = cbor_type_error::<(), _, Error>(&val, &"a"); + expect_err(e, "tuple variant"); + let val = cbor::Value::Map(btreemap! {}); + let e = cbor_type_error::<(), _, Error>(&val, &"a"); + expect_err(e, "struct variant"); + let val = cbor::Value::Tag(1, Box::new(cbor::Value::Null)); + let e = cbor_type_error::<(), _, Error>(&val, &"a"); + expect_err(e, "invalid type"); +}