Merge 020f9917e9c69fddac2eca54612ec6727eaf1230 on remote branch

Change-Id: I9adb17328f0ffe8ed10bbac37a510c0ca0ff8d46
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/Android.bp b/Android.bp
new file mode 100644
index 0000000..45509ce
--- /dev/null
+++ b/Android.bp
@@ -0,0 +1,18 @@
+// This file is generated by cargo2android.py --run --device --dependencies.
+// Do not modify this file as changes will be overridden on upgrade.
+
+
+
+rust_library {
+    name: "libcoset",
+    host_supported: true,
+    crate_name: "coset",
+    cargo_env_compat: true,
+    cargo_pkg_version: "0.3.0",
+    srcs: ["src/lib.rs"],
+    edition: "2018",
+    rustlibs: [
+        "libciborium",
+        "libciborium_io",
+    ],
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..97eb7ff
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,61 @@
+# Change Log
+
+## 0.3.0 - 2022-01-19
+
+- Change to use `ciborium` as CBOR library. Breaking change with many knock-on effects:
+    - Re-export `ciborium` as `coset::cbor` (rather than `sk-cbor`).
+    - Use `ciborium`'s `Value` type rather than `sk-cbor`'s version.
+    - Change `CoseError` to no longer wrap `sk-cbor` errors.
+    - Drop `derive` of `Eq` for data types (`ciborium` supports float values, which are inherently non-`Eq`)
+    - Add `#[must_use]` attributes to builder methods.
+    - Update MSRV to 1.56.0, as `ciborium` is `edition=2021`
+- Use new `ProtectedHeader` type for protected headers (breaking change).  This variant of `Header` preserves any
+  originally-parsed data, so that calculations (signatures, decryption, etc.) over the data can use the bit-for-bit wire
+  data instead of a reconstituted (and potentially different) version.
+- Add more specific error cases to `CoseError` (breaking change):
+    - Use new `OutOfRangeIntegerValue` error when an integer value is too large for the representation used in this
+      crate.
+    - Use new `DuplicateMapKey` error when a CBOR map contains duplicate keys (and is thus invalid).
+    - Extend `DecodeFailed` error to include the underlying `ciborium::de::Error` value.
+    - Use new `ExtraneousData` error when data remains after reading a CBOR value.
+    - Rename `UnexpectedType` error to `UnexpectedItem` to reflect broader usage than type.
+- Add a crate-specific `Result` type whose `E` field defaults to `CoseError`.
+
+## 0.2.0 - 2021-12-09
+
+- Change to use `sk-cbor` as CBOR library, due to deprecation of `serde-cbor`. Breaking change with many knock-on
+  effects:
+    - Re-export `sk-cbor` as `coset::cbor`.
+    - Use `sk-cbor`'s `Value` type rather than `serde-cbor`'s version.
+    - Change encoding methods to consume `self`.
+    - Change encoding methods to be fallible.
+    - Move to be `no_std` (but still using `alloc`)
+    - Add `CoseError` error type and use throughout.
+    - Use `Vec` of pairs not `BTreeMap`s for CBOR map values.
+    - Use `i64` not `i128` for integer values throughout.
+    - Drop use of `serde`'s `Serialize` and `Deserialize` traits; instead…
+    - Add `CborSerializable` extension trait for conversion to/from bytes.
+    - Drop `from_tagged_reader` / `to_tagged_writer` methods from `TaggedCborSerializable` trait.
+    - Derive `Debug` for builders.
+    - Convert `CoseKeySet` to a newtype, and add standard traits.
+
+## 0.1.2 - 2021-08-24
+
+- Add fallible variants of builder methods that invoke closures (#20):
+    - `CoseRecipientBuilder::try_create_ciphertext()`
+    - `CoseEncryptBuilder::try_create_ciphertext()`
+    - `CoseEncrypt0Builder::try_create_ciphertext()`
+    - `CoseMacBuilder::try_create_tag()`
+    - `CoseMac0Builder::try_create_tag()`
+    - `CoseSignBuilder::try_add_created_signature()`
+    - `CoseSign1Builder::try_create_signature()`
+- Upgrade dependencies.
+
+## 0.1.1 - 2021-06-24
+
+- Make `KeyType` and `KeyOperation` public.
+- Upgrade dependencies.
+
+## 0.1.0 - 2021-05-18
+
+- Initial version, using `serde-cbor` as CBOR library.
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 <https://cla.developers.google.com/> 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.toml b/Cargo.toml
new file mode 100644
index 0000000..774a9b7
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "coset"
+version = "0.3.0"
+authors = ["David Drysdale <drysdale@google.com>", "Paul Crowley <paulcrowley@google.com>"]
+edition = "2018"
+license = "Apache-2.0"
+description = "Set of types for supporting COSE"
+repository = "https://github.com/google/coset"
+keywords = ["cryptography", "cose"]
+categories = ["cryptography"]
+
+[dependencies]
+ciborium = { version = "^0.2.0", default-features = false }
+ciborium-io = "^0.2.0"
+
+[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/METADATA b/METADATA
new file mode 100644
index 0000000..1faae87
--- /dev/null
+++ b/METADATA
@@ -0,0 +1,18 @@
+name: "coset"
+description:
+    "Set of types for supporting CBOR Object Signing and Encryption (COSE)."
+
+third_party {
+  url {
+    type: HOMEPAGE
+    value: "https://crates.io/crates/coset"
+  }
+  url {
+    type: ARCHIVE
+    value: "https://static.crates.io/crates/coset/coset-0.3.0.crate"
+  }
+  version: "0.3.0"
+  last_upgrade_date { year: 2022 month: 1 day: 26 }
+  license_type: NOTICE
+}
+
diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/MODULE_LICENSE_APACHE2
diff --git a/OWNERS b/OWNERS
new file mode 100644
index 0000000..f6f0a7f
--- /dev/null
+++ b/OWNERS
@@ -0,0 +1,6 @@
+include platform/prebuilts/rust:master:/OWNERS
+# Android Hardware Security
+paulcrowley@google.com
+drysdale@google.com
+asbel@google.com
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5b51f27
--- /dev/null
+++ b/README.md
@@ -0,0 +1,69 @@
+# COSET
+
+[![Docs](https://img.shields.io/badge/docs-rust-brightgreen?style=for-the-badge)](https://google.github.io/coset)
+[![CI Status](https://img.shields.io/github/workflow/status/google/coset/CI?color=blue&style=for-the-badge)](https://github.com/google/coset/actions?query=workflow%3ACI)
+[![codecov](https://img.shields.io/codecov/c/github/google/coset?style=for-the-badge)](https://codecov.io/gh/google/coset)
+
+This crate holds a set of Rust types for working with CBOR Object Signing and Encryption (COSE) objects, as defined in
+[RFC 8152](https://tools.ietf.org/html/rfc8152).  It builds on the core [CBOR](https://tools.ietf.org/html/rfc7049)
+parsing functionality from the [`ciborium` crate](https://docs.rs/ciborium).
+
+See [crate docs](https://google.github.io/coset/rust/coset/index.html), or the [signature
+example](examples/signature.rs) for documentation on how to use the code.
+
+**This repo is under construction** and so details of the API and the code may change without warning.
+
+## `no_std` Support
+
+This crate supports `no_std`, but uses the `alloc` crate.
+
+## Minimum Supported Rust Version
+
+MSRV is 1.56 (the main `ciborium` dependency is `edition="2021"`)
+
+## Integer Ranges
+
+CBOR supports integers in the range:
+
+```text
+[-18_446_744_073_709_551_616, -1] ∪ [0, 18_446_744_073_709_551_615]
+```
+
+which is [-2<sup>64</sup>, -1] ∪ [0, 2<sup>64</sup> - 1].
+
+This does not map onto a single Rust integer type, so different CBOR crates take different approaches.
+
+- The [`serde_cbor`](https://docs.rs/serde_cbor) crate uses a single `i128` integer type for all integer values, which
+  means that all CBOR integer values can be expressed, but there are also `i128` values that cannot be encoded in CBOR.
+  This also means that data size is larger.
+- The [`ciborium`](https://docs.rs/ciborium) also uses a single `i128` integer type internally, but wraps it in its own
+  [`Integer`](https://docs.rs/ciborium/latest/ciborium/value/struct.Integer.html) type and only implements `TryFrom`
+  (not `From`) for `i128` / `u128` conversions so that unrepresentable numbers can be rejected.
+- The [`sk-cbor`](https://docs.rs/sk-cbor) crate uses distinct types:
+    - positive numbers as u64, covering [0, 2<sup>64</sup> - 1]
+    - negative numbers as i64, covering [-2<sup>63</sup>, -1] (which means that some theoretically-valid large negative
+      values are not represented).
+
+This crate uses a single type to encompass both positive and negative values, but uses `i64` for that type to keep data
+sizes smaller.  This means that:
+
+- positive numbers in `i64` cover [0, 2<sup>63</sup> - 1]
+- negative numbers in `i64` cover [-2<sup>63</sup>, -1]
+
+and so there are large values &ndash; both positive and negative &ndash; which are not supported by this crate.
+
+## Working on the Code
+
+Local coding conventions are enforced by the [continuous integration jobs](.github/workflows) and include:
+
+- Build cleanly and pass all tests.
+- Free of [Clippy](https://github.com/rust-lang/rust-clippy) warnings.
+- Formatted with `rustfmt` using the local [rustfmt.toml](.rustfmt.toml) settings.
+- Compliance with local conventions:
+    - All `TODO` markers should be of form `TODO(#99)` and refer to an open GitHub issue.
+    - Calls to functions that can panic (`panic!`, `unwrap`, `expect`) should have a comment on the same line in the
+      form `// safe: reason` (or `/* safe: reason */`) to document the reason why panicking is acceptable.
+
+## 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/examples/signature.rs b/examples/signature.rs
new file mode 100644
index 0000000..4512df7
--- /dev/null
+++ b/examples/signature.rs
@@ -0,0 +1,91 @@
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Example program demonstrating signature creation.
+use coset::{iana, CborSerializable};
+
+#[derive(Copy, Clone)]
+struct FakeSigner {}
+
+// Use a fake signer/verifier (to avoid pulling in lots of dependencies).
+impl FakeSigner {
+    fn sign(&self, data: &[u8]) -> Vec<u8> {
+        data.to_vec()
+    }
+
+    fn verify(&self, sig: &[u8], data: &[u8]) -> Result<(), String> {
+        if sig != self.sign(data) {
+            Err("failed to verify".to_owned())
+        } else {
+            Ok(())
+        }
+    }
+}
+
+fn main() {
+    // Build a fake signer/verifier (to avoid pulling in lots of dependencies).
+    let signer = FakeSigner {};
+    let verifier = signer;
+
+    // Inputs.
+    let pt = b"This is the content";
+    let aad = b"this is additional data";
+
+    // Build a `CoseSign1` object.
+    let protected = coset::HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let sign1 = coset::CoseSign1Builder::new()
+        .protected(protected)
+        .payload(pt.to_vec())
+        .create_signature(aad, |pt| signer.sign(pt))
+        .build();
+
+    // Serialize to bytes.
+    let sign1_data = sign1.to_vec().unwrap();
+    println!(
+        "'{}' + '{}' => {}",
+        String::from_utf8_lossy(pt),
+        String::from_utf8_lossy(aad),
+        hex::encode(&sign1_data)
+    );
+
+    // At the receiving end, deserialize the bytes back to a `CoseSign1` object.
+    let mut sign1 = coset::CoseSign1::from_slice(&sign1_data).unwrap();
+
+    // Check the signature, which needs to have the same `aad` provided.
+    let result = sign1.verify_signature(aad, |sig, data| verifier.verify(sig, data));
+    println!("Signature verified: {:?}.", result);
+    assert!(result.is_ok());
+
+    // Changing an unprotected header leaves the signature valid.
+    sign1.unprotected.content_type = Some(coset::ContentType::Text("text/plain".to_owned()));
+    assert!(sign1
+        .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+        .is_ok());
+
+    // Providing a different `aad` means the signature won't validate.
+    assert!(sign1
+        .verify_signature(b"not aad", |sig, data| verifier.verify(sig, data))
+        .is_err());
+
+    // Changing a protected header invalidates the signature.
+    sign1.protected.header.content_type = Some(coset::ContentType::Text("text/plain".to_owned()));
+    assert!(sign1
+        .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+        .is_err());
+}
diff --git a/fuzz/.gitignore b/fuzz/.gitignore
new file mode 100644
index 0000000..572e03b
--- /dev/null
+++ b/fuzz/.gitignore
@@ -0,0 +1,4 @@
+
+target
+corpus
+artifacts
diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock
new file mode 100644
index 0000000..443dab6
--- /dev/null
+++ b/fuzz/Cargo.lock
@@ -0,0 +1,46 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "arbitrary"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "698b65a961a9d730fb45b6b0327e20207810c9f61ee421b082b27ba003f49e2b"
+
+[[package]]
+name = "cc"
+version = "1.0.67"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
+
+[[package]]
+name = "coset"
+version = "0.2.0"
+dependencies = [
+ "sk-cbor",
+]
+
+[[package]]
+name = "coset-fuzz"
+version = "0.0.0"
+dependencies = [
+ "coset",
+ "libfuzzer-sys",
+]
+
+[[package]]
+name = "libfuzzer-sys"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86c975d637bc2a2f99440932b731491fc34c7f785d239e38af3addd3c2fd0e46"
+dependencies = [
+ "arbitrary",
+ "cc",
+]
+
+[[package]]
+name = "sk-cbor"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e94879a793aba6e65d691f345cfd172c4cc924a78259d5f9612a2cbfb78847a"
diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml
new file mode 100644
index 0000000..435f2c0
--- /dev/null
+++ b/fuzz/Cargo.toml
@@ -0,0 +1,44 @@
+
+[package]
+name = "coset-fuzz"
+version = "0.0.0"
+authors = ["Automatically generated"]
+publish = false
+edition = "2018"
+
+[package.metadata]
+cargo-fuzz = true
+
+[dependencies]
+libfuzzer-sys = "0.4"
+
+[dependencies.coset]
+path = ".."
+
+# Prevent this from interfering with workspaces
+[workspace]
+members = ["."]
+
+[[bin]]
+name = "encrypt"
+path = "fuzz_targets/encrypt.rs"
+test = false
+doc = false
+
+[[bin]]
+name = "key"
+path = "fuzz_targets/key.rs"
+test = false
+doc = false
+
+[[bin]]
+name = "mac"
+path = "fuzz_targets/mac.rs"
+test = false
+doc = false
+
+[[bin]]
+name = "sign"
+path = "fuzz_targets/sign.rs"
+test = false
+doc = false
diff --git a/fuzz/fuzz_targets/encrypt.rs b/fuzz/fuzz_targets/encrypt.rs
new file mode 100644
index 0000000..b579053
--- /dev/null
+++ b/fuzz/fuzz_targets/encrypt.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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Fuzz COSE_Encrypt* parsing.
+
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+use coset::CborSerializable;
+
+fuzz_target!(|data: &[u8]| {
+    let _recipient = coset::CoseRecipient::from_slice(data);
+    let _encrypt = coset::CoseEncrypt::from_slice(data);
+    let _encrypt0 = coset::CoseEncrypt0::from_slice(data);
+});
diff --git a/fuzz/fuzz_targets/key.rs b/fuzz/fuzz_targets/key.rs
new file mode 100644
index 0000000..24217e9
--- /dev/null
+++ b/fuzz/fuzz_targets/key.rs
@@ -0,0 +1,26 @@
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Fuzz COSE_Key parsing.
+
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+use coset::CborSerializable;
+
+fuzz_target!(|data: &[u8]| {
+    let _key = coset::CoseKey::from_slice(data);
+});
diff --git a/fuzz/fuzz_targets/mac.rs b/fuzz/fuzz_targets/mac.rs
new file mode 100644
index 0000000..5d5ec09
--- /dev/null
+++ b/fuzz/fuzz_targets/mac.rs
@@ -0,0 +1,27 @@
+// 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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Fuzz COSE_Mac* parsing.
+
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+use coset::CborSerializable;
+
+fuzz_target!(|data: &[u8]| {
+    let _mac = coset::CoseMac::from_slice(data);
+    let _mac0 = coset::CoseMac0::from_slice(data);
+});
diff --git a/fuzz/fuzz_targets/sign.rs b/fuzz/fuzz_targets/sign.rs
new file mode 100644
index 0000000..56242d4
--- /dev/null
+++ b/fuzz/fuzz_targets/sign.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.
+//
+////////////////////////////////////////////////////////////////////////////////
+
+//! Fuzz COSE_Sign* parsing.
+
+#![no_main]
+use libfuzzer_sys::fuzz_target;
+
+use coset::CborSerializable;
+
+fuzz_target!(|data: &[u8]| {
+    let _signature = coset::CoseSignature::from_slice(data);
+    let _sign = coset::CoseSign::from_slice(data);
+    let _sign1 = coset::CoseSign1::from_slice(data);
+});
diff --git a/patches/std.diff b/patches/std.diff
new file mode 100644
index 0000000..dd24015
--- /dev/null
+++ b/patches/std.diff
@@ -0,0 +1,15 @@
+diff --git a/src/lib.rs b/src/lib.rs
+index 2a8ceb3..3c46fcf 100644
+--- a/src/lib.rs
++++ b/src/lib.rs
+@@ -100,6 +100,9 @@
+ #![deny(rustdoc::broken_intra_doc_links)]
+ extern crate alloc;
+ 
++/// Use std to allow building as a dylib.
++extern crate std;
++
+ /// Re-export of the `ciborium` crate used for underlying CBOR encoding.
+ pub use ciborium as cbor;
+ 
+
diff --git a/scripts/build-gh-pages.sh b/scripts/build-gh-pages.sh
new file mode 100755
index 0000000..fb191ad
--- /dev/null
+++ b/scripts/build-gh-pages.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/env bash
+# Copyright 2020 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 -o errexit
+set -o nounset
+set -o xtrace
+set -o pipefail
+
+# Update the gh-pages branch. Note that `cargo doc` is **not deterministic** so
+# this should only be done when there is a real change.
+readonly RUST_BRANCH=${1:-main}
+readonly RUST_GH_BRANCH=gh-pages
+
+if [ -z "${FORCE+x}" ]; then
+  readonly PREV_COMMIT=$(git log --oneline -n 1 ${RUST_GH_BRANCH} | sed 's/.*branch at \([0-9a-f]*\)/\1/')
+  readonly CHANGES=$(git diff "${PREV_COMMIT}..${RUST_BRANCH}" | grep -e '[+-]//[/!]')
+
+  if [ -z "${CHANGES}" ]; then
+    echo "No doc comment changes found in ${PREV_COMMIT}..${RUST_BRANCH} subdir rust/"
+    exit 0
+  fi
+fi
+
+git switch "${RUST_BRANCH}"
+readonly RUST_BRANCH_SHA1=$(git rev-parse --short HEAD)
+readonly RUST_BRANCH_SUBJECT=$(git log -n 1 --format=format:%s)
+readonly COMMIT_MESSAGE=$(cat <<-END
+Update Rust docs to ${RUST_BRANCH} branch at ${RUST_BRANCH_SHA1}
+
+Auto-generated from commit ${RUST_BRANCH_SHA1} ("${RUST_BRANCH_SUBJECT}").
+END
+)
+
+readonly TGZ_FILE="/tmp/coset-doc-${RUST_BRANCH_SHA1}.tgz"
+# Build Cargo docs and save them off outside the repo
+(
+    rm -rf target/doc
+    cargo doc --no-deps
+    cargo deadlinks
+    cd target/doc || exit
+    tar czf "${TGZ_FILE}" ./*
+)
+
+# Shift to ${RUST_GH_BRANCH} branch and replace contents of (just) ./rust/
+git switch ${RUST_GH_BRANCH}
+
+readonly DOC_DIR=rust
+rm -rf ${DOC_DIR}
+mkdir ${DOC_DIR}
+(
+    cd "${DOC_DIR}" || exit
+    tar xzf "${TGZ_FILE}"
+)
+
+# Commit any differences
+git add "${DOC_DIR}"
+git commit --message="${COMMIT_MESSAGE}"
+git switch "${RUST_BRANCH}"
diff --git a/scripts/check-format.sh b/scripts/check-format.sh
new file mode 100755
index 0000000..445a564
--- /dev/null
+++ b/scripts/check-format.sh
@@ -0,0 +1,107 @@
+#!/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:'| grep --invert-match --regexp=':[0-9]*://')
+    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 + $?))
+  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 + $?))
+  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..bc602e6
--- /dev/null
+++ b/src/common/mod.rs
@@ -0,0 +1,359 @@
+// 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::{
+    cbor,
+    cbor::value::Value,
+    iana,
+    iana::{EnumI64, WithPrivateRange},
+    util::{cbor_type_error, AsCborValue, ValueTryAs},
+};
+use alloc::{boxed::Box, string::String, vec::Vec};
+use core::{cmp::Ordering, convert::TryInto};
+
+#[cfg(test)]
+mod tests;
+
+/// Marker structure indicating that the EOF was encountered when reading CBOR data.
+#[derive(Debug)]
+pub struct EndOfFile;
+
+/// Error type for failures in encoding or decoding COSE types.
+pub enum CoseError {
+    /// CBOR decoding failure.
+    DecodeFailed(cbor::de::Error<EndOfFile>),
+    /// Duplicate map key detected.
+    DuplicateMapKey,
+    /// CBOR encoding failure.
+    EncodeFailed,
+    /// CBOR input had extra data.
+    ExtraneousData,
+    /// Integer value on the wire is outside the range of integers representable in this crate.
+    /// See <https://crates.io/crates/coset/#integer-ranges>.
+    OutOfRangeIntegerValue,
+    /// Unexpected CBOR item encountered (got, want).
+    UnexpectedItem(&'static str, &'static str),
+    /// Unrecognized value in IANA-controlled range (with no private range).
+    UnregisteredIanaValue,
+    /// Unrecognized value in neither IANA-controlled range nor private range.
+    UnregisteredIanaNonPrivateValue,
+}
+
+/// Crate-specific Result type
+pub type Result<T, E = CoseError> = core::result::Result<T, E>;
+
+impl core::convert::From<cbor::de::Error<EndOfFile>> for CoseError {
+    fn from(e: cbor::de::Error<EndOfFile>) -> Self {
+        CoseError::DecodeFailed(e)
+    }
+}
+
+impl<T> core::convert::From<cbor::ser::Error<T>> for CoseError {
+    fn from(_e: cbor::ser::Error<T>) -> Self {
+        CoseError::EncodeFailed
+    }
+}
+
+impl core::convert::From<core::num::TryFromIntError> for CoseError {
+    fn from(_: core::num::TryFromIntError) -> Self {
+        CoseError::OutOfRangeIntegerValue
+    }
+}
+
+impl core::fmt::Debug for CoseError {
+    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+        match self {
+            CoseError::DecodeFailed(e) => write!(f, "decode CBOR failure: {}", e),
+            CoseError::DuplicateMapKey => write!(f, "duplicate map key"),
+            CoseError::EncodeFailed => write!(f, "encode CBOR failure"),
+            CoseError::ExtraneousData => write!(f, "extraneous data in CBOR input"),
+            CoseError::OutOfRangeIntegerValue => write!(f, "out of range integer value"),
+            CoseError::UnexpectedItem(got, want) => write!(f, "got {}, expected {}", got, want),
+            CoseError::UnregisteredIanaValue => write!(f, "expected recognized IANA value"),
+            CoseError::UnregisteredIanaNonPrivateValue => {
+                write!(f, "expected value in IANA or private use range")
+            }
+        }
+    }
+}
+
+/// Newtype wrapper around a byte slice to allow left-over data to be detected.
+struct MeasuringReader<'a>(&'a [u8]);
+
+impl<'a> MeasuringReader<'a> {
+    fn new(buf: &'a [u8]) -> MeasuringReader<'a> {
+        MeasuringReader(buf)
+    }
+
+    fn is_empty(&self) -> bool {
+        self.0.is_empty()
+    }
+}
+
+impl<'a> ciborium_io::Read for &mut MeasuringReader<'a> {
+    type Error = EndOfFile;
+
+    fn read_exact(&mut self, data: &mut [u8]) -> Result<(), Self::Error> {
+        if data.len() > self.0.len() {
+            return Err(EndOfFile);
+        }
+
+        let (prefix, suffix) = self.0.split_at(data.len());
+        data.copy_from_slice(prefix);
+        self.0 = suffix;
+        Ok(())
+    }
+}
+
+/// Read a CBOR [`Value`] from a byte slice, failing if any extra data remains after the `Value` has
+/// been read.
+fn read_to_value(slice: &[u8]) -> Result<Value> {
+    let mut mr = MeasuringReader::new(slice);
+    let value = cbor::de::from_reader(&mut mr)?;
+    if mr.is_empty() {
+        Ok(value)
+    } else {
+        Err(CoseError::ExtraneousData)
+    }
+}
+
+/// Extension trait that adds serialization/deserialization methods.
+pub trait CborSerializable: AsCborValue {
+    /// Create an object instance from serialized CBOR data in a slice.
+    fn from_slice(slice: &[u8]) -> Result<Self> {
+        Self::from_cbor_value(read_to_value(slice)?)
+    }
+
+    /// Serialize this object to a vector, consuming it along the way.
+    fn to_vec(self) -> Result<Vec<u8>> {
+        let mut data = Vec::new();
+        cbor::ser::into_writer(&self.to_cbor_value()?, &mut data)?;
+        Ok(data)
+    }
+}
+
+/// Extension trait that adds tagged serialization/deserialization methods.
+pub trait TaggedCborSerializable: AsCborValue {
+    /// The associated tag value.
+    const TAG: u64;
+
+    /// Create an object instance from serialized CBOR data in a slice, expecting an initial
+    /// tag value.
+    fn from_tagged_slice(slice: &[u8]) -> Result<Self> {
+        let (t, v) = read_to_value(slice)?.try_as_tag()?;
+        if t != Self::TAG {
+            return Err(CoseError::UnexpectedItem("tag", "other tag"));
+        }
+        Self::from_cbor_value(*v)
+    }
+
+    /// Serialize this object to a vector, including initial tag, consuming the object along the
+    /// way.
+    fn to_tagged_vec(self) -> Result<Vec<u8>> {
+        let mut data = Vec::new();
+        cbor::ser::into_writer(
+            &Value::Tag(Self::TAG, Box::new(self.to_cbor_value()?)),
+            &mut data,
+        )?;
+        Ok(data)
+    }
+}
+
+/// Algorithm identifier.
+pub type Algorithm = crate::RegisteredLabelWithPrivate<iana::Algorithm>;
+
+impl Default for Algorithm {
+    fn default() -> Self {
+        Algorithm::Assigned(iana::Algorithm::Reserved)
+    }
+}
+
+/// A COSE label may be either a signed integer value or a string.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum Label {
+    Int(i64),
+    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 {
+        match (self, other) {
+            (Label::Int(i1), Label::Int(i2)) => match (i1.signum(), i2.signum()) {
+                (-1, -1) => (-i1).cmp(&(-i2)),
+                (-1, 0) => Ordering::Greater,
+                (-1, 1) => Ordering::Greater,
+                (0, -1) => Ordering::Less,
+                (0, 0) => Ordering::Equal,
+                (0, 1) => Ordering::Less,
+                (1, -1) => Ordering::Less,
+                (1, 0) => Ordering::Greater,
+                (1, 1) => i1.cmp(i2),
+                (_, _) => unreachable!(), // safe: all possibilies covered
+            },
+            (Label::Int(_i1), Label::Text(_t2)) => Ordering::Less,
+            (Label::Text(_t1), Label::Int(_i2)) => Ordering::Greater,
+            (Label::Text(t1), Label::Text(t2)) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)),
+        }
+    }
+}
+
+impl PartialOrd for Label {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl AsCborValue for Label {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        match value {
+            Value::Integer(i) => Ok(Label::Int(i.try_into()?)),
+            Value::Text(t) => Ok(Label::Text(t)),
+            v => cbor_type_error(&v, "int/tstr"),
+        }
+    }
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(match self {
+            Label::Int(i) => Value::from(i),
+            Label::Text(t) => Value::Text(t),
+        })
+    }
+}
+
+/// 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<T: EnumI64> {
+    Assigned(T),
+    Text(String),
+}
+
+impl<T: EnumI64> CborSerializable for RegisteredLabel<T> {}
+
+/// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected.
+impl<T: EnumI64> Ord for RegisteredLabel<T> {
+    fn cmp(&self, other: &Self) -> Ordering {
+        match (self, other) {
+            (RegisteredLabel::Assigned(i1), RegisteredLabel::Assigned(i2)) => {
+                Label::Int(i1.to_i64()).cmp(&Label::Int(i2.to_i64()))
+            }
+            (RegisteredLabel::Assigned(_i1), RegisteredLabel::Text(_t2)) => Ordering::Less,
+            (RegisteredLabel::Text(_t1), RegisteredLabel::Assigned(_i2)) => Ordering::Greater,
+            (RegisteredLabel::Text(t1), RegisteredLabel::Text(t2)) => {
+                t1.len().cmp(&t2.len()).then(t1.cmp(t2))
+            }
+        }
+    }
+}
+
+impl<T: EnumI64> PartialOrd for RegisteredLabel<T> {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl<T: EnumI64> AsCborValue for RegisteredLabel<T> {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        match value {
+            Value::Integer(i) => {
+                if let Some(a) = T::from_i64(i.try_into()?) {
+                    Ok(RegisteredLabel::Assigned(a))
+                } else {
+                    Err(CoseError::UnregisteredIanaValue)
+                }
+            }
+            Value::Text(t) => Ok(RegisteredLabel::Text(t)),
+            v => cbor_type_error(&v, "int/tstr"),
+        }
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(match self {
+            RegisteredLabel::Assigned(e) => Value::from(e.to_i64()),
+            RegisteredLabel::Text(t) => Value::Text(t),
+        })
+    }
+}
+
+/// 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<T: EnumI64 + WithPrivateRange> {
+    PrivateUse(i64),
+    Assigned(T),
+    Text(String),
+}
+
+impl<T: EnumI64 + WithPrivateRange> CborSerializable for RegisteredLabelWithPrivate<T> {}
+
+/// Manual implementation of [`Ord`] to ensure that CBOR canonical ordering is respected.
+impl<T: EnumI64 + WithPrivateRange> Ord for RegisteredLabelWithPrivate<T> {
+    fn cmp(&self, other: &Self) -> Ordering {
+        use RegisteredLabelWithPrivate::{Assigned, PrivateUse, Text};
+        match (self, other) {
+            (Assigned(i1), Assigned(i2)) => Label::Int(i1.to_i64()).cmp(&Label::Int(i2.to_i64())),
+            (Assigned(i1), PrivateUse(i2)) => Label::Int(i1.to_i64()).cmp(&Label::Int(*i2)),
+            (PrivateUse(i1), Assigned(i2)) => Label::Int(*i1).cmp(&Label::Int(i2.to_i64())),
+            (PrivateUse(i1), PrivateUse(i2)) => Label::Int(*i1).cmp(&Label::Int(*i2)),
+            (Assigned(_i1), Text(_t2)) => Ordering::Less,
+            (PrivateUse(_i1), Text(_t2)) => Ordering::Less,
+            (Text(_t1), Assigned(_i2)) => Ordering::Greater,
+            (Text(_t1), PrivateUse(_i2)) => Ordering::Greater,
+            (Text(t1), Text(t2)) => t1.len().cmp(&t2.len()).then(t1.cmp(t2)),
+        }
+    }
+}
+
+impl<T: EnumI64 + WithPrivateRange> PartialOrd for RegisteredLabelWithPrivate<T> {
+    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+        Some(self.cmp(other))
+    }
+}
+
+impl<T: EnumI64 + WithPrivateRange> AsCborValue for RegisteredLabelWithPrivate<T> {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        match value {
+            Value::Integer(i) => {
+                let i = i.try_into()?;
+                if let Some(a) = T::from_i64(i) {
+                    Ok(RegisteredLabelWithPrivate::Assigned(a))
+                } else if T::is_private(i) {
+                    Ok(RegisteredLabelWithPrivate::PrivateUse(i))
+                } else {
+                    Err(CoseError::UnregisteredIanaNonPrivateValue)
+                }
+            }
+            Value::Text(t) => Ok(RegisteredLabelWithPrivate::Text(t)),
+            v => cbor_type_error(&v, "int/tstr"),
+        }
+    }
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(match self {
+            RegisteredLabelWithPrivate::PrivateUse(i) => Value::from(i),
+            RegisteredLabelWithPrivate::Assigned(i) => Value::from(i.to_i64()),
+            RegisteredLabelWithPrivate::Text(t) => Value::Text(t),
+        })
+    }
+}
diff --git a/src/common/tests.rs b/src/common/tests.rs
new file mode 100644
index 0000000..5cde798
--- /dev/null
+++ b/src/common/tests.rs
@@ -0,0 +1,372 @@
+// 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 alloc::{borrow::ToOwned, format, vec};
+use core::cmp::Ordering;
+
+#[test]
+fn test_error_convert() {
+    let e = CoseError::from(crate::cbor::ser::Error::<String>::Value(
+        "error message lost".to_owned(),
+    ));
+    match e {
+        CoseError::EncodeFailed => {
+            assert!(format!("{:?}", e).contains("encode CBOR failure"));
+        }
+        _ => panic!("unexpected error enum after conversion"),
+    }
+}
+
+#[test]
+fn test_label_encode() {
+    let tests = vec![
+        (Label::Int(2), "02"),
+        (Label::Int(-1), "20"),
+        (Label::Text("abc".to_owned()), "63616263"),
+    ];
+
+    for (i, (label, label_data)) in tests.iter().enumerate() {
+        let got = label.clone().to_vec().unwrap();
+        assert_eq!(*label_data, hex::encode(&got), "case {}", i);
+
+        let got = Label::from_slice(&got).unwrap();
+        assert_eq!(*label, got);
+    }
+}
+
+#[test]
+fn test_label_sort() {
+    // Pairs of `Label`s with the "smaller" first.
+    let pairs = vec![
+        (Label::Int(0x1234), Label::Text("a".to_owned())),
+        (Label::Int(0x1234), Label::Text("ab".to_owned())),
+        (Label::Int(0), Label::Text("ab".to_owned())),
+        (Label::Int(-1), Label::Text("ab".to_owned())),
+        (Label::Int(0), Label::Int(10)),
+        (Label::Int(0), Label::Int(-10)),
+        (Label::Int(10), Label::Int(-1)),
+        (Label::Int(-1), Label::Int(-2)),
+        (Label::Int(0x12), Label::Int(0x1234)),
+        (Label::Int(0x99), Label::Int(0x1234)),
+        (Label::Int(0x1234), Label::Int(0x1235)),
+        (Label::Text("a".to_owned()), Label::Text("ab".to_owned())),
+        (Label::Text("aa".to_owned()), Label::Text("ab".to_owned())),
+    ];
+    for (left, right) in pairs.into_iter() {
+        let value_cmp = left.cmp(&right);
+        let value_partial_cmp = left.partial_cmp(&right);
+        let left_data = left.clone().to_vec().unwrap();
+        let right_data = right.clone().to_vec().unwrap();
+        let data_cmp = left_data.cmp(&right_data);
+        let reverse_cmp = right.cmp(&left);
+        let equal_cmp = left.cmp(&left);
+
+        assert_eq!(value_cmp, Ordering::Less, "{:?} < {:?}", left, right);
+        assert_eq!(
+            value_partial_cmp,
+            Some(Ordering::Less),
+            "{:?} < {:?}",
+            left,
+            right
+        );
+        assert_eq!(
+            data_cmp,
+            Ordering::Less,
+            "{:?}={} < {:?}={}",
+            left,
+            hex::encode(&left_data),
+            right,
+            hex::encode(&right_data)
+        );
+        assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left);
+        assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left);
+    }
+}
+
+#[test]
+fn test_label_decode_fail() {
+    let tests = vec![
+        ("43010203", "expected int/tstr"),
+        ("", "decode CBOR failure: Io(EndOfFile"),
+        ("1e", "decode CBOR failure: Syntax"),
+        ("0202", "extraneous data"),
+    ];
+    for (label_data, err_msg) in tests.iter() {
+        let data = hex::decode(label_data).unwrap();
+        let result = Label::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_registered_label_encode() {
+    let tests = vec![
+        (RegisteredLabel::Assigned(iana::Algorithm::A192GCM), "02"),
+        (RegisteredLabel::Assigned(iana::Algorithm::EdDSA), "27"),
+        (RegisteredLabel::Text("abc".to_owned()), "63616263"),
+    ];
+
+    for (i, (label, label_data)) in tests.iter().enumerate() {
+        let got = label.clone().to_vec().unwrap();
+        assert_eq!(*label_data, hex::encode(&got), "case {}", i);
+
+        let got = RegisteredLabel::from_slice(&got).unwrap();
+        assert_eq!(*label, got);
+    }
+}
+
+#[test]
+fn test_registered_label_sort() {
+    use RegisteredLabel::{Assigned, Text};
+    // Pairs of `RegisteredLabel`s with the "smaller" first.
+    let pairs = vec![
+        (Assigned(iana::Algorithm::A192GCM), Text("a".to_owned())),
+        (Assigned(iana::Algorithm::WalnutDSA), Text("ab".to_owned())),
+        (Text("ab".to_owned()), Text("cd".to_owned())),
+        (Text("ab".to_owned()), Text("abcd".to_owned())),
+        (
+            Assigned(iana::Algorithm::AES_CCM_16_64_128),
+            Assigned(iana::Algorithm::A128KW),
+        ),
+        (
+            Assigned(iana::Algorithm::A192GCM),
+            Assigned(iana::Algorithm::AES_CCM_16_64_128),
+        ),
+    ];
+    for (left, right) in pairs.into_iter() {
+        let value_cmp = left.cmp(&right);
+        let value_partial_cmp = left.partial_cmp(&right);
+        let left_data = left.clone().to_vec().unwrap();
+        let right_data = right.clone().to_vec().unwrap();
+        let data_cmp = left_data.cmp(&right_data);
+        let reverse_cmp = right.cmp(&left);
+        let equal_cmp = left.cmp(&left);
+
+        assert_eq!(value_cmp, Ordering::Less, "{:?} < {:?}", left, right);
+        assert_eq!(
+            value_partial_cmp,
+            Some(Ordering::Less),
+            "{:?} < {:?}",
+            left,
+            right
+        );
+        assert_eq!(
+            data_cmp,
+            Ordering::Less,
+            "{:?}={} < {:?}={}",
+            left,
+            hex::encode(&left_data),
+            right,
+            hex::encode(&right_data)
+        );
+        assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left);
+        assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left);
+    }
+}
+
+#[test]
+fn test_registered_label_decode_fail() {
+    let tests = vec![
+        ("43010203", "expected int/tstr"),
+        ("", "decode CBOR failure: Io(EndOfFile"),
+        ("09", "expected recognized IANA value"),
+        ("394e1f", "expected recognized IANA value"),
+    ];
+    for (label_data, err_msg) in tests.iter() {
+        let data = hex::decode(label_data).unwrap();
+        let result = RegisteredLabel::<iana::EllipticCurve>::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+iana_registry! {
+    TestPrivateLabel {
+        Reserved: 0,
+        Something: 1,
+    }
+}
+
+impl WithPrivateRange for TestPrivateLabel {
+    fn is_private(i: i64) -> bool {
+        i > 10 || i < 1000
+    }
+}
+
+#[test]
+fn test_registered_label_with_private_encode() {
+    let tests = vec![
+        (
+            RegisteredLabelWithPrivate::Assigned(TestPrivateLabel::Something),
+            "01",
+        ),
+        (
+            RegisteredLabelWithPrivate::Text("abc".to_owned()),
+            "63616263",
+        ),
+        (
+            RegisteredLabelWithPrivate::PrivateUse(-70_000),
+            "3a0001116f",
+        ),
+        (RegisteredLabelWithPrivate::PrivateUse(11), "0b"),
+    ];
+
+    for (i, (label, label_data)) in tests.iter().enumerate() {
+        let got = label.clone().to_vec().unwrap();
+        assert_eq!(*label_data, hex::encode(&got), "case {}", i);
+
+        let got = RegisteredLabelWithPrivate::from_slice(&got).unwrap();
+        assert_eq!(*label, got);
+    }
+}
+
+#[test]
+fn test_registered_label_with_private_sort() {
+    use RegisteredLabelWithPrivate::{Assigned, PrivateUse, Text};
+    // Pairs of `RegisteredLabelWithPrivate`s with the "smaller" first.
+    let pairs = vec![
+        (Assigned(iana::Algorithm::A192GCM), Text("a".to_owned())),
+        (Assigned(iana::Algorithm::WalnutDSA), Text("ab".to_owned())),
+        (Text("ab".to_owned()), Text("cd".to_owned())),
+        (Text("ab".to_owned()), Text("abcd".to_owned())),
+        (
+            Assigned(iana::Algorithm::AES_CCM_16_64_128),
+            Assigned(iana::Algorithm::A128KW),
+        ),
+        (
+            Assigned(iana::Algorithm::A192GCM),
+            Assigned(iana::Algorithm::AES_CCM_16_64_128),
+        ),
+        (
+            Assigned(iana::Algorithm::AES_CCM_16_64_128),
+            PrivateUse(-70_000),
+        ),
+        (PrivateUse(-70_000), PrivateUse(-70_001)),
+        (PrivateUse(-70_000), Text("a".to_owned())),
+    ];
+    for (left, right) in pairs.into_iter() {
+        let value_cmp = left.cmp(&right);
+        let value_partial_cmp = left.partial_cmp(&right);
+        let left_data = left.clone().to_vec().unwrap();
+        let right_data = right.clone().to_vec().unwrap();
+        let data_cmp = left_data.cmp(&right_data);
+        let reverse_cmp = right.cmp(&left);
+        let equal_cmp = left.cmp(&left);
+
+        assert_eq!(value_cmp, Ordering::Less, "{:?} < {:?}", left, right);
+        assert_eq!(
+            value_partial_cmp,
+            Some(Ordering::Less),
+            "{:?} < {:?}",
+            left,
+            right
+        );
+        assert_eq!(
+            data_cmp,
+            Ordering::Less,
+            "{:?}={} < {:?}={}",
+            left,
+            hex::encode(&left_data),
+            right,
+            hex::encode(&right_data)
+        );
+        assert_eq!(reverse_cmp, Ordering::Greater, "{:?} > {:?}", right, left);
+        assert_eq!(equal_cmp, Ordering::Equal, "{:?} = {:?}", left, left);
+    }
+}
+
+#[test]
+fn test_registered_label_with_private_decode_fail() {
+    let tests = vec![
+        ("43010203", "expected int/tstr"),
+        ("", "decode CBOR failure: Io(EndOfFile"),
+        ("09", "expected value in IANA or private use range"),
+        ("394e1f", "expected value in IANA or private use range"),
+    ];
+    for (label_data, err_msg) in tests.iter() {
+        let data = hex::decode(label_data).unwrap();
+        let result = RegisteredLabelWithPrivate::<iana::Algorithm>::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+// The most negative integer value that can be encoded in CBOR is:
+//    0x3B (0b001_11011) 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
+// which is -18_446_744_073_709_551_616 (-1 - 18_446_744_073_709_551_615).
+//
+// However, this crate uses `i64` for all integers, which cannot hold
+// negative values below `i64::MIN` (=-2^63 = 0x8000000000000000).
+const CBOR_NINT_MIN_HEX: &str = "3b7fffffffffffffff";
+const CBOR_NINT_OUT_OF_RANGE_HEX: &str = "3b8000000000000000";
+
+// The largest positive integer value that can be encoded in CBOR is:
+//    0x1B (0b000_11011) 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF 0xFF
+// which is 18_446_744_073_709_551_615.
+//
+// However, this crate uses `i64` for all integers, which cannot hold
+// positive values above `i64::MAX` (=-2^63 - 1 = 0x7fffffffffffffff).
+const CBOR_INT_MAX_HEX: &str = "1b7fffffffffffffff";
+const CBOR_INT_OUT_OF_RANGE_HEX: &str = "1b8000000000000000";
+
+#[test]
+fn test_large_label_decode() {
+    let tests = vec![(CBOR_NINT_MIN_HEX, i64::MIN), (CBOR_INT_MAX_HEX, i64::MAX)];
+    for (label_data, want) in tests.iter() {
+        let data = hex::decode(label_data).unwrap();
+        let got = Label::from_slice(&data).unwrap();
+        assert_eq!(got, Label::Int(*want))
+    }
+}
+
+#[test]
+fn test_large_label_decode_fail() {
+    let tests = vec![
+        (CBOR_NINT_OUT_OF_RANGE_HEX, "out of range integer value"),
+        (CBOR_INT_OUT_OF_RANGE_HEX, "out of range integer value"),
+    ];
+    for (label_data, err_msg) in tests.iter() {
+        let data = hex::decode(label_data).unwrap();
+        let result = Label::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_large_registered_label_decode_fail() {
+    let tests = vec![
+        (CBOR_NINT_OUT_OF_RANGE_HEX, "out of range integer value"),
+        (CBOR_INT_OUT_OF_RANGE_HEX, "out of range integer value"),
+    ];
+    for (label_data, err_msg) in tests.iter() {
+        let data = hex::decode(label_data).unwrap();
+        let result = RegisteredLabel::<crate::iana::HeaderParameter>::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_large_registered_label_with_private_decode_fail() {
+    let tests = vec![
+        (CBOR_NINT_OUT_OF_RANGE_HEX, "out of range integer value"),
+        (CBOR_INT_OUT_OF_RANGE_HEX, "out of range integer value"),
+    ];
+    for (label_data, err_msg) in tests.iter() {
+        let data = hex::decode(label_data).unwrap();
+        let result = RegisteredLabelWithPrivate::<crate::iana::HeaderParameter>::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
diff --git a/src/context/mod.rs b/src/context/mod.rs
new file mode 100644
index 0000000..ee53afe
--- /dev/null
+++ b/src/context/mod.rs
@@ -0,0 +1,267 @@
+// 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_KDF_Context functionality.
+
+use crate::{
+    cbor::value::Value,
+    iana,
+    util::{cbor_type_error, AsCborValue, ValueTryAs},
+    Algorithm, CoseError, ProtectedHeader, Result,
+};
+use alloc::{vec, vec::Vec};
+use core::convert::TryInto;
+
+#[cfg(test)]
+mod tests;
+
+/// A nonce value.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum Nonce {
+    Bytes(Vec<u8>),
+    Integer(i64),
+}
+
+/// Structure representing a party involved in key derivation.
+///
+/// ```cddl
+///  PartyInfo = (
+///      identity : bstr / nil,
+///      nonce : bstr / int / nil,
+///      other : bstr / nil
+///  )
+///  ```
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct PartyInfo {
+    pub identity: Option<Vec<u8>>,
+    pub nonce: Option<Nonce>,
+    pub other: Option<Vec<u8>>,
+}
+
+impl crate::CborSerializable for PartyInfo {}
+
+impl AsCborValue for PartyInfo {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() != 3 {
+            return Err(CoseError::UnexpectedItem("array", "array with 3 items"));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        Ok(Self {
+            other: match a.remove(2) {
+                Value::Null => None,
+                Value::Bytes(b) => Some(b),
+                v => return cbor_type_error(&v, "bstr / nil"),
+            },
+            nonce: match a.remove(1) {
+                Value::Null => None,
+                Value::Bytes(b) => Some(Nonce::Bytes(b)),
+                Value::Integer(u) => Some(Nonce::Integer(u.try_into()?)),
+                v => return cbor_type_error(&v, "bstr / int / nil"),
+            },
+            identity: match a.remove(0) {
+                Value::Null => None,
+                Value::Bytes(b) => Some(b),
+                v => return cbor_type_error(&v, "bstr / nil"),
+            },
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(Value::Array(vec![
+            match self.identity {
+                None => Value::Null,
+                Some(b) => Value::Bytes(b),
+            },
+            match self.nonce {
+                None => Value::Null,
+                Some(Nonce::Bytes(b)) => Value::Bytes(b),
+                Some(Nonce::Integer(i)) => Value::from(i),
+            },
+            match self.other {
+                None => Value::Null,
+                Some(b) => Value::Bytes(b),
+            },
+        ]))
+    }
+}
+
+/// Builder for [`PartyInfo`] objects.
+#[derive(Debug, Default)]
+pub struct PartyInfoBuilder(PartyInfo);
+
+impl PartyInfoBuilder {
+    builder! {PartyInfo}
+    builder_set_optional! {identity: Vec<u8>}
+    builder_set_optional! {nonce: Nonce}
+    builder_set_optional! {other: Vec<u8>}
+}
+
+/// Structure representing supplemental public information.
+///
+/// ```cddl
+///  SuppPubInfo : [
+///      keyDataLength : uint,
+///      protected : empty_or_serialized_map,
+///      ? other : bstr
+///  ],
+///  ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct SuppPubInfo {
+    pub key_data_length: u64,
+    pub protected: ProtectedHeader,
+    pub other: Option<Vec<u8>>,
+}
+
+impl crate::CborSerializable for SuppPubInfo {}
+
+impl AsCborValue for SuppPubInfo {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() != 2 && a.len() != 3 {
+            return Err(CoseError::UnexpectedItem(
+                "array",
+                "array with 2 or 3 items",
+            ));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        Ok(Self {
+            other: {
+                if a.len() == 3 {
+                    Some(a.remove(2).try_as_bytes()?)
+                } else {
+                    None
+                }
+            },
+            protected: ProtectedHeader::from_cbor_bstr(a.remove(1))?,
+            key_data_length: a.remove(0).try_as_integer()?.try_into()?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        let mut v = vec![
+            Value::from(self.key_data_length),
+            self.protected.cbor_bstr()?,
+        ];
+        if let Some(other) = self.other {
+            v.push(Value::Bytes(other));
+        }
+        Ok(Value::Array(v))
+    }
+}
+
+/// Builder for [`SuppPubInfo`] objects.
+#[derive(Debug, Default)]
+pub struct SuppPubInfoBuilder(SuppPubInfo);
+
+impl SuppPubInfoBuilder {
+    builder! {SuppPubInfo}
+    builder_set! {key_data_length: u64}
+    builder_set_protected! {protected}
+    builder_set_optional! {other: Vec<u8>}
+}
+
+/// Structure representing a a key derivation context.
+/// ```cdl
+///  COSE_KDF_Context = [
+///      AlgorithmID : int / tstr,
+///      PartyUInfo : [ PartyInfo ],
+///      PartyVInfo : [ PartyInfo ],
+///      SuppPubInfo : [
+///          keyDataLength : uint,
+///          protected : empty_or_serialized_map,
+///          ? other : bstr
+///      ],
+///      ? SuppPrivInfo : bstr
+///  ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseKdfContext {
+    algorithm_id: Algorithm,
+    party_u_info: PartyInfo,
+    party_v_info: PartyInfo,
+    supp_pub_info: SuppPubInfo,
+    supp_priv_info: Vec<Vec<u8>>,
+}
+
+impl crate::CborSerializable for CoseKdfContext {}
+
+impl AsCborValue for CoseKdfContext {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() < 4 {
+            return Err(CoseError::UnexpectedItem(
+                "array",
+                "array with at least 4 items",
+            ));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        let mut supp_priv_info = Vec::with_capacity(a.len() - 4);
+        for i in (4..a.len()).rev() {
+            supp_priv_info.push(a.remove(i).try_as_bytes()?);
+        }
+        supp_priv_info.reverse();
+
+        Ok(Self {
+            supp_priv_info,
+            supp_pub_info: SuppPubInfo::from_cbor_value(a.remove(3))?,
+            party_v_info: PartyInfo::from_cbor_value(a.remove(2))?,
+            party_u_info: PartyInfo::from_cbor_value(a.remove(1))?,
+            algorithm_id: Algorithm::from_cbor_value(a.remove(0))?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        let mut v = vec![
+            self.algorithm_id.to_cbor_value()?,
+            self.party_u_info.to_cbor_value()?,
+            self.party_v_info.to_cbor_value()?,
+            self.supp_pub_info.to_cbor_value()?,
+        ];
+        for supp_priv_info in self.supp_priv_info {
+            v.push(Value::Bytes(supp_priv_info));
+        }
+        Ok(Value::Array(v))
+    }
+}
+
+/// Builder for [`CoseKdfContext`] objects.
+#[derive(Debug, Default)]
+pub struct CoseKdfContextBuilder(CoseKdfContext);
+
+impl CoseKdfContextBuilder {
+    builder! {CoseKdfContext}
+    builder_set! {party_u_info: PartyInfo}
+    builder_set! {party_v_info: PartyInfo}
+    builder_set! {supp_pub_info: SuppPubInfo}
+
+    /// Set the algorithm.
+    #[must_use]
+    pub fn algorithm(mut self, alg: iana::Algorithm) -> Self {
+        self.0.algorithm_id = Algorithm::Assigned(alg);
+        self
+    }
+
+    /// Add supplemental private info.
+    #[must_use]
+    pub fn add_supp_priv_info(mut self, supp_priv_info: Vec<u8>) -> Self {
+        self.0.supp_priv_info.push(supp_priv_info);
+        self
+    }
+}
diff --git a/src/context/tests.rs b/src/context/tests.rs
new file mode 100644
index 0000000..0ad2b36
--- /dev/null
+++ b/src/context/tests.rs
@@ -0,0 +1,391 @@
+// 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, HeaderBuilder};
+use alloc::vec;
+
+#[test]
+fn test_context_encode() {
+    let tests = vec![
+        (
+            CoseKdfContext::default(),
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+        ),
+        (
+            CoseKdfContextBuilder::new()
+                .algorithm(iana::Algorithm::A128GCM)
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "01", // int : AES-128-GCM
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+        ),
+        (
+            CoseKdfContextBuilder::new()
+                .algorithm(iana::Algorithm::A128GCM)
+                .party_u_info(PartyInfoBuilder::new().identity(vec![]).build())
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "01", // int : AES-128-GCM
+                "83", "40f6f6", // 3-tuple: [0-bstr, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+        ),
+        (
+            CoseKdfContextBuilder::new()
+                .algorithm(iana::Algorithm::A128GCM)
+                .party_u_info(
+                    PartyInfoBuilder::new()
+                        .identity(vec![3, 6])
+                        .nonce(Nonce::Integer(7))
+                        .build(),
+                )
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "01", // int : AES-128-GCM
+                "83", "420306", "07f6", // 3-tuple: [2-bstr, int, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+        ),
+        (
+            CoseKdfContextBuilder::new()
+                .algorithm(iana::Algorithm::A128GCM)
+                .party_u_info(
+                    PartyInfoBuilder::new()
+                        .identity(vec![3, 6])
+                        .nonce(Nonce::Integer(-2))
+                        .build(),
+                )
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "01", // int : AES-128-GCM
+                "83", "420306", "21f6", // 3-tuple: [2-bstr, nint, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+        ),
+        (
+            CoseKdfContextBuilder::new()
+                .algorithm(iana::Algorithm::A128GCM)
+                .party_v_info(
+                    PartyInfoBuilder::new()
+                        .identity(vec![3, 6])
+                        .nonce(Nonce::Bytes(vec![7, 3]))
+                        .build(),
+                )
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "01", // int : AES-128-GCM
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "420306", "420703", "f6", // 3-tuple: [2-bstr, 2-bstr, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+        ),
+        (
+            CoseKdfContextBuilder::new()
+                .algorithm(iana::Algorithm::A128GCM)
+                .party_v_info(
+                    PartyInfoBuilder::new()
+                        .identity(vec![3, 6])
+                        .other(vec![7, 3])
+                        .build(),
+                )
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "01", // int : AES-128-GCM
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "420306", "f6", "420703", // 3-tuple: [2-bstr, nil, 2-bstr]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+        ),
+        (
+            CoseKdfContextBuilder::new()
+                .supp_pub_info(
+                    SuppPubInfoBuilder::new()
+                        .key_data_length(10)
+                        .protected(
+                            HeaderBuilder::new()
+                                .algorithm(iana::Algorithm::A128GCM)
+                                .build(),
+                        )
+                        .build(),
+                )
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0a43", "a10101" // 2-tuple: [10, 3-bstr]
+            ),
+        ),
+        (
+            CoseKdfContextBuilder::new()
+                .supp_pub_info(
+                    SuppPubInfoBuilder::new()
+                        .key_data_length(10)
+                        .other(vec![1, 3, 5])
+                        .build(),
+                )
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "0a40", "43010305", // 3-tuple: [10, 0-bstr, 3-bstr]
+            ),
+        ),
+        (
+            CoseKdfContextBuilder::new()
+                .add_supp_priv_info(vec![1, 2, 3])
+                .build(),
+            concat!(
+                "85", // 5-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+                "43", "010203", // 3-bstr
+            ),
+        ),
+        (
+            CoseKdfContextBuilder::new()
+                .add_supp_priv_info(vec![1, 2, 3])
+                .add_supp_priv_info(vec![2, 3])
+                .add_supp_priv_info(vec![3])
+                .build(),
+            concat!(
+                "87", // 7-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+                "43", "010203", // 3-bstr
+                "42", "0203", // 2-bstr
+                "41", "03", // 1-bstr
+            ),
+        ),
+    ];
+    for (i, (key, key_data)) in tests.iter().enumerate() {
+        let got = key.clone().to_vec().unwrap();
+        assert_eq!(*key_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseKdfContext::from_slice(&got).unwrap();
+        got.supp_pub_info.protected.original_data = None;
+        assert_eq!(*key, got);
+    }
+}
+
+#[test]
+fn test_context_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a2", // 2-map
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "83", // 3-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+            ),
+            "expected array with at least 4 items",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+            ),
+            "decode CBOR failure: Io(EndOfFile",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "08", // int : unassigned value
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+            "expected value in IANA or private use range",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr : invalid
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+            "expected int/tstr",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "a1", "f6f6", // 1-map
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "84", "f6f6f6f6", // 4-tuple: [nil, nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+            "expected array with 3 items",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f660f6", // 3-tuple: [nil, 0-tstr, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+            "expected bstr / int / nil",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f660", // 3-tuple: [nil, nil, 0-tstr]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+            "expected bstr / nil",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "60f6f6", // 3-tuple: [0-tstr, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+            ),
+            "expected bstr / nil",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "a1", "0040", // 1-map: {0: 0-bstr}
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "81", "00", // 2-tuple: [0]
+            ),
+            "expected array with 2 or 3 items",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "4040", // 2-tuple: [0-bstr, 0-bstr]
+            ),
+            "expected int",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0060", // 2-tuple: [0, 0-tstr]
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "004060", // 3-tuple: [0, 0-bstr, 0-tstr]
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "85", // 5-tuple
+                "00", // int : reserved
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "83", "f6f6f6", // 3-tuple: [nil, nil, nil]
+                "82", "0040", // 2-tuple: [0, 0-bstr]
+                "60",   // 0-tstr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "01", // int : AES-128-GCM
+                "83", // 3-tuple: [0-bstr, out-of-range int, nil]
+                "401b8000000000000000f6",
+                "83", // 3-tuple: [nil, nil, nil]
+                "f6f6f6",
+                "82", // 2-tuple: [0, 0-bstr]
+                "0040",
+            ),
+            "out of range integer value",
+        ),
+    ];
+    for (context_data, err_msg) in tests.iter() {
+        let data = hex::decode(context_data).unwrap();
+        let result = CoseKdfContext::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
diff --git a/src/encrypt/mod.rs b/src/encrypt/mod.rs
new file mode 100644
index 0000000..589f611
--- /dev/null
+++ b/src/encrypt/mod.rs
@@ -0,0 +1,505 @@
+// 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_Encrypt functionality.
+
+use crate::{
+    cbor,
+    cbor::value::Value,
+    iana,
+    util::{cbor_type_error, to_cbor_array, AsCborValue, ValueTryAs},
+    CoseError, Header, ProtectedHeader, Result,
+};
+use alloc::{borrow::ToOwned, vec, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Structure representing the recipient of encrypted data.
+///
+/// ```cddl
+///  COSE_Recipient = [
+///      Headers,
+///      ciphertext : bstr / nil,
+///      ? recipients : [+COSE_recipient]
+///  ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseRecipient {
+    pub protected: ProtectedHeader,
+    pub unprotected: Header,
+    pub ciphertext: Option<Vec<u8>>,
+    pub recipients: Vec<CoseRecipient>,
+}
+
+impl crate::CborSerializable for CoseRecipient {}
+
+impl AsCborValue for CoseRecipient {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() != 3 && a.len() != 4 {
+            return Err(CoseError::UnexpectedItem(
+                "array",
+                "array with 3 or 4 items",
+            ));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        let recipients = if a.len() == 4 {
+            a.remove(3)
+                .try_as_array_then_convert(CoseRecipient::from_cbor_value)?
+        } else {
+            Vec::new()
+        };
+
+        Ok(Self {
+            recipients,
+            ciphertext: match a.remove(2) {
+                Value::Bytes(b) => Some(b),
+                Value::Null => None,
+                v => return cbor_type_error(&v, "bstr / null"),
+            },
+            unprotected: Header::from_cbor_value(a.remove(1))?,
+            protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        let mut v = vec![
+            self.protected.cbor_bstr()?,
+            self.unprotected.to_cbor_value()?,
+            match self.ciphertext {
+                None => Value::Null,
+                Some(b) => Value::Bytes(b),
+            },
+        ];
+        if !self.recipients.is_empty() {
+            v.push(to_cbor_array(self.recipients)?);
+        }
+        Ok(Value::Array(v))
+    }
+}
+
+impl CoseRecipient {
+    /// Decrypt the `ciphertext` value, using `cipher` to decrypt the cipher text and
+    /// combined AAD.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if no `ciphertext` is available. It will also panic
+    /// if the `context` parameter does not refer to a recipient context.
+    pub fn decrypt<F, E>(
+        &self,
+        context: EncryptionContext,
+        external_aad: &[u8],
+        cipher: F,
+    ) -> Result<Vec<u8>, E>
+    where
+        F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+    {
+        let ct = self.ciphertext.as_ref().unwrap(/* safe: documented */);
+        match context {
+            EncryptionContext::EncRecipient
+            | EncryptionContext::MacRecipient
+            | EncryptionContext::RecRecipient => {}
+            _ => panic!("unsupported encryption context {:?}", context), // safe: documented
+        }
+        let aad = enc_structure_data(context, self.protected.clone(), external_aad);
+        cipher(ct, &aad)
+    }
+}
+
+/// Builder for [`CoseRecipient`] objects.
+#[derive(Debug, Default)]
+pub struct CoseRecipientBuilder(CoseRecipient);
+
+impl CoseRecipientBuilder {
+    builder! {CoseRecipient}
+    builder_set_protected! {protected}
+    builder_set! {unprotected: Header}
+    builder_set_optional! {ciphertext: Vec<u8>}
+
+    /// Add a [`CoseRecipient`].
+    #[must_use]
+    pub fn add_recipient(mut self, recipient: CoseRecipient) -> Self {
+        self.0.recipients.push(recipient);
+        self
+    }
+
+    /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+    /// plaintext and combined AAD (in that order).  Any protected header values should be set
+    /// before using this method.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `context` parameter does not refer to a recipient context.
+    #[must_use]
+    pub fn create_ciphertext<F>(
+        self,
+        context: EncryptionContext,
+        plaintext: &[u8],
+        external_aad: &[u8],
+        cipher: F,
+    ) -> Self
+    where
+        F: FnOnce(&[u8], &[u8]) -> Vec<u8>,
+    {
+        let aad = self.aad(context, external_aad);
+        self.ciphertext(cipher(plaintext, &aad))
+    }
+
+    /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+    /// plaintext and combined AAD (in that order).  Any protected header values should be set
+    /// before using this method.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `context` parameter does not refer to a recipient context.
+    pub fn try_create_ciphertext<F, E>(
+        self,
+        context: EncryptionContext,
+        plaintext: &[u8],
+        external_aad: &[u8],
+        cipher: F,
+    ) -> Result<Self, E>
+    where
+        F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+    {
+        let aad = self.aad(context, external_aad);
+        Ok(self.ciphertext(cipher(plaintext, &aad)?))
+    }
+
+    /// Construct the combined AAD data needed for encryption. Any protected header values should be
+    /// set before using this method.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `context` parameter does not refer to a recipient context.
+    #[must_use]
+    fn aad(&self, context: EncryptionContext, external_aad: &[u8]) -> Vec<u8> {
+        match context {
+            EncryptionContext::EncRecipient
+            | EncryptionContext::MacRecipient
+            | EncryptionContext::RecRecipient => {}
+            _ => panic!("unsupported encryption context {:?}", context), // safe: documented
+        }
+        enc_structure_data(context, self.0.protected.clone(), external_aad)
+    }
+}
+
+/// Structure representing an encrypted object.
+///
+/// ```cddl
+///  COSE_Encrypt = [
+///      Headers,
+///      ciphertext : bstr / nil,
+///      recipients : [+COSE_recipient]
+///  ]
+///  ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseEncrypt {
+    pub protected: ProtectedHeader,
+    pub unprotected: Header,
+    pub ciphertext: Option<Vec<u8>>,
+    pub recipients: Vec<CoseRecipient>,
+}
+
+impl crate::CborSerializable for CoseEncrypt {}
+
+impl crate::TaggedCborSerializable for CoseEncrypt {
+    const TAG: u64 = iana::CborTag::CoseEncrypt as u64;
+}
+
+impl AsCborValue for CoseEncrypt {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() != 4 {
+            return Err(CoseError::UnexpectedItem("array", "array with 4 items"));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        let recipients = a
+            .remove(3)
+            .try_as_array_then_convert(CoseRecipient::from_cbor_value)?;
+        Ok(Self {
+            recipients,
+            ciphertext: match a.remove(2) {
+                Value::Bytes(b) => Some(b),
+                Value::Null => None,
+                v => return cbor_type_error(&v, "bstr"),
+            },
+            unprotected: Header::from_cbor_value(a.remove(1))?,
+            protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(Value::Array(vec![
+            self.protected.cbor_bstr()?,
+            self.unprotected.to_cbor_value()?,
+            match self.ciphertext {
+                None => Value::Null,
+                Some(b) => Value::Bytes(b),
+            },
+            to_cbor_array(self.recipients)?,
+        ]))
+    }
+}
+
+impl CoseEncrypt {
+    /// Decrypt the `ciphertext` value, using `cipher` to decrypt the cipher text and
+    /// combined AAD.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if no `ciphertext` is available.
+    pub fn decrypt<F, E>(&self, external_aad: &[u8], cipher: F) -> Result<Vec<u8>, E>
+    where
+        F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+    {
+        let ct = self.ciphertext.as_ref().unwrap(/* safe: documented */);
+        let aad = enc_structure_data(
+            EncryptionContext::CoseEncrypt,
+            self.protected.clone(),
+            external_aad,
+        );
+        cipher(ct, &aad)
+    }
+}
+
+/// Builder for [`CoseEncrypt`] objects.
+#[derive(Debug, Default)]
+pub struct CoseEncryptBuilder(CoseEncrypt);
+
+impl CoseEncryptBuilder {
+    builder! {CoseEncrypt}
+    builder_set_protected! {protected}
+    builder_set! {unprotected: Header}
+    builder_set_optional! {ciphertext: Vec<u8>}
+
+    /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+    /// plaintext and combined AAD (in that order).  Any protected header values should be set
+    /// before using this method.
+    #[must_use]
+    pub fn create_ciphertext<F>(self, plaintext: &[u8], external_aad: &[u8], cipher: F) -> Self
+    where
+        F: FnOnce(&[u8], &[u8]) -> Vec<u8>,
+    {
+        let aad = enc_structure_data(
+            EncryptionContext::CoseEncrypt,
+            self.0.protected.clone(),
+            external_aad,
+        );
+        self.ciphertext(cipher(plaintext, &aad))
+    }
+
+    /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+    /// plaintext and combined AAD (in that order).  Any protected header values should be set
+    /// before using this method.
+    pub fn try_create_ciphertext<F, E>(
+        self,
+        plaintext: &[u8],
+        external_aad: &[u8],
+        cipher: F,
+    ) -> Result<Self, E>
+    where
+        F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+    {
+        let aad = enc_structure_data(
+            EncryptionContext::CoseEncrypt,
+            self.0.protected.clone(),
+            external_aad,
+        );
+        Ok(self.ciphertext(cipher(plaintext, &aad)?))
+    }
+
+    /// Add a [`CoseRecipient`].
+    #[must_use]
+    pub fn add_recipient(mut self, recipient: CoseRecipient) -> Self {
+        self.0.recipients.push(recipient);
+        self
+    }
+}
+
+/// Structure representing an encrypted object.
+///
+/// ```cddl
+///  COSE_Encrypt0 = [
+///      Headers,
+///      ciphertext : bstr / nil,
+///  ]
+///  ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseEncrypt0 {
+    pub protected: ProtectedHeader,
+    pub unprotected: Header,
+    pub ciphertext: Option<Vec<u8>>,
+}
+
+impl crate::CborSerializable for CoseEncrypt0 {}
+
+impl crate::TaggedCborSerializable for CoseEncrypt0 {
+    const TAG: u64 = iana::CborTag::CoseEncrypt0 as u64;
+}
+
+impl AsCborValue for CoseEncrypt0 {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() != 3 {
+            return Err(CoseError::UnexpectedItem("array", "array with 3 items"));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        Ok(Self {
+            ciphertext: match a.remove(2) {
+                Value::Bytes(b) => Some(b),
+                Value::Null => None,
+                v => return cbor_type_error(&v, "bstr"),
+            },
+
+            unprotected: Header::from_cbor_value(a.remove(1))?,
+            protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(Value::Array(vec![
+            self.protected.cbor_bstr()?,
+            self.unprotected.to_cbor_value()?,
+            match self.ciphertext {
+                None => Value::Null,
+                Some(b) => Value::Bytes(b),
+            },
+        ]))
+    }
+}
+
+impl CoseEncrypt0 {
+    /// Decrypt the `ciphertext` value, using `cipher` to decrypt the cipher text and
+    /// combined AAD.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if no `ciphertext` is available.
+    pub fn decrypt<F, E>(&self, external_aad: &[u8], cipher: F) -> Result<Vec<u8>, E>
+    where
+        F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+    {
+        let ct = self.ciphertext.as_ref().unwrap(/* safe: documented */);
+        let aad = enc_structure_data(
+            EncryptionContext::CoseEncrypt0,
+            self.protected.clone(),
+            external_aad,
+        );
+        cipher(ct, &aad)
+    }
+}
+
+/// Builder for [`CoseEncrypt0`] objects.
+#[derive(Debug, Default)]
+pub struct CoseEncrypt0Builder(CoseEncrypt0);
+
+impl CoseEncrypt0Builder {
+    builder! {CoseEncrypt0}
+    builder_set_protected! {protected}
+    builder_set! {unprotected: Header}
+    builder_set_optional! {ciphertext: Vec<u8>}
+
+    /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+    /// plaintext and combined AAD (in that order).  Any protected header values should be set
+    /// before using this method.
+    #[must_use]
+    pub fn create_ciphertext<F>(self, plaintext: &[u8], external_aad: &[u8], cipher: F) -> Self
+    where
+        F: FnOnce(&[u8], &[u8]) -> Vec<u8>,
+    {
+        let aad = enc_structure_data(
+            EncryptionContext::CoseEncrypt0,
+            self.0.protected.clone(),
+            external_aad,
+        );
+        self.ciphertext(cipher(plaintext, &aad))
+    }
+
+    /// Calculate the ciphertext value, using `cipher` to generate the encrypted bytes from the
+    /// plaintext and combined AAD (in that order).  Any protected header values should be set
+    /// before using this method.
+    pub fn try_create_ciphertext<F, E>(
+        self,
+        plaintext: &[u8],
+        external_aad: &[u8],
+        cipher: F,
+    ) -> Result<Self, E>
+    where
+        F: FnOnce(&[u8], &[u8]) -> Result<Vec<u8>, E>,
+    {
+        let aad = enc_structure_data(
+            EncryptionContext::CoseEncrypt0,
+            self.0.protected.clone(),
+            external_aad,
+        );
+        Ok(self.ciphertext(cipher(plaintext, &aad)?))
+    }
+}
+
+/// Possible encryption contexts.
+#[derive(Clone, Copy, Debug)]
+pub enum EncryptionContext {
+    CoseEncrypt,
+    CoseEncrypt0,
+    EncRecipient,
+    MacRecipient,
+    RecRecipient,
+}
+
+impl EncryptionContext {
+    /// Return the context string as per RFC 8152 section 5.3.
+    fn text(&self) -> &'static str {
+        match self {
+            EncryptionContext::CoseEncrypt => "Encrypt",
+            EncryptionContext::CoseEncrypt0 => "Encrypt0",
+            EncryptionContext::EncRecipient => "Enc_Recipient",
+            EncryptionContext::MacRecipient => "Mac_Recipient",
+            EncryptionContext::RecRecipient => "Rec_Recipient",
+        }
+    }
+}
+
+/// Create a binary blob that will be signed.
+//
+/// ```cddl
+///  Enc_structure = [
+///      context : "Encrypt" / "Encrypt0" / "Enc_Recipient" /
+///          "Mac_Recipient" / "Rec_Recipient",
+///      protected : empty_or_serialized_map,
+///      external_aad : bstr
+///  ]
+/// ```
+pub fn enc_structure_data(
+    context: EncryptionContext,
+    protected: ProtectedHeader,
+    external_aad: &[u8],
+) -> Vec<u8> {
+    let arr = vec![
+        Value::Text(context.text().to_owned()),
+        protected.cbor_bstr().expect("failed to serialize header"), // safe: always serializable
+        Value::Bytes(external_aad.to_vec()),
+    ];
+
+    let mut data = Vec::new();
+    cbor::ser::into_writer(&Value::Array(arr), &mut data).unwrap(); // safe: always serializable
+    data
+}
diff --git a/src/encrypt/tests.rs b/src/encrypt/tests.rs
new file mode 100644
index 0000000..56e24c4
--- /dev/null
+++ b/src/encrypt/tests.rs
@@ -0,0 +1,1084 @@
+// 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::{
+    cbor::value::Value, iana, util::expect_err, CborSerializable, ContentType, CoseKeyBuilder,
+    CoseRecipientBuilder, CoseSignatureBuilder, HeaderBuilder, TaggedCborSerializable,
+};
+use alloc::{
+    string::{String, ToString},
+    vec,
+    vec::Vec,
+};
+
+#[test]
+fn test_cose_recipient_decode() {
+    let tests: Vec<(CoseRecipient, &'static str)> = vec![
+        (
+            CoseRecipientBuilder::new().build(),
+            concat!(
+                "83", // 3-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "f6", // null
+            ),
+        ),
+        (
+            CoseRecipientBuilder::new().ciphertext(vec![]).build(),
+            concat!(
+                "83", // 3-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+            ),
+        ),
+        (
+            CoseRecipientBuilder::new()
+                .ciphertext(vec![])
+                .add_recipient(CoseRecipientBuilder::new().build())
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "81", // 1-tuple
+                "83", // 3-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "f6", // null
+            ),
+        ),
+    ];
+
+    for (i, (recipient, recipient_data)) in tests.iter().enumerate() {
+        let got = recipient.clone().to_vec().unwrap();
+        assert_eq!(*recipient_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseRecipient::from_slice(&got).unwrap();
+        got.protected.original_data = None;
+        for mut recip in &mut got.recipients {
+            recip.protected.original_data = None;
+        }
+        assert_eq!(*recipient, got);
+    }
+}
+
+#[test]
+fn test_cose_recipient_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a2",   // 2-map (should be tuple)
+                "40",   // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0",   // 0-map
+                "4161", // 1-bstr
+                "40",   // 0-bstr
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "82", // 2-tuple (should be 4-tuple)
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+            ),
+            "expected array with 3 or 4 items",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "80", // 0-tuple (should be bstr)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "80", // 0-arr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40", // 0-bstr (should be map)
+                "40", // 0-bstr
+                "80", // 0-arr
+            ),
+            "expected map",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "60", // 0-tstr (should be bstr)
+                "80", // 0-arr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "40", // 0-bstr
+            ),
+            "expected array",
+        ),
+    ];
+    for (recipient_data, err_msg) in tests.iter() {
+        let data = hex::decode(recipient_data).unwrap();
+        let result = CoseRecipient::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_cose_encrypt_decode() {
+    let tests: Vec<(CoseEncrypt, &'static str)> = vec![
+        (
+            CoseEncryptBuilder::new().build(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "f6", // null
+                "80", // 0-tuple
+            ),
+        ),
+        (
+            CoseEncryptBuilder::new().ciphertext(vec![]).build(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "80", // 0-tuple
+            ),
+        ),
+    ];
+
+    for (i, (encrypt, encrypt_data)) in tests.iter().enumerate() {
+        let got = encrypt.clone().to_vec().unwrap();
+        assert_eq!(*encrypt_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseEncrypt::from_slice(&got).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*encrypt, got);
+    }
+}
+
+#[test]
+fn test_cose_encrypt_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a2",   // 2-map (should be tuple)
+                "40",   // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0",   // 0-map
+                "4161", // 1-bstr
+                "40",   // 0-bstr
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "83", // 3-tuple (should be 4-tuple)
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+            ),
+            "expected array with 4 items",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "80", // 0-tuple (should be bstr)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "80", // 0-arr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40", // 0-bstr (should be map)
+                "40", // 0-bstr
+                "80", // 0-arr
+            ),
+            "expected map",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "60", // 0-tstr (should be bstr)
+                "80", // 0-arr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "40", // 0-bstr
+            ),
+            "expected array",
+        ),
+    ];
+    for (encrypt_data, err_msg) in tests.iter() {
+        let data = hex::decode(encrypt_data).unwrap();
+        let result = CoseEncrypt::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+#[test]
+fn test_rfc8152_cose_encrypt_decode() {
+    // COSE_Encrypt structures from RFC 8152 section C.3.
+    let tests: Vec<(CoseEncrypt, &'static str)> = vec![
+        (
+            CoseEncryptBuilder::new()
+                .protected(
+                    HeaderBuilder::new()
+                        .algorithm(iana::Algorithm::A128GCM)
+                        .build(),
+                )
+                .unprotected(
+                    HeaderBuilder::new()
+                        .iv(hex::decode("c9cf4df2fe6c632bf7886413").unwrap())
+                        .build(),
+                )
+                .ciphertext(
+                    hex::decode(
+                        "7adbe2709ca818fb415f1e5df66f4e1a51053ba6d65a1a0c52a357da7a644b8070a151b0",
+                    )
+                    .unwrap(),
+                )
+                .add_recipient(
+                    CoseRecipientBuilder::new()
+                        .protected(
+                            HeaderBuilder::new()
+                                .algorithm(iana::Algorithm::ECDH_ES_HKDF_256)
+                                .build(),
+                        )
+                        .unprotected(
+                            HeaderBuilder::new()
+                                .value(iana::HeaderAlgorithmParameter::EphemeralKey as i64,
+                                       CoseKeyBuilder::new_ec2_pub_key_y_sign(iana::EllipticCurve::P_256,
+                                                                              hex::decode("98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280").unwrap(),
+                                                                              true)
+                                       .build().to_cbor_value().unwrap())
+                                .key_id(b"meriadoc.brandybuck@buckland.example".to_vec())
+                                .build(),
+                        )
+                        .ciphertext(vec![])
+                        .build(),
+                )
+                .build(),
+            // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+            concat!(
+                "d860",
+                "84", "43", "a10101",
+                "a1", "05", "4c", "c9cf4df2fe6c632bf7886413",
+                "5824", "7adbe2709ca818fb415f1e5df66f4e1a51053ba6d65a1a0c52a357da7a644b8070a151b0",
+                "81",
+                "83",
+                "44", "a1013818",
+                "a2",
+                "04",
+                "5824", "6d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65",
+                "20",
+                "a4",
+                "01", "02",
+                "20", "01",
+                "21", "5820", "98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280",
+                "22", "f5",
+                "40",
+            ),
+        ),
+        (
+            CoseEncryptBuilder::new()
+                .protected(HeaderBuilder::new().algorithm(iana::Algorithm::AES_CCM_16_64_128).build())
+                .unprotected(HeaderBuilder::new().iv(hex::decode("89f52f65a1c580933b5261a76c").unwrap()).build())
+                .ciphertext(hex::decode("753548a19b1307084ca7b2056924ed95f2e3b17006dfe931b687b847").unwrap())
+                .add_recipient(CoseRecipientBuilder::new()
+                               .protected(HeaderBuilder::new().algorithm(iana::Algorithm::Direct_HKDF_SHA_256).build())
+                               .unprotected(
+                                   HeaderBuilder::new()
+                                       .key_id(b"our-secret".to_vec())
+                                       .value(iana::HeaderAlgorithmParameter::Salt as i64,
+                                              Value::Bytes(b"aabbccddeeffgghh".to_vec()))
+                                       .build())
+                               .ciphertext(vec![])
+                               .build())
+                .build(),
+            // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+            concat!(
+                "d860",
+                "84",
+                "43",
+                "a1010a",
+                "a1",
+                "05",
+                "4d", "89f52f65a1c580933b5261a76c",
+                "581c", "753548a19b1307084ca7b2056924ed95f2e3b17006dfe931b687b847",
+                "81",
+                "83",
+                "43",
+                "a10129",
+                "a2",
+                "04", "4a", "6f75722d736563726574",
+                "33", "50", "61616262636364646565666667676868",
+                "40",
+            ),
+        ),
+
+        (
+            CoseEncryptBuilder::new()
+                .protected(HeaderBuilder::new().algorithm(iana::Algorithm::A128GCM).build())
+                .unprotected(HeaderBuilder::new()
+                             .iv(hex::decode("c9cf4df2fe6c632bf7886413").unwrap())
+                             .add_counter_signature(CoseSignatureBuilder::new()
+                                                    .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES512).build())
+                                                    .unprotected(HeaderBuilder::new().key_id(b"bilbo.baggins@hobbiton.example".to_vec()).build())
+                                                    .signature(hex::decode("00929663c8789bb28177ae28467e66377da12302d7f9594d2999afa5dfa531294f8896f2b6cdf1740014f4c7f1a358e3a6cf57f4ed6fb02fcf8f7aa989f5dfd07f0700a3a7d8f3c604ba70fa9411bd10c2591b483e1d2c31de003183e434d8fba18f17a4c7e3dfa003ac1cf3d30d44d2533c4989d3ac38c38b71481cc3430c9d65e7ddff").unwrap())
+                                                    .build())
+                             .build())
+                .ciphertext(hex::decode("7adbe2709ca818fb415f1e5df66f4e1a51053ba6d65a1a0c52a357da7a644b8070a151b0").unwrap())
+                .add_recipient(CoseRecipientBuilder::new()
+                               .protected(
+                                   HeaderBuilder::new()
+                                       .algorithm(iana::Algorithm::ECDH_ES_HKDF_256)
+                                       .build())
+                               .unprotected(
+                                   HeaderBuilder::new()
+                                       .value(iana::HeaderAlgorithmParameter::EphemeralKey as i64,
+                                              CoseKeyBuilder::new_ec2_pub_key_y_sign(iana::EllipticCurve::P_256,
+                                                                                     hex::decode("98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280").unwrap(),
+                                                                                     true)
+                                              .build().to_cbor_value().unwrap())
+                                       .key_id(b"meriadoc.brandybuck@buckland.example".to_vec())
+                                       .build())
+                        .ciphertext(vec![])
+                               .build())
+                .build(),
+            // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+            concat!(
+                "d860",
+                "84",
+                "43",
+                "a10101",
+                "a2",
+                "05",
+                "4c", "c9cf4df2fe6c632bf7886413",
+                "07",
+                "83",
+                "44",
+                "a1013823",
+                "a1",
+                "04",
+                "581e", "62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+                "5884",
+                "00929663c8789bb28177ae28467e66377da12302d7f9594d2999afa5dfa531294f8896f2b6cdf1740014f4c7f1a358e3a6cf57f4ed6fb02fcf8f7aa989f5dfd07f0700a3a7d8f3c604ba70fa9411bd10c2591b483e1d2c31de003183e434d8fba18f17a4c7e3dfa003ac1cf3d30d44d2533c4989d3ac38c38b71481cc3430c9d65e7ddff",
+                "5824",
+                "7adbe2709ca818fb415f1e5df66f4e1a51053ba6d65a1a0c52a357da7a644b8070a151b0",
+                "81",
+                "83",
+                "44", "a1013818",
+                "a2",
+                "04",
+                "5824", "6d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65",
+                "20",
+                "a4",
+                "01",
+                "02",
+                "20",
+                "01",
+                "21",
+                "5820", "98f50a4ff6c05861c8860d13a638ea56c3f5ad7590bbfbf054e1c7b4d91d6280",
+                "22",
+                "f5",
+                "40",
+            ),
+        ),
+        (
+            CoseEncryptBuilder::new()
+                .protected(HeaderBuilder::new().algorithm(iana::Algorithm::A128GCM).build())
+                .unprotected(HeaderBuilder::new().iv(hex::decode("02d1f7e6f26c43d4868d87ce").unwrap()).build())
+                .ciphertext(hex::decode("64f84d913ba60a76070a9a48f26e97e863e28529d8f5335e5f0165eee976b4a5f6c6f09d").unwrap())
+                .add_recipient(CoseRecipientBuilder::new()
+                               .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ECDH_SS_A128KW).build())
+                               .unprotected(HeaderBuilder::new()
+                                            .key_id(b"meriadoc.brandybuck@buckland.example".to_vec())
+                                            .value(
+                                                iana::HeaderAlgorithmParameter::StaticKeyId as i64,
+                                                Value::Bytes(b"peregrin.took@tuckborough.example".to_vec())
+                                            )
+                                            .value(
+                                                iana::HeaderAlgorithmParameter::PartyUNonce as i64,
+                                                Value::Bytes(hex::decode("0101").unwrap())
+                                            )
+                                            .build())
+                               .ciphertext(hex::decode("41e0d76f579dbd0d936a662d54d8582037de2e366fde1c62").unwrap())
+                               .build())
+                .build(),
+            // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+            concat!(
+                "d860",
+                "84",
+                "43",
+                "a10101",
+                "a1",
+                "05",
+                "4c", "02d1f7e6f26c43d4868d87ce",
+                "5824", "64f84d913ba60a76070a9a48f26e97e863e28529d8f5335e5f0165eee976b4a5f6c6f09d",
+                "81",
+                "83",
+                "44", "a101381f",
+                "a3",
+                "04",
+                "5824", "6d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65",
+                "22",
+                "5821", "706572656772696e2e746f6f6b407475636b626f726f7567682e6578616d706c65",
+                "35",
+                "42",
+                "0101",
+                "5818", "41e0d76f579dbd0d936a662d54d8582037de2e366fde1c62",
+            ),
+        ),
+    ];
+
+    for (i, (encrypt, encrypt_data)) in tests.iter().enumerate() {
+        let got = encrypt.clone().to_tagged_vec().unwrap();
+        assert_eq!(*encrypt_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseEncrypt::from_tagged_slice(&got).unwrap();
+        got.protected.original_data = None;
+        for mut recip in &mut got.recipients {
+            recip.protected.original_data = None;
+        }
+        for mut sig in &mut got.unprotected.counter_signatures {
+            sig.protected.original_data = None;
+        }
+        assert_eq!(*encrypt, got);
+    }
+}
+
+#[test]
+fn test_cose_encrypt0_decode() {
+    let tests: Vec<(CoseEncrypt0, &'static str)> = vec![
+        (
+            CoseEncrypt0Builder::new().build(),
+            concat!(
+                "83", // 3-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "f6", // null
+            ),
+        ),
+        (
+            CoseEncrypt0Builder::new().ciphertext(vec![]).build(),
+            concat!(
+                "83", // 3-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+            ),
+        ),
+    ];
+
+    for (i, (encrypt, encrypt_data)) in tests.iter().enumerate() {
+        let got = encrypt.clone().to_vec().unwrap();
+        assert_eq!(*encrypt_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseEncrypt0::from_slice(&got).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*encrypt, got);
+    }
+}
+
+#[test]
+fn test_cose_encrypt0_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a2",   // 2-map (should be tuple)
+                "40",   // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0",   // 0-map
+                "4100", // 1-bstr
+                "40",   // 0-bstr
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "82", // 2-tuple (should be 3-tuple)
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+            ),
+            "expected array with 3 items",
+        ),
+        (
+            concat!(
+                "83", // 3-tuple
+                "80", // 0-tuple (should be bstr)
+                "a0", // 0-map
+                "40", // 0-bstr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "83", // 3-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40", // 0-bstr (should be map)
+                "40", // 0-bstr
+            ),
+            "expected map",
+        ),
+        (
+            concat!(
+                "83", // 3-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "60", // 0-tstr (should be bstr)
+            ),
+            "expected bstr",
+        ),
+    ];
+    for (encrypt_data, err_msg) in tests.iter() {
+        let data = hex::decode(encrypt_data).unwrap();
+        let result = CoseEncrypt0::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_rfc8152_cose_encrypt0_decode() {
+    // COSE_Encrypt0 structures from RFC 8152 section C.4.
+    let tests: Vec<(CoseEncrypt0, &'static str)> = vec![
+        (
+            CoseEncrypt0Builder::new()
+                .protected(
+                    HeaderBuilder::new()
+                        .algorithm(iana::Algorithm::AES_CCM_16_64_128)
+                        .build(),
+                )
+                .unprotected(
+                    HeaderBuilder::new()
+                        .iv(hex::decode("89f52f65a1c580933b5261a78c").unwrap())
+                        .build(),
+                )
+                .ciphertext(
+                    hex::decode("5974e1b99a3a4cc09a659aa2e9e7fff161d38ce71cb45ce460ffb569")
+                        .unwrap(),
+                )
+                .build(),
+            concat!(
+                "d0",
+                "83",
+                "43",
+                "a1010a",
+                "a1",
+                "05",
+                "4d",
+                "89f52f65a1c580933b5261a78c",
+                "581c",
+                "5974e1b99a3a4cc09a659aa2e9e7fff161d38ce71cb45ce460ffb569",
+            ),
+        ),
+        (
+            CoseEncrypt0Builder::new()
+                .protected(
+                    HeaderBuilder::new()
+                        .algorithm(iana::Algorithm::AES_CCM_16_64_128)
+                        .build(),
+                )
+                .unprotected(
+                    HeaderBuilder::new()
+                        .partial_iv(hex::decode("61a7").unwrap())
+                        .build(),
+                )
+                .ciphertext(
+                    hex::decode("252a8911d465c125b6764739700f0141ed09192de139e053bd09abca")
+                        .unwrap(),
+                )
+                .build(),
+            concat!(
+                "d0",
+                "83",
+                "43",
+                "a1010a",
+                "a1",
+                "06",
+                "42",
+                "61a7",
+                "581c",
+                "252a8911d465c125b6764739700f0141ed09192de139e053bd09abca",
+            ),
+        ),
+    ];
+
+    for (i, (encrypt, encrypt_data)) in tests.iter().enumerate() {
+        let got = encrypt.clone().to_tagged_vec().unwrap();
+        assert_eq!(*encrypt_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseEncrypt0::from_tagged_slice(&got).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*encrypt, got);
+    }
+}
+
+struct FakeCipher {}
+
+impl FakeCipher {
+    fn encrypt(&self, plaintext: &[u8], additional_data: &[u8]) -> Result<Vec<u8>, String> {
+        let mut result = vec![];
+        result.extend_from_slice(&(plaintext.len() as u32).to_be_bytes());
+        result.extend_from_slice(plaintext);
+        result.extend_from_slice(additional_data);
+        Ok(result)
+    }
+
+    fn decrypt(&self, ciphertext: &[u8], additional_data: &[u8]) -> Result<Vec<u8>, String> {
+        if ciphertext.len() < 4 {
+            return Err("not long enough".to_owned());
+        }
+        let pt_len =
+            u32::from_be_bytes([ciphertext[0], ciphertext[1], ciphertext[2], ciphertext[3]])
+                as usize;
+        let pt = &ciphertext[4..4 + pt_len];
+        let recovered_aad = &ciphertext[4 + pt_len..];
+        if recovered_aad != additional_data {
+            return Err("aad doesn't match".to_owned());
+        }
+        Ok(pt.to_vec())
+    }
+    fn fail_encrypt(&self, _plaintext: &[u8], _additional_data: &[u8]) -> Result<Vec<u8>, String> {
+        Err("failed".to_string())
+    }
+}
+
+#[test]
+fn test_cose_recipient_roundtrip() {
+    let pt = b"This is the plaintext";
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    for context in &[
+        EncryptionContext::EncRecipient,
+        EncryptionContext::MacRecipient,
+        EncryptionContext::RecRecipient,
+    ] {
+        let protected = HeaderBuilder::new()
+            .algorithm(iana::Algorithm::ES256)
+            .key_id(b"11".to_vec())
+            .build();
+
+        let mut recipient = CoseRecipientBuilder::new()
+            .protected(protected)
+            .create_ciphertext(*context, pt, external_aad, |pt, aad| {
+                cipher.encrypt(pt, aad).unwrap()
+            })
+            .build();
+
+        let recovered_pt = recipient
+            .decrypt(*context, external_aad, |ct, aad| cipher.decrypt(ct, aad))
+            .unwrap();
+        assert_eq!(&pt[..], recovered_pt);
+
+        // Changing an unprotected header leaves the ciphertext decipherable.
+        recipient.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+        assert!(recipient
+            .decrypt(*context, external_aad, |ct, aad| {
+                cipher.decrypt(ct, aad)
+            })
+            .is_ok());
+
+        // Providing a different `aad` means the ciphertext won't validate.
+        assert!(recipient
+            .decrypt(*context, b"not aad", |ct, aad| { cipher.decrypt(ct, aad) })
+            .is_err());
+
+        // Changing a protected header invalidates the ciphertext.
+        recipient.protected = ProtectedHeader::default();
+        assert!(recipient
+            .decrypt(*context, external_aad, |ct, aad| {
+                cipher.decrypt(ct, aad)
+            })
+            .is_err());
+    }
+}
+
+#[test]
+fn test_cose_recipient_noncanonical() {
+    let pt = b"aa";
+    let aad = b"bb";
+    let cipher = FakeCipher {};
+    let context = EncryptionContext::EncRecipient;
+
+    // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+    let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+    assert_eq!(protected.header, Header::default());
+    assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+    let mut recipient = CoseRecipient {
+        protected: protected.clone(),
+        ..Default::default()
+    };
+    let internal_aad = crate::encrypt::enc_structure_data(context, protected, aad);
+    recipient.ciphertext = Some(cipher.encrypt(pt, &internal_aad).unwrap());
+
+    // Deciphering the ciphertext should still succeed, because the `ProtectedHeader`
+    // includes the wire data and uses it for building the decryption input.
+    let recovered_pt = recipient
+        .decrypt(context, aad, |ct, aad| cipher.decrypt(ct, aad))
+        .unwrap();
+    assert_eq!(&pt[..], recovered_pt);
+
+    // However, if we attempt to build the same decryption inputs by hand (thus not including the
+    // non-canonical wire data)...
+    let recreated_recipient = CoseRecipientBuilder::new()
+        .ciphertext(recipient.ciphertext.unwrap())
+        .build();
+
+    // ...then the transplanted cipher text will not decipher, because the re-building of the
+    // inputs will use the canonical encoding of the protected header, which is not what was
+    // originally used for the input.
+    assert!(recreated_recipient
+        .decrypt(context, aad, |ct, aad| cipher.decrypt(ct, aad))
+        .is_err());
+}
+
+#[test]
+fn test_cose_recipient_result() {
+    let pt = b"This is the plaintext";
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    let protected = HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let _recipient = CoseRecipientBuilder::new()
+        .protected(protected.clone())
+        .try_create_ciphertext(
+            EncryptionContext::EncRecipient,
+            pt,
+            external_aad,
+            |pt, aad| cipher.encrypt(pt, aad),
+        )
+        .unwrap()
+        .build();
+    let status = CoseRecipientBuilder::new()
+        .protected(protected)
+        .try_create_ciphertext(
+            EncryptionContext::EncRecipient,
+            pt,
+            external_aad,
+            |pt, aad| cipher.fail_encrypt(pt, aad),
+        );
+    expect_err(status, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_cose_recipient_missing_ciphertext() {
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    let recipient = CoseRecipient::default();
+
+    // No ciphertext has been set, do decryption will panic.
+    let _result = recipient.decrypt(EncryptionContext::EncRecipient, external_aad, |ct, aad| {
+        cipher.decrypt(ct, aad)
+    });
+}
+
+#[test]
+#[should_panic]
+fn test_cose_recipient_builder_invalid_context() {
+    let pt = b"This is the plaintext";
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    // Can't use a non-recipient context.
+    let _recipient = CoseRecipientBuilder::new()
+        .create_ciphertext(
+            EncryptionContext::CoseEncrypt,
+            pt,
+            external_aad,
+            |pt, aad| cipher.encrypt(pt, aad).unwrap(),
+        )
+        .build();
+}
+
+#[test]
+#[should_panic]
+fn test_cose_recipient_decrypt_invalid_context() {
+    let pt = b"This is the plaintext";
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    let recipient = CoseRecipientBuilder::new()
+        .create_ciphertext(
+            EncryptionContext::EncRecipient,
+            pt,
+            external_aad,
+            |pt, aad| cipher.encrypt(pt, aad).unwrap(),
+        )
+        .build();
+
+    // Can't use a non-recipient context.
+    let _result = recipient.decrypt(EncryptionContext::CoseEncrypt, external_aad, |ct, aad| {
+        cipher.decrypt(ct, aad)
+    });
+}
+
+#[test]
+fn test_cose_encrypt_roundtrip() {
+    let pt = b"This is the plaintext";
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    let protected = HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let mut encrypt = CoseEncryptBuilder::new()
+        .protected(protected)
+        .create_ciphertext(pt, external_aad, |pt, aad| cipher.encrypt(pt, aad).unwrap())
+        .build();
+
+    let recovered_pt = encrypt
+        .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+        .unwrap();
+    assert_eq!(&pt[..], recovered_pt);
+
+    // Changing an unprotected header leaves the ciphertext decipherable.
+    encrypt.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+    assert!(encrypt
+        .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+        .is_ok());
+
+    // Providing a different `aad` means the signature won't validate.
+    assert!(encrypt
+        .decrypt(b"not aad", |ct, aad| cipher.decrypt(ct, aad))
+        .is_err());
+
+    // Changing a protected header invalidates the ciphertext.
+    encrypt.protected = ProtectedHeader::default();
+    assert!(encrypt
+        .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+        .is_err());
+}
+
+#[test]
+fn test_cose_encrypt_noncanonical() {
+    let pt = b"aa";
+    let external_aad = b"bb";
+    let cipher = FakeCipher {};
+
+    // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+    let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+    assert_eq!(protected.header, Header::default());
+    assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+    let mut encrypt = CoseEncrypt {
+        protected: protected.clone(),
+        ..Default::default()
+    };
+    let aad = enc_structure_data(
+        EncryptionContext::CoseEncrypt,
+        protected.clone(),
+        external_aad,
+    );
+    encrypt.ciphertext = Some(cipher.encrypt(pt, &aad).unwrap());
+
+    // Deciphering the ciphertext should still succeed, because the `ProtectedHeader`
+    // includes the wire data and uses it for building the decryption input.
+    let recovered_pt = encrypt
+        .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+        .unwrap();
+    assert_eq!(&pt[..], recovered_pt);
+
+    // However, if we attempt to build the same decryption inputs by hand (thus not including the
+    // non-canonical wire data)...
+    let recreated_encrypt = CoseEncryptBuilder::new()
+        .protected(protected.header)
+        .ciphertext(encrypt.ciphertext.unwrap())
+        .build();
+
+    // ...then the transplanted cipher text will not decipher, because the re-building of the
+    // inputs will use the canonical encoding of the protected header, which is not what was
+    // originally used for the input.
+    assert!(recreated_encrypt
+        .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+        .is_err());
+}
+
+#[test]
+fn test_cose_encrypt_status() {
+    let pt = b"This is the plaintext";
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    let protected = HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let _encrypt = CoseEncryptBuilder::new()
+        .protected(protected.clone())
+        .try_create_ciphertext(pt, external_aad, |pt, aad| cipher.encrypt(pt, aad))
+        .unwrap()
+        .build();
+    let status = CoseEncryptBuilder::new()
+        .protected(protected)
+        .try_create_ciphertext(pt, external_aad, |pt, aad| cipher.fail_encrypt(pt, aad));
+    expect_err(status, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_cose_encrypt_missing_ciphertext() {
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    let encrypt = CoseEncrypt::default();
+
+    // No ciphertext has been set, do decryption will panic.
+    let _result = encrypt.decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad));
+}
+
+#[test]
+fn test_cose_encrypt0_roundtrip() {
+    let pt = b"This is the plaintext";
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    let protected = HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let mut encrypt = CoseEncrypt0Builder::new()
+        .protected(protected)
+        .create_ciphertext(pt, external_aad, |pt, aad| cipher.encrypt(pt, aad).unwrap())
+        .build();
+
+    let recovered_pt = encrypt
+        .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+        .unwrap();
+    assert_eq!(&pt[..], recovered_pt);
+
+    // Changing an unprotected header leaves the ciphertext decipherable.
+    encrypt.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+    assert!(encrypt
+        .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+        .is_ok());
+
+    // Providing a different `aad` means the ciphertext won't decrypt.
+    assert!(encrypt
+        .decrypt(b"not aad", |ct, aad| cipher.decrypt(ct, aad))
+        .is_err());
+
+    // Changing a protected header invalidates the ciphertext.
+    encrypt.protected = ProtectedHeader::default();
+    assert!(encrypt
+        .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+        .is_err());
+}
+
+#[test]
+fn test_cose_encrypt0_noncanonical() {
+    let pt = b"aa";
+    let external_aad = b"bb";
+    let cipher = FakeCipher {};
+
+    // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+    let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+    assert_eq!(protected.header, Header::default());
+    assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+    let mut encrypt = CoseEncrypt0 {
+        protected: protected.clone(),
+        ..Default::default()
+    };
+    let aad = enc_structure_data(
+        EncryptionContext::CoseEncrypt0,
+        protected.clone(),
+        external_aad,
+    );
+    encrypt.ciphertext = Some(cipher.encrypt(pt, &aad).unwrap());
+
+    // Deciphering the ciphertext should still succeed, because the `ProtectedHeader`
+    // includes the wire data and uses it for building the decryption input.
+    let recovered_pt = encrypt
+        .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+        .unwrap();
+    assert_eq!(&pt[..], recovered_pt);
+
+    // However, if we attempt to build the same decryption inputs by hand (thus not including the
+    // non-canonical wire data)...
+    let recreated_encrypt = CoseEncrypt0Builder::new()
+        .protected(protected.header)
+        .ciphertext(encrypt.ciphertext.unwrap())
+        .build();
+
+    // ...then the transplanted cipher text will not decipher, because the re-building of the
+    // inputs will use the canonical encoding of the protected header, which is not what was
+    // originally used for the input.
+    assert!(recreated_encrypt
+        .decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad))
+        .is_err());
+}
+#[test]
+fn test_cose_encrypt0_status() {
+    let pt = b"This is the plaintext";
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    let protected = HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let _encrypt = CoseEncrypt0Builder::new()
+        .protected(protected.clone())
+        .try_create_ciphertext(pt, external_aad, |pt, aad| cipher.encrypt(pt, aad))
+        .unwrap()
+        .build();
+    let status = CoseEncrypt0Builder::new()
+        .protected(protected)
+        .try_create_ciphertext(pt, external_aad, |pt, aad| cipher.fail_encrypt(pt, aad));
+    expect_err(status, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_cose_encrypt0_missing_ciphertext() {
+    let external_aad = b"This is the external aad";
+    let cipher = FakeCipher {};
+
+    let encrypt = CoseEncrypt0::default();
+
+    // No ciphertext has been set, do decryption will panic.
+    let _result = encrypt.decrypt(external_aad, |ct, aad| cipher.decrypt(ct, aad));
+}
diff --git a/src/header/mod.rs b/src/header/mod.rs
new file mode 100644
index 0000000..aa11174
--- /dev/null
+++ b/src/header/mod.rs
@@ -0,0 +1,406 @@
+// 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 Headers functionality.
+
+use crate::{
+    cbor::value::Value,
+    iana,
+    iana::EnumI64,
+    util::{cbor_type_error, to_cbor_array, AsCborValue, ValueTryAs},
+    Algorithm, CborSerializable, CoseError, CoseSignature, Label, RegisteredLabel, Result,
+};
+use alloc::{collections::BTreeSet, string::String, vec, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Content type.
+pub type ContentType = crate::RegisteredLabel<iana::CoapContentFormat>;
+
+/// Structure representing a common COSE header map.
+///
+/// ```cddl
+///   header_map = {
+///       Generic_Headers,
+///       * label => values
+///   }
+///
+///   Generic_Headers = (
+///       ? 1 => int / tstr,  ; algorithm identifier
+///       ? 2 => [+label],    ; criticality
+///       ? 3 => tstr / int,  ; content type
+///       ? 4 => bstr,        ; key identifier
+///       ? 5 => bstr,        ; IV
+///       ? 6 => bstr,        ; Partial IV
+///       ? 7 => COSE_Signature / [+COSE_Signature] ; Counter signature
+///   )
+///  ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct Header {
+    /// Cryptographic algorithm to use
+    pub alg: Option<Algorithm>,
+    /// Critical headers to be understood
+    pub crit: Vec<RegisteredLabel<iana::HeaderParameter>>,
+    /// Content type of the payload
+    pub content_type: Option<ContentType>,
+    /// Key identifier.
+    pub key_id: Vec<u8>,
+    /// Full initialization vector
+    pub iv: Vec<u8>,
+    /// Partial initialization vector
+    pub partial_iv: Vec<u8>,
+    /// Counter signature
+    pub counter_signatures: Vec<CoseSignature>,
+    /// Any additional header (label,value) pairs.  If duplicate labels are present, CBOR-encoding
+    /// will fail.
+    pub rest: Vec<(Label, Value)>,
+}
+
+impl Header {
+    /// Indicate whether the `Header` is empty.
+    pub fn is_empty(&self) -> bool {
+        self.alg.is_none()
+            && self.crit.is_empty()
+            && self.content_type.is_none()
+            && self.key_id.is_empty()
+            && self.iv.is_empty()
+            && self.partial_iv.is_empty()
+            && self.counter_signatures.is_empty()
+            && self.rest.is_empty()
+    }
+}
+
+impl crate::CborSerializable for Header {}
+
+const ALG: Label = Label::Int(iana::HeaderParameter::Alg as i64);
+const CRIT: Label = Label::Int(iana::HeaderParameter::Crit as i64);
+const CONTENT_TYPE: Label = Label::Int(iana::HeaderParameter::ContentType as i64);
+const KID: Label = Label::Int(iana::HeaderParameter::Kid as i64);
+const IV: Label = Label::Int(iana::HeaderParameter::Iv as i64);
+const PARTIAL_IV: Label = Label::Int(iana::HeaderParameter::PartialIv as i64);
+const COUNTER_SIG: Label = Label::Int(iana::HeaderParameter::CounterSignature as i64);
+
+impl AsCborValue for Header {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let m = value.try_as_map()?;
+        let mut headers = Self::default();
+        let mut seen = BTreeSet::new();
+        for (l, value) in m.into_iter() {
+            // The `ciborium` CBOR library does not police duplicate map keys.
+            // RFC 8152 section 14 requires that COSE does police duplicates, so do it here.
+            let label = Label::from_cbor_value(l)?;
+            if seen.contains(&label) {
+                return Err(CoseError::DuplicateMapKey);
+            }
+            seen.insert(label.clone());
+            match label {
+                ALG => headers.alg = Some(Algorithm::from_cbor_value(value)?),
+
+                CRIT => match value {
+                    Value::Array(a) => {
+                        if a.is_empty() {
+                            return Err(CoseError::UnexpectedItem(
+                                "empty array",
+                                "non-empty array",
+                            ));
+                        }
+                        for v in a {
+                            headers.crit.push(
+                                RegisteredLabel::<iana::HeaderParameter>::from_cbor_value(v)?,
+                            );
+                        }
+                    }
+                    v => return cbor_type_error(&v, "array value"),
+                },
+
+                CONTENT_TYPE => {
+                    headers.content_type = Some(ContentType::from_cbor_value(value)?);
+                    if let Some(ContentType::Text(text)) = &headers.content_type {
+                        if text.is_empty() {
+                            return Err(CoseError::UnexpectedItem("empty tstr", "non-empty tstr"));
+                        }
+                        if text.trim() != text {
+                            return Err(CoseError::UnexpectedItem(
+                                "leading/trailing whitespace",
+                                "no leading/trailing whitespace",
+                            ));
+                        }
+                        // Basic check that the content type is of form type/subtype.
+                        // We don't check the precise definition though (RFC 6838 s4.2)
+                        if text.matches('/').count() != 1 {
+                            return Err(CoseError::UnexpectedItem(
+                                "arbitrary text",
+                                "text of form type/subtype",
+                            ));
+                        }
+                    }
+                }
+
+                KID => {
+                    headers.key_id = value.try_as_nonempty_bytes()?;
+                }
+
+                IV => {
+                    headers.iv = value.try_as_nonempty_bytes()?;
+                }
+
+                PARTIAL_IV => {
+                    headers.partial_iv = value.try_as_nonempty_bytes()?;
+                }
+                COUNTER_SIG => {
+                    let sig_or_sigs = value.try_as_array()?;
+                    if sig_or_sigs.is_empty() {
+                        return Err(CoseError::UnexpectedItem(
+                            "empty sig array",
+                            "non-empty sig array",
+                        ));
+                    }
+                    // The encoding of counter signature[s] is pesky:
+                    // - a single counter signature is encoded as `COSE_Signature` (a 3-tuple)
+                    // - multiple counter signatures are encoded as `[+ COSE_Signature]`
+                    //
+                    // Determine which is which by looking at the first entry of the array:
+                    // - If it's a bstr, sig_or_sigs is a single signature.
+                    // - If it's an array, sig_or_sigs is an array of signatures
+                    match &sig_or_sigs[0] {
+                        Value::Bytes(_) => headers
+                            .counter_signatures
+                            .push(CoseSignature::from_cbor_value(Value::Array(sig_or_sigs))?),
+                        Value::Array(_) => {
+                            for sig in sig_or_sigs.into_iter() {
+                                headers
+                                    .counter_signatures
+                                    .push(CoseSignature::from_cbor_value(sig)?);
+                            }
+                        }
+                        v => return cbor_type_error(v, "array or bstr value"),
+                    }
+                }
+
+                label => headers.rest.push((label, value)),
+            }
+            // RFC 8152 section 3.1: "The 'Initialization Vector' and 'Partial Initialization
+            // Vector' parameters MUST NOT both be present in the same security layer."
+            if !headers.iv.is_empty() && !headers.partial_iv.is_empty() {
+                return Err(CoseError::UnexpectedItem(
+                    "IV and partial-IV specified",
+                    "only one of IV and partial IV",
+                ));
+            }
+        }
+        Ok(headers)
+    }
+
+    fn to_cbor_value(mut self) -> Result<Value> {
+        let mut map = Vec::<(Value, Value)>::new();
+        if let Some(alg) = self.alg {
+            map.push((ALG.to_cbor_value()?, alg.to_cbor_value()?));
+        }
+        if !self.crit.is_empty() {
+            map.push((CRIT.to_cbor_value()?, to_cbor_array(self.crit)?));
+        }
+        if let Some(content_type) = self.content_type {
+            map.push((CONTENT_TYPE.to_cbor_value()?, content_type.to_cbor_value()?));
+        }
+        if !self.key_id.is_empty() {
+            map.push((KID.to_cbor_value()?, Value::Bytes(self.key_id)));
+        }
+        if !self.iv.is_empty() {
+            map.push((IV.to_cbor_value()?, Value::Bytes(self.iv)));
+        }
+        if !self.partial_iv.is_empty() {
+            map.push((PARTIAL_IV.to_cbor_value()?, Value::Bytes(self.partial_iv)));
+        }
+        if !self.counter_signatures.is_empty() {
+            if self.counter_signatures.len() == 1 {
+                // A single counter signature is encoded differently.
+                map.push((
+                    COUNTER_SIG.to_cbor_value()?,
+                    self.counter_signatures.remove(0).to_cbor_value()?,
+                ));
+            } else {
+                map.push((
+                    COUNTER_SIG.to_cbor_value()?,
+                    to_cbor_array(self.counter_signatures)?,
+                ));
+            }
+        }
+        let mut seen = BTreeSet::new();
+        for (label, value) in self.rest.into_iter() {
+            if seen.contains(&label) {
+                return Err(CoseError::DuplicateMapKey);
+            }
+            seen.insert(label.clone());
+            map.push((label.to_cbor_value()?, value));
+        }
+        Ok(Value::Map(map))
+    }
+}
+
+/// Builder for [`Header`] objects.
+#[derive(Debug, Default)]
+pub struct HeaderBuilder(Header);
+
+impl HeaderBuilder {
+    builder! {Header}
+    builder_set! {key_id: Vec<u8>}
+
+    /// Set the algorithm.
+    #[must_use]
+    pub fn algorithm(mut self, alg: iana::Algorithm) -> Self {
+        self.0.alg = Some(Algorithm::Assigned(alg));
+        self
+    }
+
+    /// Add a critical header.
+    #[must_use]
+    pub fn add_critical(mut self, param: iana::HeaderParameter) -> Self {
+        self.0.crit.push(RegisteredLabel::Assigned(param));
+        self
+    }
+
+    /// Add a critical header.
+    #[must_use]
+    pub fn add_critical_label(mut self, label: RegisteredLabel<iana::HeaderParameter>) -> Self {
+        self.0.crit.push(label);
+        self
+    }
+
+    /// Set the content type to a numeric value.
+    #[must_use]
+    pub fn content_format(mut self, content_type: iana::CoapContentFormat) -> Self {
+        self.0.content_type = Some(ContentType::Assigned(content_type));
+        self
+    }
+
+    /// Set the content type to a text value.
+    #[must_use]
+    pub fn content_type(mut self, content_type: String) -> Self {
+        self.0.content_type = Some(ContentType::Text(content_type));
+        self
+    }
+
+    /// Set the IV, and clear any partial IV already set.
+    #[must_use]
+    pub fn iv(mut self, iv: Vec<u8>) -> Self {
+        self.0.iv = iv;
+        self.0.partial_iv.clear();
+        self
+    }
+
+    /// Set the partial IV, and clear any IV already set.
+    #[must_use]
+    pub fn partial_iv(mut self, iv: Vec<u8>) -> Self {
+        self.0.partial_iv = iv;
+        self.0.iv.clear();
+        self
+    }
+
+    /// Add a counter signature.
+    #[must_use]
+    pub fn add_counter_signature(mut self, sig: CoseSignature) -> Self {
+        self.0.counter_signatures.push(sig);
+        self
+    }
+
+    /// Set a header label:value pair. If duplicate labels are added to a [`Header`],
+    /// subsequent attempts to CBOR-encode the header will fail.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if it used to set a header label from the range [1, 6].
+    #[must_use]
+    pub fn value(mut self, label: i64, value: Value) -> Self {
+        if label >= iana::HeaderParameter::Alg.to_i64()
+            && label <= iana::HeaderParameter::CounterSignature.to_i64()
+        {
+            panic!("value() method used to set core header parameter"); // safe: invalid input
+        }
+        self.0.rest.push((Label::Int(label), value));
+        self
+    }
+
+    /// Set a header label:value pair where the `label` is text.
+    #[must_use]
+    pub fn text_value(mut self, label: String, value: Value) -> Self {
+        self.0.rest.push((Label::Text(label), value));
+        self
+    }
+}
+
+/// Structure representing a protected COSE header map.
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct ProtectedHeader {
+    /// If this structure was created by parsing serialized data, this field
+    /// holds the entire contents of the original `bstr` data.
+    pub original_data: Option<Vec<u8>>,
+    /// Parsed header information.
+    pub header: Header,
+}
+
+impl ProtectedHeader {
+    /// Constructor from a [`Value`] that holds a `bstr` encoded header.
+    #[inline]
+    pub fn from_cbor_bstr(val: Value) -> Result<Self> {
+        let data = val.try_as_bytes()?;
+        let header = if data.is_empty() {
+            // An empty bstr is used as a short cut for an empty header map.
+            Header::default()
+        } else {
+            Header::from_slice(&data)?
+        };
+        Ok(ProtectedHeader {
+            original_data: Some(data),
+            header,
+        })
+    }
+
+    /// Convert this header to a `bstr` encoded map, as a [`Value`], consuming the object along the
+    /// way.
+    #[inline]
+    pub fn cbor_bstr(self) -> Result<Value> {
+        Ok(Value::Bytes(
+            if let Some(original_data) = self.original_data {
+                original_data
+            } else if self.is_empty() {
+                vec![]
+            } else {
+                self.to_vec()?
+            },
+        ))
+    }
+
+    /// Indicate whether the `ProtectedHeader` is empty.
+    pub fn is_empty(&self) -> bool {
+        self.header.is_empty()
+    }
+}
+
+impl crate::CborSerializable for ProtectedHeader {}
+
+impl AsCborValue for ProtectedHeader {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        Ok(ProtectedHeader {
+            original_data: None,
+            header: Header::from_cbor_value(value)?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        self.header.to_cbor_value()
+    }
+}
diff --git a/src/header/tests.rs b/src/header/tests.rs
new file mode 100644
index 0000000..6ff901a
--- /dev/null
+++ b/src/header/tests.rs
@@ -0,0 +1,487 @@
+// 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::{cbor::value::Value, iana, util::expect_err, CborSerializable, Label};
+use alloc::{borrow::ToOwned, vec};
+
+#[test]
+fn test_header_encode() {
+    let tests = vec![
+        (
+            Header {
+                alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+                key_id: vec![1, 2, 3],
+                partial_iv: vec![1, 2, 3],
+                ..Default::default()
+            },
+            concat!(
+                "a3", // 3-map
+                "01", "01", // 1 (alg) => A128GCM
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "06", "43", "010203", // 6 (partial-iv) => 3-bstr
+            ),
+        ),
+        (
+            Header {
+                alg: Some(Algorithm::PrivateUse(i64::MIN)),
+                ..Default::default()
+            },
+            concat!(
+                "a1", // 1-map
+                "01",
+                "3b7fffffffffffffff", // 1 (alg) => -lots
+            ),
+        ),
+        (
+            Header {
+                alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+                crit: vec![RegisteredLabel::Assigned(iana::HeaderParameter::Alg)],
+                content_type: Some(ContentType::Assigned(iana::CoapContentFormat::CoseEncrypt0)),
+                key_id: vec![1, 2, 3],
+                iv: vec![1, 2, 3],
+                rest: vec![
+                    (Label::Int(0x46), Value::from(0x47)),
+                    (Label::Int(0x66), Value::from(0x67)),
+                ],
+                ..Default::default()
+            },
+            concat!(
+                "a7", // 7-map
+                "01", "01", // 1 (alg) => A128GCM
+                "02", "81", "01", // 2 (crit) => 1-arr [x01]
+                "03", "10", // 3 (content-type) => 16
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "05", "43", "010203", // 5 (iv) => 3-bstr
+                "1846", "1847", // 46 => 47  (note canonical ordering)
+                "1866", "1867", // 66 => 67
+            ),
+        ),
+        (
+            Header {
+                alg: Some(Algorithm::Text("abc".to_owned())),
+                crit: vec![RegisteredLabel::Text("d".to_owned())],
+                content_type: Some(ContentType::Text("a/b".to_owned())),
+                key_id: vec![1, 2, 3],
+                iv: vec![1, 2, 3],
+                rest: vec![
+                    (Label::Int(0x46), Value::from(0x47)),
+                    (Label::Text("a".to_owned()), Value::from(0x47)),
+                ],
+                counter_signatures: vec![CoseSignature {
+                    signature: vec![1, 2, 3],
+                    ..Default::default()
+                }],
+                ..Default::default()
+            },
+            concat!(
+                "a8", // 8-map
+                "01", "63616263", // 1 (alg) => "abc"
+                "02", "81", "6164", // 2 (crit) => 1-arr ["d"]
+                "03", "63612f62", // 3 (content-type) => "a/b"
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "05", "43", "010203", // 5 (iv) => 3-bstr
+                "07", "83", // 7 (sig) => [3-arr for COSE_Signature
+                "40", "a0", "43010203", // ]
+                "1846", "1847", // 46 => 47  (note canonical ordering)
+                "6161", "1847", // "a" => 47
+            ),
+        ),
+        (
+            Header {
+                alg: Some(Algorithm::Text("abc".to_owned())),
+                crit: vec![RegisteredLabel::Text("d".to_owned())],
+                content_type: Some(ContentType::Text("a/b".to_owned())),
+                key_id: vec![1, 2, 3],
+                iv: vec![1, 2, 3],
+                rest: vec![
+                    (Label::Int(0x46), Value::from(0x47)),
+                    (Label::Text("a".to_owned()), Value::from(0x47)),
+                ],
+                counter_signatures: vec![
+                    CoseSignature {
+                        signature: vec![1, 2, 3],
+                        ..Default::default()
+                    },
+                    CoseSignature {
+                        signature: vec![3, 4, 5],
+                        ..Default::default()
+                    },
+                ],
+                ..Default::default()
+            },
+            concat!(
+                "a8", // 8-map
+                "01", "63616263", // 1 (alg) => "abc"
+                "02", "81", "6164", // 2 (crit) => 1-arr ["d"]
+                "03", "63612f62", // 3 (content-type) => "a/b"
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "05", "43", "010203", // 5 (iv) => 3-bstr
+                "07", "82", // 7 (sig) => 2-array
+                "83", "40", "a0", "43010203", // [3-arr for COSE_Signature]
+                "83", "40", "a0", "43030405", // [3-arr for COSE_Signature]
+                "1846", "1847", // 46 => 47  (note canonical ordering)
+                "6161", "1847", // "a" => 47
+            ),
+        ),
+        (
+            HeaderBuilder::new()
+                .add_critical(iana::HeaderParameter::Alg)
+                .add_critical(iana::HeaderParameter::Alg)
+                .build(),
+            concat!(
+                "a1", // 1-map
+                "02", "820101", // crit => 2-arr [1, 1]
+            ),
+        ),
+    ];
+    for (i, (header, header_data)) in tests.iter().enumerate() {
+        let got = header.clone().to_vec().unwrap();
+        assert_eq!(*header_data, hex::encode(&got), "case {}", i);
+
+        let mut got = Header::from_slice(&got).unwrap();
+        for mut sig in &mut got.counter_signatures {
+            sig.protected.original_data = None;
+        }
+        assert_eq!(*header, got);
+        assert!(!got.is_empty());
+
+        // The same data also parses as a `ProtectedHeader`
+        let protected = ProtectedHeader {
+            original_data: None,
+            header: header.clone(),
+        };
+        let protected_data = protected.clone().to_vec().unwrap();
+        assert_eq!(*header_data, hex::encode(&protected_data), "case {}", i);
+
+        let mut got = ProtectedHeader::from_slice(&protected_data).unwrap();
+        for mut sig in &mut got.header.counter_signatures {
+            sig.protected.original_data = None;
+        }
+        assert!(!got.is_empty());
+        assert_eq!(*header, got.header);
+
+        // Also try parsing as a protected header inside a `bstr`
+        let prot_bstr_val = protected.cbor_bstr().unwrap();
+        let mut got = ProtectedHeader::from_cbor_bstr(prot_bstr_val).unwrap();
+        for mut sig in &mut got.header.counter_signatures {
+            sig.protected.original_data = None;
+        }
+        assert!(!got.is_empty());
+        assert_eq!(*header, got.header);
+        assert_eq!(
+            *header_data,
+            hex::encode(&got.original_data.expect("missing original data"))
+        );
+    }
+}
+
+#[test]
+fn test_header_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a1", // 1-map
+                "01", "01", // 1 (alg) => 01
+                "01", // extraneous data
+            ),
+            "extraneous data in CBOR input",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "01", "08", // 1 (alg) => invalid value
+            ),
+            "expected value in IANA or private use range",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "01", "4101", // 1 (alg) => bstr (invalid value type)
+            ),
+            "expected int/tstr",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "02", "4101", // 2 (crit) => bstr (invalid value type)
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "02", "81", "4101", // 2 (crit) => [bstr] (invalid value type)
+            ),
+            "expected int/tstr",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "02", "80", // 2 (crit) => []
+            ),
+            "expected non-empty array",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "03", "81", "4101", // 3 (content-type) => [bstr] (invalid value type)
+            ),
+            "expected int/tstr",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "03", "19", "0606", // 3 (content-type) => invalid value 1542
+            ),
+            "expected recognized IANA value",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "03", "64", "20612f62" // 3 (content-type) => invalid value " a/b"
+            ),
+            "expected no leading/trailing whitespace",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "03", "64", "612f6220" // 3 (content-type) => invalid value "a/b "
+            ),
+            "expected no leading/trailing whitespace",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "03", "62", "6162" // 3 (content-type) => invalid value "ab"
+            ),
+            "expected text of form type/subtype",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "03", "60", // 3 (content-type) => invalid value ""
+            ),
+            "expected non-empty tstr",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "04", "40", // 4 (key-id) => 0-bstr
+            ),
+            "expected non-empty bstr",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "04", "01", // 4 (key-id) => invalid value type
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "05", "40", // 5 (iv) => 0-bstr
+            ),
+            "expected non-empty bstr",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "05", "01", // 5 (iv) => invalid value type
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "06", "40", // 6 (partial-iv) => 0-bstr
+            ),
+            "expected non-empty bstr",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "06", "01", // 6 (partial-iv) => invalid value type
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "07", "01", // 7 (counter-sig) => invalid value type
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "a1", // 1-map
+                "07", "80", // 7 (counter-sig) => 0-arr
+            ),
+            "expected non-empty sig array",
+        ),
+        (
+            concat!(
+                "a2", // 1-map
+                "05", "4101", // 5 (iv) => 1-bstr
+                "06", "4101", // 6 (partial-iv) => 1-bstr
+            ),
+            "expected only one of IV and partial IV",
+        ),
+        (
+            concat!(
+                "a2", // 2-map
+                "01", "63616263", // 1 (alg) => "abc"
+                "07", "82",       // 7 (sig) => 2-array
+                "63616263", // tstr (invalid)
+                "83", "40", "a0", "43010203", // [3-arr for COSE_Signature]
+            ),
+            "array or bstr value",
+        ),
+    ];
+    for (header_data, err_msg) in tests.iter() {
+        let data = hex::decode(header_data).unwrap();
+        let result = Header::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_header_decode_dup_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a3", // 3-map
+                "01", "01", // 1 (alg) => A128GCM
+                "1866", "1867", // 66 => 67
+                "1866", "1847", // 66 => 47
+            ),
+            "duplicate map key",
+        ),
+        (
+            concat!(
+                "a3", // 3-map
+                "01", "01", // 1 (alg) => A128GCM
+                "1866", "1867", // 66 => 67
+                "01", "01", // 1 (alg) => A128GCM (duplicate label)
+            ),
+            "duplicate map key",
+        ),
+    ];
+    for (header_data, err_msg) in tests.iter() {
+        let data = hex::decode(header_data).unwrap();
+        let result = Header::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_header_encode_dup_fail() {
+    let tests = vec![
+        Header {
+            alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+            crit: vec![RegisteredLabel::Assigned(iana::HeaderParameter::Alg)],
+            content_type: Some(ContentType::Assigned(iana::CoapContentFormat::CoseEncrypt0)),
+            key_id: vec![1, 2, 3],
+            iv: vec![1, 2, 3],
+            rest: vec![
+                (Label::Int(0x46), Value::from(0x47)),
+                (Label::Int(0x46), Value::from(0x67)),
+            ],
+            ..Default::default()
+        },
+        HeaderBuilder::new()
+            .text_value("doop".to_owned(), Value::from(1))
+            .text_value("doop".to_owned(), Value::from(2))
+            .build(),
+    ];
+    for header in tests {
+        let result = header.clone().to_vec();
+        expect_err(result, "duplicate map key");
+    }
+}
+
+#[test]
+fn test_header_builder() {
+    let tests = vec![
+        (
+            HeaderBuilder::new().build(),
+            Header {
+                ..Default::default()
+            },
+        ),
+        (
+            HeaderBuilder::new()
+                .algorithm(iana::Algorithm::A128GCM)
+                .add_critical(iana::HeaderParameter::Alg)
+                .add_critical_label(RegisteredLabel::Text("abc".to_owned()))
+                .content_format(iana::CoapContentFormat::CoseEncrypt0)
+                .key_id(vec![1, 2, 3])
+                .partial_iv(vec![4, 5, 6]) // removed by .iv() call
+                .iv(vec![1, 2, 3])
+                .value(0x46, Value::from(0x47))
+                .value(0x66, Value::from(0x67))
+                .build(),
+            Header {
+                alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+                crit: vec![
+                    RegisteredLabel::Assigned(iana::HeaderParameter::Alg),
+                    RegisteredLabel::Text("abc".to_owned()),
+                ],
+                content_type: Some(ContentType::Assigned(iana::CoapContentFormat::CoseEncrypt0)),
+                key_id: vec![1, 2, 3],
+                iv: vec![1, 2, 3],
+                rest: vec![
+                    (Label::Int(0x46), Value::from(0x47)),
+                    (Label::Int(0x66), Value::from(0x67)),
+                ],
+                ..Default::default()
+            },
+        ),
+        (
+            HeaderBuilder::new()
+                .algorithm(iana::Algorithm::A128GCM)
+                .add_critical(iana::HeaderParameter::Alg)
+                .add_critical_label(RegisteredLabel::Text("abc".to_owned()))
+                .content_type("type/subtype".to_owned())
+                .key_id(vec![1, 2, 3])
+                .iv(vec![1, 2, 3]) // removed by .partial_iv() call
+                .partial_iv(vec![4, 5, 6])
+                .build(),
+            Header {
+                alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+                crit: vec![
+                    RegisteredLabel::Assigned(iana::HeaderParameter::Alg),
+                    RegisteredLabel::Text("abc".to_owned()),
+                ],
+                content_type: Some(ContentType::Text("type/subtype".to_owned())),
+                key_id: vec![1, 2, 3],
+                partial_iv: vec![4, 5, 6],
+                ..Default::default()
+            },
+        ),
+    ];
+    for (got, want) in tests {
+        assert_eq!(got, want);
+    }
+}
+
+#[test]
+#[should_panic]
+fn test_header_builder_core_param_panic() {
+    // Attempting to set a core header parameter (in range [1,7]) via `.param()` panics.
+    let _hdr = HeaderBuilder::new().value(1, Value::Null).build();
+}
diff --git a/src/iana/mod.rs b/src/iana/mod.rs
new file mode 100644
index 0000000..41c2ef8
--- /dev/null
+++ b/src/iana/mod.rs
@@ -0,0 +1,745 @@
+// 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.
+//!
+//! Sources:
+//! - <https://www.iana.org/assignments/cose/cose.xhtml>
+//! - <https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml>
+//! - <https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats>
+
+#[cfg(test)]
+mod tests;
+
+/// Trait indicating an enum that can be constructed from `i64` values.
+pub trait EnumI64: Sized + Eq {
+    fn from_i64(i: i64) -> Option<Self>;
+    fn to_i64(&self) -> i64;
+}
+
+/// Trait indicating an enum with a range of private values.
+pub trait WithPrivateRange {
+    fn is_private(i: i64) -> bool;
+}
+
+/// Generate an enum with associated values, plus a `from_i64` method.
+macro_rules! iana_registry {
+    ( $(#[$attr:meta])* $enum_name:ident {$($(#[$fattr:meta])* $name:ident: $val:expr,)* } ) => {
+        #[allow(non_camel_case_types)]
+        $(#[$attr])*
+        #[non_exhaustive]
+        #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+        pub enum $enum_name {
+            $($(#[$fattr])* $name = $val,)*
+        }
+        impl EnumI64 for $enum_name {
+            fn from_i64(i: i64) -> Option<Self> {
+                match i {
+                    $(x if x == Self::$name as i64 => Some(Self::$name),)*
+                    _ => None,
+                }
+            }
+            #[inline]
+            fn to_i64(&self) -> i64 {
+                *self as i64
+            }
+        }
+    }
+}
+
+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: i64 = -65536;
+
+impl WithPrivateRange for HeaderParameter {
+    fn is_private(i: i64) -> 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,
+        /// Reserved
+        Reserved: 0,
+        /// 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: i64 = -65536;
+
+impl WithPrivateRange for Algorithm {
+    fn is_private(i: i64) -> 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: i64 = -65536;
+
+impl WithPrivateRange for EllipticCurve {
+    fn is_private(i: i64) -> 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,
+    }
+}
+
+iana_registry! {
+    /// CBOR tag values for COSE structures.
+    ///
+    /// From IANA registry <https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml>
+    /// as of 2021-03-19.
+    CborTag {
+        /// COSE Single Recipient Encrypted Data Object
+        CoseEncrypt0: 16,
+        /// COSE Mac w/o Recipients Object
+        CoseMac0: 17,
+        /// COSE Single Signer Data Object
+        CoseSign1: 18,
+        /// CBOR Web Token (CWT)
+        Cwt: 61,
+        /// COSE Encrypted Data Object
+        CoseEncrypt: 96,
+        /// COSE MACed Data Object
+        CoseMac: 97,
+        /// COSE Signed Data Object
+        CoseSign: 98,
+    }
+}
+
+iana_registry! {
+    /// CoAP Content Formats
+    ///
+    /// From IANA registry <https://www.iana.org/assignments/core-parameters/core-parameters.xhtml#content-formats>
+    /// as of 2021-03-19.
+    CoapContentFormat {
+        /// text/plain; charset=utf-8
+        TextPlainUtf8: 0,
+        /// application/cose; cose-type="cose-encrypt0"
+        CoseEncrypt0: 16,
+        /// application/cose; cose-type="cose-mac0"
+        CoseMac0: 17,
+        /// application/cose; cose-type="cose-sign1"
+        CoseSign1: 18,
+        /// application/link-format
+        LinkFormat: 40,
+        /// application/xml
+        Xml: 41,
+        /// application/octet-stream
+        OctetStream: 42,
+        /// application/exi
+        Exi: 47,
+        /// application/json
+        Json: 50,
+        /// application/json-patch+json
+        JsonPatchJson: 51,
+        /// application/merge-patch+json
+        MergePatchJson: 52,
+        /// application/cbor
+        Cbor: 60,
+        /// application/cwt
+        Cwt: 61,
+        /// application/multipart-core
+        MultipartCore: 62,
+        /// application/cbor-seq
+        CborSeq: 63,
+        /// application/cose; cose-type="cose-encrypt"
+        CoseEncrypt: 96,
+        /// application/cose; cose-type="cose-mac"
+        CoseMac: 97,
+        /// application/cose; cose-type="cose-sign"
+        CoseSign: 98,
+        /// application/cose-key
+        CoseKey: 101,
+        /// application/cose-key-set
+        CoseKeySet: 102,
+        /// application/senml+json
+        SenmlJson: 110,
+        /// application/sensml+json
+        SensmlJson: 111,
+        /// application/senml+cbor
+        SenmlCbor: 112,
+        /// application/sensml+cbor
+        SensmlCbor: 113,
+        /// application/senml-exi
+        SenmlExi: 114,
+        /// application/sensml-exi
+        SensmlExi: 115,
+        /// application/coap-group+json
+        CoapGroupJson: 256,
+        /// application/dots+cbor
+        DotsCbor: 271,
+        /// application/pkcs7-mime; smime-type=server-generated-key
+        Pkcs7MimeSmimeTypeServerGeneratedKey: 280,
+        /// application/pkcs7-mime; smime-type=certs-only
+        Pkcs7MimeSmimeTypeCertsOnly: 281,
+        /// application/pkcs7-mime; smime-type=CMC-Request
+        Pkcs7MimeSmimeTypeCmcRequest: 282,
+        /// application/pkcs7-mime; smime-type=CMC-Response
+        Pkcs7MimeSmimeTypeCmcResponse: 283,
+        /// application/pkcs8
+        Pkcs8: 284,
+        /// application/csrattrs
+        Csrattrs: 285,
+        /// application/pkcs10
+        Pkcs10: 286,
+        /// application/pkix-cert
+        PkixCert: 287,
+        /// application/senml+xml
+        SenmlXml: 310,
+        /// application/sensml+xml
+        SensmlXml: 311,
+        /// application/senml-etch+json
+        SenmlEtchJson: 320,
+        /// application/senml-etch+cbor
+        SenmlEtchCbor: 322,
+        /// application/td+json
+        TdJson: 432,
+        /// application/vnd.ocf+cbor
+        VndOcfCbor: 10000,
+        /// application/oscore
+        Oscore: 10001,
+        // application/json deflate
+        JsonDeflate: 11050,
+        // application/cbor deflate
+        CborDeflate: 11060,
+        /// application/vnd.oma.lwm2m+tlv
+        VndOmaLwm2mTlv: 11542,
+        /// application/vnd.oma.lwm2m+json
+        VndOmaLwm2mJson: 11543,
+        /// application/vnd.oma.lwm2m+cbor
+        VndOmaLwm2mCbor: 11544,
+    }
+}
diff --git a/src/iana/tests.rs b/src/iana/tests.rs
new file mode 100644
index 0000000..52cb203
--- /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_i64(-7));
+    assert_eq!(Some(Algorithm::A128GCM), Algorithm::from_i64(1));
+    assert_eq!(Algorithm::A128GCM as i64, 1);
+    assert_eq!(None, Algorithm::from_i64(8));
+    assert_eq!(None, Algorithm::from_i64(-65538));
+}
+
+#[test]
+fn test_header_param_private_range() {
+    assert!(!HeaderParameter::is_private(1));
+    assert!(HeaderParameter::is_private(-70_000));
+}
+
+#[test]
+fn test_elliptic_curve_private_range() {
+    assert!(!EllipticCurve::is_private(1));
+    assert!(EllipticCurve::is_private(-70_000));
+}
diff --git a/src/key/mod.rs b/src/key/mod.rs
new file mode 100644
index 0000000..f0c1eea
--- /dev/null
+++ b/src/key/mod.rs
@@ -0,0 +1,280 @@
+// 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::{
+    cbor::value::Value,
+    iana,
+    iana::EnumI64,
+    util::{to_cbor_array, AsCborValue, ValueTryAs},
+    Algorithm, CoseError, Label, Result,
+};
+use alloc::{collections::BTreeSet, vec, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Key type.
+pub type KeyType = crate::RegisteredLabel<iana::KeyType>;
+
+impl Default for KeyType {
+    fn default() -> Self {
+        KeyType::Assigned(iana::KeyType::Reserved)
+    }
+}
+
+/// Key operation.
+pub type KeyOperation = crate::RegisteredLabel<iana::KeyOperation>;
+
+/// A collection of [`CoseKey`] objects.
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseKeySet(pub Vec<CoseKey>);
+
+impl crate::CborSerializable for CoseKeySet {}
+
+impl AsCborValue for CoseKeySet {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        Ok(Self(
+            value.try_as_array_then_convert(CoseKey::from_cbor_value)?,
+        ))
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        to_cbor_array(self.0)
+    }
+}
+
+/// 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, PartialEq)]
+pub struct CoseKey {
+    /// Key type identification.
+    pub kty: KeyType,
+    /// Key identification.
+    pub key_id: Vec<u8>,
+    /// Key use restriction to this algorithm.
+    pub alg: Option<Algorithm>,
+    /// Restrict set of possible operations.
+    pub key_ops: BTreeSet<KeyOperation>,
+    /// Base IV to be xor-ed with partial IVs.
+    pub base_iv: Vec<u8>,
+    /// Any additional parameter (label,value) pairs.  If duplicate labels are present,
+    /// CBOR-encoding will fail.
+    pub params: Vec<(Label, Value)>,
+}
+
+impl crate::CborSerializable for CoseKey {}
+
+const KTY: Label = Label::Int(iana::KeyParameter::Kty as i64);
+const KID: Label = Label::Int(iana::KeyParameter::Kid as i64);
+const ALG: Label = Label::Int(iana::KeyParameter::Alg as i64);
+const KEY_OPS: Label = Label::Int(iana::KeyParameter::KeyOps as i64);
+const BASE_IV: Label = Label::Int(iana::KeyParameter::BaseIv as i64);
+
+impl AsCborValue for CoseKey {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let m = value.try_as_map()?;
+        let mut key = Self::default();
+        let mut seen = BTreeSet::new();
+        for (l, value) in m.into_iter() {
+            // The `ciborium` CBOR library does not police duplicate map keys.
+            // RFC 8152 section 14 requires that COSE does police duplicates, so do it here.
+            let label = Label::from_cbor_value(l)?;
+            if seen.contains(&label) {
+                return Err(CoseError::DuplicateMapKey);
+            }
+            seen.insert(label.clone());
+            match label {
+                KTY => key.kty = KeyType::from_cbor_value(value)?,
+
+                KID => {
+                    key.key_id = value.try_as_nonempty_bytes()?;
+                }
+
+                ALG => key.alg = Some(Algorithm::from_cbor_value(value)?),
+
+                KEY_OPS => {
+                    let key_ops = value.try_as_array()?;
+                    for key_op in key_ops.into_iter() {
+                        if !key.key_ops.insert(KeyOperation::from_cbor_value(key_op)?) {
+                            return Err(CoseError::UnexpectedItem(
+                                "repeated array entry",
+                                "unique array label",
+                            ));
+                        }
+                    }
+                    if key.key_ops.is_empty() {
+                        return Err(CoseError::UnexpectedItem("empty array", "non-empty array"));
+                    }
+                }
+
+                BASE_IV => {
+                    key.base_iv = value.try_as_nonempty_bytes()?;
+                }
+
+                label => key.params.push((label, value)),
+            }
+        }
+        // Check that key type has been set.
+        if key.kty == KeyType::Assigned(iana::KeyType::Reserved) {
+            return Err(CoseError::UnexpectedItem(
+                "no kty label",
+                "mandatory kty label",
+            ));
+        }
+
+        Ok(key)
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        let mut map: Vec<(Value, Value)> = vec![(KTY.to_cbor_value()?, self.kty.to_cbor_value()?)];
+        if !self.key_id.is_empty() {
+            map.push((KID.to_cbor_value()?, Value::Bytes(self.key_id)));
+        }
+        if let Some(alg) = self.alg {
+            map.push((ALG.to_cbor_value()?, alg.to_cbor_value()?));
+        }
+        if !self.key_ops.is_empty() {
+            map.push((KEY_OPS.to_cbor_value()?, to_cbor_array(self.key_ops)?));
+        }
+        if !self.base_iv.is_empty() {
+            map.push((BASE_IV.to_cbor_value()?, Value::Bytes(self.base_iv)));
+        }
+        let mut seen = BTreeSet::new();
+        for (label, value) in self.params {
+            if seen.contains(&label) {
+                return Err(CoseError::DuplicateMapKey);
+            }
+            seen.insert(label.clone());
+            map.push((label.to_cbor_value()?, value));
+        }
+        Ok(Value::Map(map))
+    }
+}
+
+/// Builder for [`CoseKey`] objects.
+#[derive(Debug, Default)]
+pub struct CoseKeyBuilder(CoseKey);
+
+impl CoseKeyBuilder {
+    builder! {CoseKey}
+    builder_set! {key_id: Vec<u8>}
+    builder_set! {base_iv: Vec<u8>}
+
+    /// Constructor for an elliptic curve public key specified by `x` and `y` coordinates.
+    pub fn new_ec2_pub_key(curve: iana::EllipticCurve, x: Vec<u8>, y: Vec<u8>) -> Self {
+        Self(CoseKey {
+            kty: KeyType::Assigned(iana::KeyType::EC2),
+            params: vec![
+                (
+                    Label::Int(iana::Ec2KeyParameter::Crv as i64),
+                    Value::from(curve as u64),
+                ),
+                (Label::Int(iana::Ec2KeyParameter::X as i64), Value::Bytes(x)),
+                (Label::Int(iana::Ec2KeyParameter::Y as i64), 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<u8>, y_sign: bool) -> Self {
+        Self(CoseKey {
+            kty: KeyType::Assigned(iana::KeyType::EC2),
+            params: vec![
+                (
+                    Label::Int(iana::Ec2KeyParameter::Crv as i64),
+                    Value::from(curve as u64),
+                ),
+                (Label::Int(iana::Ec2KeyParameter::X as i64), Value::Bytes(x)),
+                (
+                    Label::Int(iana::Ec2KeyParameter::Y as i64),
+                    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<u8>,
+        y: Vec<u8>,
+        d: Vec<u8>,
+    ) -> Self {
+        let mut builder = Self::new_ec2_pub_key(curve, x, y);
+        builder
+            .0
+            .params
+            .push((Label::Int(iana::Ec2KeyParameter::D as i64), Value::Bytes(d)));
+        builder
+    }
+
+    /// Constructor for a symmetric key specified by `k`.
+    pub fn new_symmetric_key(k: Vec<u8>) -> Self {
+        Self(CoseKey {
+            kty: KeyType::Assigned(iana::KeyType::Symmetric),
+            params: vec![(
+                Label::Int(iana::SymmetricKeyParameter::K as i64),
+                Value::Bytes(k),
+            )],
+            ..Default::default()
+        })
+    }
+
+    /// Set the algorithm.
+    #[must_use]
+    pub fn algorithm(mut self, alg: iana::Algorithm) -> Self {
+        self.0.alg = Some(Algorithm::Assigned(alg));
+        self
+    }
+
+    /// Add a key operation.
+    #[must_use]
+    pub fn add_key_op(mut self, op: iana::KeyOperation) -> Self {
+        self.0.key_ops.insert(KeyOperation::Assigned(op));
+        self
+    }
+
+    /// Set a parameter value.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if it used to set a parameter label from the [`iana::KeyParameter`]
+    /// range.
+    #[must_use]
+    pub fn param(mut self, label: i64, value: Value) -> Self {
+        if iana::KeyParameter::from_i64(label).is_some() {
+            panic!("param() method used to set KeyParameter"); // safe: invalid input
+        }
+        self.0.params.push((Label::Int(label), value));
+        self
+    }
+}
diff --git a/src/key/tests.rs b/src/key/tests.rs
new file mode 100644
index 0000000..713fe1a
--- /dev/null
+++ b/src/key/tests.rs
@@ -0,0 +1,719 @@
+// 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::{cbor::value::Value, iana, util::expect_err, CborSerializable};
+use alloc::{borrow::ToOwned, vec};
+
+#[test]
+fn test_cose_key_encode() {
+    let tests = vec![
+        (
+            CoseKey {
+                kty: KeyType::Assigned(iana::KeyType::OKP),
+                key_id: 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),
+                key_id: vec![1, 2, 3],
+                key_ops: vec![
+                    KeyOperation::Assigned(iana::KeyOperation::Encrypt),
+                    KeyOperation::Assigned(iana::KeyOperation::Decrypt),
+                    KeyOperation::Text("abc".to_owned()),
+                ]
+                .into_iter()
+                .collect(),
+                ..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: vec![
+                    (Label::Int(0x46), Value::from(0x47)),
+                    (Label::Int(0x66), Value::from(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: vec![
+                    (Label::Int(0x1234), Value::from(0x47)),
+                    (Label::Text("a".to_owned()), Value::from(0x67)),
+                ],
+                ..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: vec![
+                    (Label::Int(0x66), Value::from(0x67)),
+                    (Label::Text("a".to_owned()), Value::from(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, Value::Null)
+            .build(),
+            concat!(
+                "a60102032620012158206b4ad240073b",
+                "99cad65ab8417ce29c6844ad0ae77ce8",
+                "b3f7e41233f5b9129465225820a7dc1c",
+                "39391ab300f7b1787b6e569a031dd075",
+                "0fe2509b880a41f06666fff7853a0001",
+                "116ff6"
+            ),
+        ),
+        (
+            CoseKeyBuilder::new_ec2_pub_key_y_sign(
+                iana::EllipticCurve::P_256,
+                hex::decode("aabbcc").unwrap(),
+                false,
+            )
+            .build(),
+            concat!(
+                "a4", // 3-map
+                "01", "02", // 1 (kty) => 2 (EC2)
+                "20", "01", // -1 (crv) => 1 (P_256)
+                "21", "43", "aabbcc", // -2 (x) => 3-bstr
+                "22", "f4" // -3 (y) => false
+            ),
+        ),
+    ];
+    for (i, (key, key_data)) in tests.iter().enumerate() {
+        let got = key.clone().to_vec().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<u8> = 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_data = keyset.clone().to_vec().unwrap();
+    assert_eq!(hex::encode(keyset_data), hex::encode(&got_data));
+
+    let got = CoseKeySet::from_slice(&got_data).unwrap();
+    assert_eq!(got, keyset);
+}
+
+#[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 = key.clone().to_vec().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<u8> = 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 = keyset.to_vec().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),
+                key_id: b"our-secret".to_vec(),
+                params: vec![
+                    (Label::Int(iana::SymmetricKeyParameter::K as i64) ,
+                        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),
+                key_id: b"our-secret2".to_vec(),
+                params: vec![(
+                    Label::Int(iana::SymmetricKeyParameter::K as i64) ,
+                        Value::Bytes(hex::decode("849b5786457c1491be3a76dcea6c4271").unwrap()),
+                )],
+                ..Default::default()
+            },
+            concat!("a3",
+                    "0104",
+                    "024b6f75722d73656372657432",
+                    "2050849b5786457c1491be3a76dcea6c4271",
+            ),
+        ),
+        (
+            CoseKey {
+                kty: KeyType::Assigned(iana::KeyType::Symmetric),
+                key_id: b"018c0ae5-4d9b-471b-bfd6-eef314bc7037".to_vec(),
+                params: vec![(
+                    Label::Int(iana::SymmetricKeyParameter::K as i64) ,
+                        Value::Bytes(hex::decode("849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188").unwrap()),
+                )],
+                ..Default::default()
+            },
+            concat!("a3",
+                    "0104",
+                    "02582430313863306165352d346439622d343731622d626664362d656566333134626337303337",
+                    "205820849b57219dae48de646d07dbb533566e976686457c1491be3a76dcea6c427188",
+            ),
+        ),
+    ];
+    for (i, (key, key_data)) in tests.iter().enumerate() {
+        let got = key.clone().to_vec().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<u8> = 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 = keyset.to_vec().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
+            ),
+            "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);
+    }
+}
+
+#[test]
+fn test_cose_keyset_decode_fail() {
+    let tests = vec![(
+        concat!(
+            "a1", // 1-map
+            "a1", // 1-map
+            "01", "01", // 1 (kty) => OKP
+            "00"
+        ),
+        "expected array",
+    )];
+    for (keyset_data, err_msg) in tests.iter() {
+        let data = hex::decode(keyset_data).unwrap();
+        let result = CoseKeySet::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+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
+            ),
+            "duplicate map key",
+        ),
+        (
+            concat!(
+                "a3", // 3-map
+                "01", "01", // 1 (kty) => OKP
+                "02", "41", "01", // 2 (kid) => 1-bstr
+                "01", "01", // 1 (kty) => OKP  (duplicate label)
+            ),
+            "duplicate map key",
+        ),
+    ];
+    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_cose_key_encode_dup_fail() {
+    let tests = vec![CoseKeyBuilder::new()
+        .param(10, Value::from(0))
+        .param(10, Value::from(0))
+        .build()];
+    for key in tests {
+        let result = key.clone().to_vec();
+        expect_err(result, "duplicate map key");
+    }
+}
+
+#[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: vec![(
+                    Label::Int(iana::SymmetricKeyParameter::K as i64),
+                    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: vec![(
+                    Label::Int(iana::SymmetricKeyParameter::K as i64),
+                    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),
+                key_id: vec![4, 5],
+                params: vec![(
+                    Label::Int(iana::SymmetricKeyParameter::K as i64),
+                    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: vec![
+                    KeyOperation::Assigned(iana::KeyOperation::Encrypt),
+                    KeyOperation::Assigned(iana::KeyOperation::Decrypt),
+                ]
+                .into_iter()
+                .collect(),
+                params: vec![(
+                    Label::Int(iana::SymmetricKeyParameter::K as i64),
+                    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: vec![(
+                    Label::Int(iana::SymmetricKeyParameter::K as i64),
+                    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, Value::Null)
+            .build();
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 0000000..3c46fcf
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,128 @@
+// 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)][COSE].
+//!
+//! Builds on the [`ciborium`](https://docs.rs/ciborium) crate for underlying [CBOR][CBOR] support.
+//!
+//! ## Usage
+//!
+//! ```
+//! # #[derive(Copy, Clone)]
+//! # struct FakeSigner {}
+//! # impl FakeSigner {
+//! #     fn sign(&self, data: &[u8]) -> Vec<u8> {
+//! #         data.to_vec()
+//! #     }
+//! #     fn verify(&self, sig: &[u8], data: &[u8]) -> Result<(), String> {
+//! #         if sig != self.sign(data) {
+//! #             Err("failed to verify".to_owned())
+//! #         } else {
+//! #             Ok(())
+//! #         }
+//! #     }
+//! # }
+//! # let signer = FakeSigner {};
+//! # let verifier = signer;
+//! use coset::{iana, CborSerializable};
+//!
+//! // Inputs.
+//! let pt = b"This is the content";
+//! let aad = b"this is additional data";
+//!
+//! // Build a `CoseSign1` object.
+//! let protected = coset::HeaderBuilder::new()
+//!     .algorithm(iana::Algorithm::ES256)
+//!     .key_id(b"11".to_vec())
+//!     .build();
+//! let sign1 = coset::CoseSign1Builder::new()
+//!     .protected(protected)
+//!     .payload(pt.to_vec())
+//!     .create_signature(aad, |pt| signer.sign(pt)) // closure to do sign operation
+//!     .build();
+//!
+//! // Serialize to bytes.
+//! let sign1_data = sign1.to_vec().unwrap();
+//! println!(
+//!     "'{}' + '{}' => {}",
+//!     String::from_utf8_lossy(pt),
+//!     String::from_utf8_lossy(aad),
+//!     hex::encode(&sign1_data)
+//! );
+//!
+//! // At the receiving end, deserialize the bytes back to a `CoseSign1` object.
+//! let mut sign1 = coset::CoseSign1::from_slice(&sign1_data).unwrap();
+//!
+//! // At this point, real code would validate the protected headers.
+//!
+//! // Check the signature, which needs to have the same `aad` provided, by
+//! // providing a closure that can do the verify operation.
+//! let result = sign1.verify_signature(aad, |sig, data| verifier.verify(sig, data));
+//! println!("Signature verified: {:?}.", result);
+//! assert!(result.is_ok());
+//!
+//! // Changing an unprotected header leaves the signature valid.
+//! sign1.unprotected.content_type = Some(coset::ContentType::Text("text/plain".to_owned()));
+//! assert!(sign1
+//!     .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+//!     .is_ok());
+//!
+//! // Providing a different `aad` means the signature won't validate.
+//! assert!(sign1
+//!     .verify_signature(b"not aad", |sig, data| verifier.verify(sig, data))
+//!     .is_err());
+//!
+//! // Changing a protected header invalidates the signature.
+//! sign1.protected.original_data = None;
+//! sign1.protected.header.content_type = Some(coset::ContentType::Text("text/plain".to_owned()));
+//! assert!(sign1
+//!     .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+//!     .is_err());
+//! ```
+//!
+//! [COSE]: https://tools.ietf.org/html/rfc8152
+//! [CBOR]: https://tools.ietf.org/html/rfc7049
+
+#![no_std]
+#![deny(rustdoc::broken_intra_doc_links)]
+extern crate alloc;
+
+/// Use std to allow building as a dylib.
+extern crate std;
+
+/// Re-export of the `ciborium` crate used for underlying CBOR encoding.
+pub use ciborium as cbor;
+
+#[macro_use]
+pub(crate) mod util;
+
+#[macro_use]
+pub mod iana;
+
+mod common;
+pub use common::*;
+mod context;
+pub use context::*;
+mod encrypt;
+pub use encrypt::*;
+mod header;
+pub use header::*;
+mod key;
+pub use key::*;
+mod mac;
+pub use mac::*;
+mod sign;
+pub use sign::*;
diff --git a/src/mac/mod.rs b/src/mac/mod.rs
new file mode 100644
index 0000000..1fb47b1
--- /dev/null
+++ b/src/mac/mod.rs
@@ -0,0 +1,346 @@
+// 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_Mac functionality.
+
+use crate::{
+    cbor,
+    cbor::value::Value,
+    iana,
+    util::{cbor_type_error, to_cbor_array, AsCborValue, ValueTryAs},
+    CoseError, CoseRecipient, Header, ProtectedHeader, Result,
+};
+use alloc::{borrow::ToOwned, vec, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Structure representing a message with authentication code (MAC).
+///
+/// ```cddl
+///  COSE_Mac = [
+///     Headers,
+///     payload : bstr / nil,
+///     tag : bstr,
+///     recipients :[+COSE_recipient]
+///  ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseMac {
+    pub protected: ProtectedHeader,
+    pub unprotected: Header,
+    pub payload: Option<Vec<u8>>,
+    pub tag: Vec<u8>,
+    pub recipients: Vec<CoseRecipient>,
+}
+
+impl crate::CborSerializable for CoseMac {}
+
+impl crate::TaggedCborSerializable for CoseMac {
+    const TAG: u64 = iana::CborTag::CoseMac as u64;
+}
+
+impl AsCborValue for CoseMac {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() != 5 {
+            return Err(CoseError::UnexpectedItem("array", "array with 5 items"));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        let recipients = a
+            .remove(4)
+            .try_as_array_then_convert(CoseRecipient::from_cbor_value)?;
+
+        Ok(Self {
+            recipients,
+            tag: a.remove(3).try_as_bytes()?,
+            payload: match a.remove(2) {
+                Value::Bytes(b) => Some(b),
+                Value::Null => None,
+                v => return cbor_type_error(&v, "bstr"),
+            },
+            unprotected: Header::from_cbor_value(a.remove(1))?,
+            protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(Value::Array(vec![
+            self.protected.cbor_bstr()?,
+            self.unprotected.to_cbor_value()?,
+            match self.payload {
+                None => Value::Null,
+                Some(b) => Value::Bytes(b),
+            },
+            Value::Bytes(self.tag),
+            to_cbor_array(self.recipients)?,
+        ]))
+    }
+}
+
+impl CoseMac {
+    /// Verify the `tag` value using the provided `mac` function, feeding it
+    /// the `tag` value and the combined to-be-MACed data (in that order).
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `payload` has not been set.
+    pub fn verify_tag<F, E>(&self, external_aad: &[u8], verify: F) -> Result<(), E>
+    where
+        F: FnOnce(&[u8], &[u8]) -> Result<(), E>,
+    {
+        let tbm = self.tbm(external_aad);
+        verify(&self.tag, &tbm)
+    }
+
+    /// Construct the to-be-MAC-ed data for this object. Any protected header values should be set
+    /// before using this method, as should the `payload`.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `payload` has not been set.
+    fn tbm(&self, external_aad: &[u8]) -> Vec<u8> {
+        mac_structure_data(
+            MacContext::CoseMac,
+            self.protected.clone(),
+            external_aad,
+            self.payload.as_ref().expect("payload missing"), // safe: documented
+        )
+    }
+}
+
+/// Builder for [`CoseMac`] objects.
+#[derive(Debug, Default)]
+pub struct CoseMacBuilder(CoseMac);
+
+impl CoseMacBuilder {
+    builder! {CoseMac}
+    builder_set_protected! {protected}
+    builder_set! {unprotected: Header}
+    builder_set! {tag: Vec<u8>}
+    builder_set_optional! {payload: Vec<u8>}
+
+    /// Add a [`CoseRecipient`].
+    #[must_use]
+    pub fn add_recipient(mut self, recipient: CoseRecipient) -> Self {
+        self.0.recipients.push(recipient);
+        self
+    }
+
+    /// Calculate the tag value, using `mac`. Any protected header values should be set
+    /// before using this method, as should the `payload`.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `payload` has not been set.
+    #[must_use]
+    pub fn create_tag<F>(self, external_aad: &[u8], create: F) -> Self
+    where
+        F: FnOnce(&[u8]) -> Vec<u8>,
+    {
+        let tbm = self.0.tbm(external_aad);
+        self.tag(create(&tbm))
+    }
+
+    /// Calculate the tag value, using `mac`. Any protected header values should be set
+    /// before using this method, as should the `payload`.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `payload` has not been set.
+    pub fn try_create_tag<F, E>(self, external_aad: &[u8], create: F) -> Result<Self, E>
+    where
+        F: FnOnce(&[u8]) -> Result<Vec<u8>, E>,
+    {
+        let tbm = self.0.tbm(external_aad);
+        Ok(self.tag(create(&tbm)?))
+    }
+}
+
+/// Structure representing a message with authentication code (MAC)
+/// where the relevant key is implicit.
+///
+/// ```cddl
+///  COSE_Mac0 = [
+///     Headers,
+///     payload : bstr / nil,
+///     tag : bstr,
+///  ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseMac0 {
+    pub protected: ProtectedHeader,
+    pub unprotected: Header,
+    pub payload: Option<Vec<u8>>,
+    pub tag: Vec<u8>,
+}
+
+impl crate::CborSerializable for CoseMac0 {}
+
+impl crate::TaggedCborSerializable for CoseMac0 {
+    const TAG: u64 = iana::CborTag::CoseMac0 as u64;
+}
+
+impl AsCborValue for CoseMac0 {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() != 4 {
+            return Err(CoseError::UnexpectedItem("array", "array with 4 items"));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        Ok(Self {
+            tag: a.remove(3).try_as_bytes()?,
+            payload: match a.remove(2) {
+                Value::Bytes(b) => Some(b),
+                Value::Null => None,
+                v => return cbor_type_error(&v, "bstr"),
+            },
+            unprotected: Header::from_cbor_value(a.remove(1))?,
+            protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(Value::Array(vec![
+            self.protected.cbor_bstr()?,
+            self.unprotected.to_cbor_value()?,
+            match self.payload {
+                None => Value::Null,
+                Some(b) => Value::Bytes(b),
+            },
+            Value::Bytes(self.tag),
+        ]))
+    }
+}
+
+impl CoseMac0 {
+    /// Verify the `tag` value using the provided `mac` function, feeding it
+    /// the `tag` value and the combined to-be-MACed data (in that order).
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `payload` has not been set.
+    pub fn verify_tag<F, E>(&self, external_aad: &[u8], verify: F) -> Result<(), E>
+    where
+        F: FnOnce(&[u8], &[u8]) -> Result<(), E>,
+    {
+        let tbm = self.tbm(external_aad);
+        verify(&self.tag, &tbm)
+    }
+
+    /// Construct the to-be-MAC-ed data for this object. Any protected header values should be set
+    /// before using this method, as should the `payload`.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `payload` has not been set.
+    fn tbm(&self, external_aad: &[u8]) -> Vec<u8> {
+        mac_structure_data(
+            MacContext::CoseMac0,
+            self.protected.clone(),
+            external_aad,
+            self.payload.as_ref().expect("payload missing"), // safe: documented
+        )
+    }
+}
+
+/// Builder for [`CoseMac0`] objects.
+#[derive(Debug, Default)]
+pub struct CoseMac0Builder(CoseMac0);
+
+impl CoseMac0Builder {
+    builder! {CoseMac0}
+    builder_set_protected! {protected}
+    builder_set! {unprotected: Header}
+    builder_set! {tag: Vec<u8>}
+    builder_set_optional! {payload: Vec<u8>}
+
+    /// Calculate the tag value, using `mac`. Any protected header values should be set
+    /// before using this method, as should the `payload`.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `payload` has not been set.
+    #[must_use]
+    pub fn create_tag<F>(self, external_aad: &[u8], create: F) -> Self
+    where
+        F: FnOnce(&[u8]) -> Vec<u8>,
+    {
+        let tbm = self.0.tbm(external_aad);
+        self.tag(create(&tbm))
+    }
+
+    /// Calculate the tag value, using `mac`. Any protected header values should be set
+    /// before using this method, as should the `payload`.
+    ///
+    /// # Panics
+    ///
+    /// This function will panic if the `payload` has not been set.
+    pub fn try_create_tag<F, E>(self, external_aad: &[u8], create: F) -> Result<Self, E>
+    where
+        F: FnOnce(&[u8]) -> Result<Vec<u8>, E>,
+    {
+        let tbm = self.0.tbm(external_aad);
+        Ok(self.tag(create(&tbm)?))
+    }
+}
+
+/// Possible MAC contexts.
+#[derive(Clone, Copy, Debug)]
+pub enum MacContext {
+    CoseMac,
+    CoseMac0,
+}
+
+impl MacContext {
+    /// Return the context string as per RFC 8152 section 6.3.
+    fn text(&self) -> &'static str {
+        match self {
+            MacContext::CoseMac => "MAC",
+            MacContext::CoseMac0 => "MAC0",
+        }
+    }
+}
+
+/// Create a binary blob that will be signed.
+//
+/// ```cddl
+///  MAC_structure = [
+///       context : "MAC" / "MAC0",
+///       protected : empty_or_serialized_map,
+///       external_aad : bstr,
+///       payload : bstr
+///  ]
+/// ```
+pub fn mac_structure_data(
+    context: MacContext,
+    protected: ProtectedHeader,
+    external_aad: &[u8],
+    payload: &[u8],
+) -> Vec<u8> {
+    let arr = vec![
+        Value::Text(context.text().to_owned()),
+        protected.cbor_bstr().expect("failed to serialize header"), // safe: always serializable
+        Value::Bytes(external_aad.to_vec()),
+        Value::Bytes(payload.to_vec()),
+    ];
+
+    let mut data = Vec::new();
+    cbor::ser::into_writer(&Value::Array(arr), &mut data).unwrap(); // safe: always serializable
+    data
+}
diff --git a/src/mac/tests.rs b/src/mac/tests.rs
new file mode 100644
index 0000000..9a5fffc
--- /dev/null
+++ b/src/mac/tests.rs
@@ -0,0 +1,763 @@
+// 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::{
+    cbor::value::Value, util::expect_err, CborSerializable, ContentType, CoseKeyBuilder,
+    CoseRecipientBuilder, HeaderBuilder, TaggedCborSerializable,
+};
+use alloc::{
+    string::{String, ToString},
+    vec,
+    vec::Vec,
+};
+
+#[test]
+fn test_cose_mac_decode() {
+    let tests: Vec<(CoseMac, &'static str)> = vec![
+        (
+            CoseMacBuilder::new().build(),
+            concat!(
+                "85", // 5-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "f6", // null
+                "40", // 0-bstr
+                "80", // 0-arr
+            ),
+        ),
+        (
+            CoseMacBuilder::new().payload(vec![]).build(),
+            concat!(
+                "85", // 5-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "40", // 0-bstr
+                "80", // 0-arr
+            ),
+        ),
+    ];
+    for (i, (mac, mac_data)) in tests.iter().enumerate() {
+        let got = mac.clone().to_vec().unwrap();
+        assert_eq!(*mac_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseMac::from_slice(&got).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*mac, got);
+    }
+}
+
+#[test]
+fn test_cose_mac_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a2",   // 2-map (should be tuple)
+                "40",   // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0",   // 0-map
+                "4100", // 1-bstr
+                "40",   // 0-bstr
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple (should be 5-tuple)
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "40", // 0-bstr
+            ),
+            "expected array with 5 items",
+        ),
+        (
+            concat!(
+                "85", // 5-tuple
+                "80", // 0-tuple (should be bstr)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "40", // 0-bstr
+                "80", // 0-arr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "85", // 5-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40", // 0-bstr (should be map)
+                "40", // 0-bstr
+                "40", // 0-bstr
+                "80", // 0-arr
+            ),
+            "expected map",
+        ),
+        (
+            concat!(
+                "85", // 5-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "60", // 0-tstr (should be bstr)
+                "40", // 0-bstr
+                "80", // 0-arr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "85", // 5-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "60", // 0-tstr
+                "80", // 0-arr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "85", // 5-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "40", // 0-bstr
+                "40", // 0-bstr
+            ),
+            "expected array",
+        ),
+    ];
+    for (mac_data, err_msg) in tests.iter() {
+        let data = hex::decode(mac_data).unwrap();
+        let result = CoseMac::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_rfc8152_cose_mac_decode() {
+    // COSE_Mac structures from RFC 8152 section C.5.
+    let tests: Vec<(CoseMac, &'static str)> = vec![
+        (
+            CoseMacBuilder::new()
+                .protected(
+                    HeaderBuilder::new()
+                        .algorithm(iana::Algorithm::AES_MAC_256_64)
+                        .build(),
+                )
+                .payload(b"This is the content.".to_vec())
+                .tag(hex::decode("9e1226ba1f81b848").unwrap())
+                .add_recipient(
+                    CoseRecipientBuilder::new()
+                        .unprotected(
+                            HeaderBuilder::new()
+                                .algorithm(iana::Algorithm::Direct)
+                                .key_id(b"our-secret".to_vec())
+                                .build(),
+                        )
+                        .ciphertext(vec![])
+                        .build(),
+                )
+                .build(),
+            concat!(
+                "d861",
+                "85",
+                "43",
+                "a1010f",
+                "a0",
+                "54",
+                "546869732069732074686520636f6e74656e742e",
+                "48",
+                "9e1226ba1f81b848",
+                "81",
+                "83",
+                "40",
+                "a2",
+                "01",
+                "25",
+                "04",
+                "4a",
+                "6f75722d736563726574",
+                "40",
+            ),
+        ),
+        (
+            CoseMacBuilder::new()
+                .protected(HeaderBuilder::new().algorithm(iana::Algorithm::HMAC_256_256).build())
+                .payload(b"This is the content.".to_vec())
+                .tag(hex::decode("81a03448acd3d305376eaa11fb3fe416a955be2cbe7ec96f012c994bc3f16a41").unwrap())
+                .add_recipient(
+                    CoseRecipientBuilder::new()
+                        .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ECDH_SS_HKDF_256).build())
+                        .unprotected(
+                            HeaderBuilder::new()
+                                .key_id(b"meriadoc.brandybuck@buckland.example".to_vec())
+                                .value(
+                                    iana::HeaderAlgorithmParameter::StaticKeyId as i64,
+                                    Value::Bytes(b"peregrin.took@tuckborough.example".to_vec())
+                                )
+                                .value(
+                                    iana::HeaderAlgorithmParameter::PartyUNonce as i64,
+                                    Value::Bytes(hex::decode("4d8553e7e74f3c6a3a9dd3ef286a8195cbf8a23d19558ccfec7d34b824f42d92bd06bd2c7f0271f0214e141fb779ae2856abf585a58368b017e7f2a9e5ce4db5").unwrap())
+                                )
+                                .build(),
+                        )
+                        .ciphertext(vec![])
+                        .build(),
+                )
+                .build(),
+            // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+            concat!(
+                "d861",
+                "85",
+                "43", "a10105",
+                "a0",
+                "54", "546869732069732074686520636f6e74656e742e",
+                "5820", "81a03448acd3d305376eaa11fb3fe416a955be2cbe7ec96f012c994bc3f16a41",
+                "81",
+                "83",
+                "44", "a101381a",
+                "a3",
+                "04",
+                "5824", "6d65726961646f632e6272616e64796275636b406275636b6c616e642e6578616d706c65",
+                "22",
+                "5821", "706572656772696e2e746f6f6b407475636b626f726f7567682e6578616d706c65",
+                "35",
+                "5840", "4d8553e7e74f3c6a3a9dd3ef286a8195cbf8a23d19558ccfec7d34b824f42d92bd06bd2c7f0271f0214e141fb779ae2856abf585a58368b017e7f2a9e5ce4db5",
+                "40",
+            ),
+        ),
+        (
+            CoseMacBuilder::new()
+                .protected(HeaderBuilder::new().algorithm(iana::Algorithm::AES_MAC_128_64).build())
+                .payload(b"This is the content.".to_vec())
+                .tag(hex::decode("36f5afaf0bab5d43").unwrap())
+                .add_recipient(
+                    CoseRecipientBuilder::new()
+                        .unprotected(
+                            HeaderBuilder::new()
+                                .algorithm(iana::Algorithm::A256KW)
+                                .key_id(b"018c0ae5-4d9b-471b-bfd6-eef314bc7037".to_vec())
+                                .build(),
+                        )
+                        .ciphertext(hex::decode("711ab0dc2fc4585dce27effa6781c8093eba906f227b6eb0").unwrap())
+                        .build(),
+                )
+                .build(),
+            concat!(
+                "d861",
+                "85",
+                "43", "a1010e",
+                "a0",
+                "54", "546869732069732074686520636f6e74656e742e",
+                "48", "36f5afaf0bab5d43",
+                "81",
+                "83",
+                "40",
+                "a2",
+                "01",
+                "24",
+                "04",
+                "5824", "30313863306165352d346439622d343731622d626664362d656566333134626337303337",
+                "5818", "711ab0dc2fc4585dce27effa6781c8093eba906f227b6eb0",
+            ),
+        ),
+        (
+            CoseMacBuilder::new()
+                .protected(HeaderBuilder::new().algorithm(iana::Algorithm::HMAC_256_256).build())
+                .payload(b"This is the content.".to_vec())
+                .tag(hex::decode("bf48235e809b5c42e995f2b7d5fa13620e7ed834e337f6aa43df161e49e9323e").unwrap())
+                .add_recipient(
+                    CoseRecipientBuilder::new()
+                        .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ECDH_ES_A128KW).build())
+                        .unprotected(
+                            HeaderBuilder::new()
+                                .value(iana::HeaderAlgorithmParameter::EphemeralKey as i64,
+                                       CoseKeyBuilder::new_ec2_pub_key_y_sign(iana::EllipticCurve::P_521,
+                                                                              hex::decode("0043b12669acac3fd27898ffba0bcd2e6c366d53bc4db71f909a759304acfb5e18cdc7ba0b13ff8c7636271a6924b1ac63c02688075b55ef2d613574e7dc242f79c3").unwrap(),
+                                                                              true)
+                                       .build().to_cbor_value().unwrap())
+                                .key_id(b"bilbo.baggins@hobbiton.example".to_vec())
+                                .build(),
+                        )
+                        .ciphertext(hex::decode("339bc4f79984cdc6b3e6ce5f315a4c7d2b0ac466fcea69e8c07dfbca5bb1f661bc5f8e0df9e3eff5").unwrap())
+                        .build(),
+                )
+                .add_recipient(
+                    CoseRecipientBuilder::new()
+                        .unprotected(
+                            HeaderBuilder::new()
+                                .algorithm(iana::Algorithm::A256KW)
+                                .key_id(b"018c0ae5-4d9b-471b-bfd6-eef314bc7037".to_vec())
+                                .build(),
+                        )
+                        .ciphertext(hex::decode("0b2c7cfce04e98276342d6476a7723c090dfdd15f9a518e7736549e998370695e6d6a83b4ae507bb").unwrap())
+                        .build(),
+                )
+                .build(),
+            // Note: contents of maps have been re-ordered from the RFC to canonical ordering.
+            concat!(
+                "d861",
+                "85",
+                "43", "a10105",
+                "a0",
+                "54", "546869732069732074686520636f6e74656e742e",
+                "5820", "bf48235e809b5c42e995f2b7d5fa13620e7ed834e337f6aa43df161e49e9323e",
+                "82",
+                "83",
+                "44", "a101381c",
+                "a2",
+                "04",
+                "581e", "62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+                "20",
+                "a4",
+                "01",
+                "02",
+                "20",
+                "03",
+                "21",
+                "5842", "0043b12669acac3fd27898ffba0bcd2e6c366d53bc4db71f909a759304acfb5e18cdc7ba0b13ff8c7636271a6924b1ac63c02688075b55ef2d613574e7dc242f79c3",
+                "22",
+                "f5",
+                "5828", "339bc4f79984cdc6b3e6ce5f315a4c7d2b0ac466fcea69e8c07dfbca5bb1f661bc5f8e0df9e3eff5",
+                "83",
+                "40",
+                "a2",
+                "01",
+                "24",
+                "04",
+                "5824", "30313863306165352d346439622d343731622d626664362d656566333134626337303337",
+                "5828", "0b2c7cfce04e98276342d6476a7723c090dfdd15f9a518e7736549e998370695e6d6a83b4ae507bb",
+            ),
+        ),
+    ];
+
+    for (i, (mac, mac_data)) in tests.iter().enumerate() {
+        let got = mac.clone().to_tagged_vec().unwrap();
+        assert_eq!(*mac_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseMac::from_tagged_slice(&got).unwrap();
+        got.protected.original_data = None;
+        for mut recip in &mut got.recipients {
+            recip.protected.original_data = None;
+        }
+        for mut sig in &mut got.unprotected.counter_signatures {
+            sig.protected.original_data = None;
+        }
+        assert_eq!(*mac, got);
+    }
+}
+
+#[test]
+fn test_cose_mac0_decode() {
+    let tests: Vec<(CoseMac0, &'static str)> = vec![
+        (
+            CoseMac0Builder::new().build(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "f6", // null
+                "40", // 0-bstr
+            ),
+        ),
+        (
+            CoseMac0Builder::new().payload(vec![]).build(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "40", // 0-bstr
+            ),
+        ),
+    ];
+    for (i, (mac, mac_data)) in tests.iter().enumerate() {
+        let got = mac.clone().to_vec().unwrap();
+        assert_eq!(*mac_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseMac0::from_slice(&got).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*mac, got);
+    }
+}
+#[test]
+fn test_cose_mac0_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a2",   // 2-map (should be tuple)
+                "40",   // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0",   // 0-map
+                "4100", // 0-bstr
+                "40",   // 0-bstr
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "83", // 3-tuple (should be 4-tuple)
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+            ),
+            "expected array with 4 items",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "80", // 0-tuple (should be bstr)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "40", // 0-bstr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40", // 0-bstr (should be map)
+                "40", // 0-bstr
+                "40", // 0-bstr
+            ),
+            "expected map",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "60", // 0-tstr (should be bstr)
+                "40", // 0-bstr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "60", // 0-tstr
+            ),
+            "expected bstr",
+        ),
+    ];
+    for (mac_data, err_msg) in tests.iter() {
+        let data = hex::decode(mac_data).unwrap();
+        let result = CoseMac0::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_rfc8152_cose_mac0_decode() {
+    // COSE_Mac0 structures from RFC 8152 section C.5.
+    let tests: Vec<(CoseMac0, &'static str)> = vec![(
+        CoseMac0Builder::new()
+            .protected(
+                HeaderBuilder::new()
+                    .algorithm(iana::Algorithm::AES_MAC_256_64)
+                    .build(),
+            )
+            .payload(b"This is the content.".to_vec())
+            .tag(hex::decode("726043745027214f").unwrap())
+            .build(),
+        concat!(
+            "d1",
+            "84",
+            "43",
+            "a1010f",
+            "a0",
+            "54",
+            "546869732069732074686520636f6e74656e742e",
+            "48",
+            "726043745027214f",
+        ),
+    )];
+
+    for (i, (mac, mac_data)) in tests.iter().enumerate() {
+        let got = mac.clone().to_tagged_vec().unwrap();
+        assert_eq!(*mac_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseMac0::from_tagged_slice(&got).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*mac, got);
+    }
+}
+
+struct FakeMac {}
+impl FakeMac {
+    fn compute(&self, data: &[u8]) -> Vec<u8> {
+        let mut val = 0u8;
+        for b in data {
+            val ^= b;
+        }
+        vec![val]
+    }
+    fn verify(&self, tag: &[u8], data: &[u8]) -> Result<(), String> {
+        if self.compute(data) == tag {
+            Ok(())
+        } else {
+            Err("mismatch".to_owned())
+        }
+    }
+    fn try_compute(&self, data: &[u8]) -> Result<Vec<u8>, String> {
+        Ok(self.compute(data))
+    }
+    fn fail_compute(&self, _data: &[u8]) -> Result<Vec<u8>, String> {
+        Err("failed".to_string())
+    }
+}
+
+#[test]
+fn test_cose_mac_roundtrip() {
+    let tagger = FakeMac {};
+    let external_aad = b"This is the external aad";
+    let mut mac = CoseMacBuilder::new()
+        .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+        .payload(b"This is the data".to_vec())
+        .create_tag(external_aad, |data| tagger.compute(data))
+        .build();
+
+    assert!(mac
+        .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+        .is_ok());
+
+    // Changing an unprotected header leaves a correct tag.
+    mac.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+    assert!(mac
+        .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+        .is_ok());
+
+    // Providing a different `aad` means the tag won't validate
+    assert!(mac
+        .verify_tag(b"not aad", |tag, data| tagger.verify(tag, data))
+        .is_err());
+
+    // Changing a protected header invalidates the tag.
+    mac.protected = ProtectedHeader::default();
+    assert!(mac
+        .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+        .is_err());
+}
+
+#[test]
+fn test_cose_mac_noncanonical() {
+    let tagger = FakeMac {};
+    let external_aad = b"aad";
+
+    // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+    let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+    assert_eq!(protected.header, Header::default());
+    assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+    let mut mac = CoseMac {
+        protected: protected.clone(),
+        payload: Some(b"data".to_vec()),
+        ..Default::default()
+    };
+    let tbm = mac.tbm(external_aad);
+    mac.tag = tagger.compute(&tbm);
+
+    // Checking the MAC should still succeed, because the `ProtectedHeader`
+    // includes the wire data and uses it for building the input.
+    assert!(mac
+        .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+        .is_ok());
+
+    // However, if we attempt to build the same decryption inputs by hand (thus not including the
+    // non-canonical wire data)...
+    let recreated_mac = CoseMacBuilder::new()
+        .protected(protected.header)
+        .payload(b"data".to_vec())
+        .tag(mac.tag)
+        .build();
+
+    // ...then the transplanted tag will not verify, because the re-building of the
+    // inputs will use the canonical encoding of the protected header, which is not what was
+    // originally used for the input.
+    assert!(recreated_mac
+        .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+        .is_err());
+}
+
+#[test]
+fn test_cose_mac_tag_result() {
+    let tagger = FakeMac {};
+    let external_aad = b"This is the external aad";
+    let mut _mac = CoseMacBuilder::new()
+        .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+        .payload(b"This is the data".to_vec())
+        .try_create_tag(external_aad, |data| tagger.try_compute(data))
+        .unwrap()
+        .build();
+
+    // Cope with MAC creation failure.
+    let result = CoseMacBuilder::new()
+        .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+        .payload(b"This is the data".to_vec())
+        .try_create_tag(external_aad, |data| tagger.fail_compute(data));
+    expect_err(result, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_cose_mac_create_tag_no_payload() {
+    let tagger = FakeMac {};
+    let external_aad = b"This is the external aad";
+    let _mac = CoseMacBuilder::new()
+        .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+        // Creating a tag before a payload has been set will panic.
+        .create_tag(external_aad, |data| tagger.compute(data))
+        .build();
+}
+
+#[test]
+#[should_panic]
+fn test_cose_mac_verify_tag_no_payload() {
+    let tagger = FakeMac {};
+    let external_aad = b"This is the external aad";
+    let mut mac = CoseMacBuilder::new()
+        .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+        .payload(b"This is the data".to_vec())
+        .create_tag(external_aad, |data| tagger.compute(data))
+        .build();
+
+    mac.payload = None;
+    // Trying to verify with no payload available panics.
+    let _result = mac.verify_tag(external_aad, |tag, data| tagger.verify(tag, data));
+}
+
+#[test]
+fn test_cose_mac0_roundtrip() {
+    let tagger = FakeMac {};
+    let external_aad = b"This is the external aad";
+    let mut mac = CoseMac0Builder::new()
+        .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+        .payload(b"This is the data".to_vec())
+        .create_tag(external_aad, |data| tagger.compute(data))
+        .build();
+
+    assert!(mac
+        .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+        .is_ok());
+
+    // Changing an unprotected header leaves a correct tag.
+    mac.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+    assert!(mac
+        .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+        .is_ok());
+
+    // Providing a different `aad` means the tag won't validate
+    assert!(mac
+        .verify_tag(b"not aad", |tag, data| tagger.verify(tag, data))
+        .is_err());
+
+    // Changing a protected header invalidates the tag.
+    mac.protected = ProtectedHeader::default();
+    assert!(mac
+        .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+        .is_err());
+}
+
+#[test]
+fn test_cose_mac0_noncanonical() {
+    let tagger = FakeMac {};
+    let external_aad = b"aad";
+
+    // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+    let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+    assert_eq!(protected.header, Header::default());
+    assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+    let mut mac = CoseMac0 {
+        protected: protected.clone(),
+        payload: Some(b"data".to_vec()),
+        ..Default::default()
+    };
+    let tbm = mac.tbm(external_aad);
+    mac.tag = tagger.compute(&tbm);
+
+    // Checking the MAC should still succeed, because the `ProtectedHeader`
+    // includes the wire data and uses it for building the input.
+    assert!(mac
+        .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+        .is_ok());
+
+    // However, if we attempt to build the same decryption inputs by hand (thus not including the
+    // non-canonical wire data)...
+    let recreated_mac = CoseMac0Builder::new()
+        .protected(protected.header)
+        .payload(b"data".to_vec())
+        .tag(mac.tag)
+        .build();
+
+    // ...then the transplanted tag will not verify, because the re-building of the
+    // inputs will use the canonical encoding of the protected header, which is not what was
+    // originally used for the input.
+    assert!(recreated_mac
+        .verify_tag(external_aad, |tag, data| tagger.verify(tag, data))
+        .is_err());
+}
+
+#[test]
+fn test_cose_mac0_tag_result() {
+    let tagger = FakeMac {};
+    let external_aad = b"This is the external aad";
+    let mut _mac = CoseMac0Builder::new()
+        .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+        .payload(b"This is the data".to_vec())
+        .try_create_tag(external_aad, |data| tagger.try_compute(data))
+        .unwrap()
+        .build();
+
+    // Cope with MAC creation failure.
+    let result = CoseMac0Builder::new()
+        .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+        .payload(b"This is the data".to_vec())
+        .try_create_tag(external_aad, |data| tagger.fail_compute(data));
+    expect_err(result, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_cose_mac0_create_tag_no_payload() {
+    let tagger = FakeMac {};
+    let external_aad = b"This is the external aad";
+    let _mac = CoseMac0Builder::new()
+        .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+        // Creating a tag before a payload has been set will panic.
+        .create_tag(external_aad, |data| tagger.compute(data))
+        .build();
+}
+
+#[test]
+#[should_panic]
+fn test_cose_mac0_verify_tag_no_payload() {
+    let tagger = FakeMac {};
+    let external_aad = b"This is the external aad";
+    let mut mac = CoseMac0Builder::new()
+        .protected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+        .payload(b"This is the data".to_vec())
+        .create_tag(external_aad, |data| tagger.compute(data))
+        .build();
+
+    mac.payload = None;
+    // Trying to verify with no payload available panics.
+    let _result = mac.verify_tag(external_aad, |tag, data| tagger.verify(tag, data));
+}
diff --git a/src/sign/mod.rs b/src/sign/mod.rs
new file mode 100644
index 0000000..d945244
--- /dev/null
+++ b/src/sign/mod.rs
@@ -0,0 +1,380 @@
+// 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_Sign* functionality.
+
+use crate::{
+    cbor,
+    cbor::value::Value,
+    iana,
+    util::{cbor_type_error, to_cbor_array, AsCborValue, ValueTryAs},
+    CoseError, Header, ProtectedHeader, Result,
+};
+use alloc::{borrow::ToOwned, vec, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Structure representing a cryptographic signature.
+///
+/// ```cddl
+///  COSE_Signature =  [
+///       Headers,
+///       signature : bstr
+///  ]
+///  ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseSignature {
+    pub protected: ProtectedHeader,
+    pub unprotected: Header,
+    pub signature: Vec<u8>,
+}
+
+impl crate::CborSerializable for CoseSignature {}
+
+impl AsCborValue for CoseSignature {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() != 3 {
+            return Err(CoseError::UnexpectedItem("array", "array with 3 items"));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        Ok(Self {
+            signature: a.remove(2).try_as_bytes()?,
+            unprotected: Header::from_cbor_value(a.remove(1))?,
+            protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(Value::Array(vec![
+            self.protected.cbor_bstr()?,
+            self.unprotected.to_cbor_value()?,
+            Value::Bytes(self.signature),
+        ]))
+    }
+}
+
+/// Builder for [`CoseSignature`] objects.
+#[derive(Debug, Default)]
+pub struct CoseSignatureBuilder(CoseSignature);
+
+impl CoseSignatureBuilder {
+    builder! {CoseSignature}
+    builder_set_protected! {protected}
+    builder_set! {unprotected: Header}
+    builder_set! {signature: Vec<u8>}
+}
+
+/// Signed payload with signatures.
+///
+/// ```cdl
+///   COSE_Sign = [
+///       Headers,
+///       payload : bstr / nil,
+///       signatures : [+ COSE_Signature]
+///   ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseSign {
+    pub protected: ProtectedHeader,
+    pub unprotected: Header,
+    pub payload: Option<Vec<u8>>,
+    pub signatures: Vec<CoseSignature>,
+}
+
+impl crate::CborSerializable for CoseSign {}
+impl crate::TaggedCborSerializable for CoseSign {
+    const TAG: u64 = iana::CborTag::CoseSign as u64;
+}
+
+impl AsCborValue for CoseSign {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() != 4 {
+            return Err(CoseError::UnexpectedItem("array", "array with 4 items"));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        let signatures = a.remove(3).try_as_array_then_convert(|v| {
+            CoseSignature::from_cbor_value(v)
+                .map_err(|_e| CoseError::UnexpectedItem("non-signature", "map for COSE_Signature"))
+        })?;
+
+        Ok(Self {
+            signatures,
+            payload: match a.remove(2) {
+                Value::Bytes(b) => Some(b),
+                Value::Null => None,
+                v => return cbor_type_error(&v, "bstr or nil"),
+            },
+            unprotected: Header::from_cbor_value(a.remove(1))?,
+            protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(Value::Array(vec![
+            self.protected.cbor_bstr()?,
+            self.unprotected.to_cbor_value()?,
+            match self.payload {
+                Some(b) => Value::Bytes(b),
+                None => Value::Null,
+            },
+            to_cbor_array(self.signatures)?,
+        ]))
+    }
+}
+
+impl CoseSign {
+    /// Verify the indidated signature value, using `verifier` on the signature value and serialized
+    /// data (in that order).
+    ///
+    /// # Panics
+    ///
+    /// This method will panic if `which` is >= `self.signatures.len()`.
+    pub fn verify_signature<F, E>(&self, which: usize, aad: &[u8], verifier: F) -> Result<(), E>
+    where
+        F: FnOnce(&[u8], &[u8]) -> Result<(), E>,
+    {
+        let sig = &self.signatures[which];
+        let tbs_data = self.tbs_data(aad, sig);
+        verifier(&sig.signature, &tbs_data)
+    }
+
+    /// Construct the to-be-signed data for this object.
+    fn tbs_data(&self, aad: &[u8], sig: &CoseSignature) -> Vec<u8> {
+        sig_structure_data(
+            SignatureContext::CoseSignature,
+            self.protected.clone(),
+            Some(sig.protected.clone()),
+            aad,
+            self.payload.as_ref().unwrap_or(&vec![]),
+        )
+    }
+}
+
+/// Builder for [`CoseSign`] objects.
+#[derive(Debug, Default)]
+pub struct CoseSignBuilder(CoseSign);
+
+impl CoseSignBuilder {
+    builder! {CoseSign}
+    builder_set_protected! {protected}
+    builder_set! {unprotected: Header}
+    builder_set_optional! {payload: Vec<u8>}
+
+    /// Add a signature value.
+    #[must_use]
+    pub fn add_signature(mut self, sig: CoseSignature) -> Self {
+        self.0.signatures.push(sig);
+        self
+    }
+
+    /// Calculate the signature value, using `signer` to generate the signature bytes that will be
+    /// used to complete `sig`.  Any protected header values should be set before using this
+    /// method.
+    #[must_use]
+    pub fn add_created_signature<F>(self, mut sig: CoseSignature, aad: &[u8], signer: F) -> Self
+    where
+        F: FnOnce(&[u8]) -> Vec<u8>,
+    {
+        let tbs_data = self.0.tbs_data(aad, &sig);
+        sig.signature = signer(&tbs_data);
+        self.add_signature(sig)
+    }
+
+    /// Calculate the signature value, using `signer` to generate the signature bytes that will be
+    /// used to complete `sig`.  Any protected header values should be set before using this
+    /// method.
+    pub fn try_add_created_signature<F, E>(
+        self,
+        mut sig: CoseSignature,
+        aad: &[u8],
+        signer: F,
+    ) -> Result<Self, E>
+    where
+        F: FnOnce(&[u8]) -> Result<Vec<u8>, E>,
+    {
+        let tbs_data = self.0.tbs_data(aad, &sig);
+        sig.signature = signer(&tbs_data)?;
+        Ok(self.add_signature(sig))
+    }
+}
+
+/// Signed payload with a single signature.
+///
+/// ```cddl
+///   COSE_Sign1 = [
+///       Headers,
+///       payload : bstr / nil,
+///       signature : bstr
+///   ]
+/// ```
+#[derive(Clone, Debug, Default, PartialEq)]
+pub struct CoseSign1 {
+    pub protected: ProtectedHeader,
+    pub unprotected: Header,
+    pub payload: Option<Vec<u8>>,
+    pub signature: Vec<u8>,
+}
+
+impl crate::CborSerializable for CoseSign1 {}
+impl crate::TaggedCborSerializable for CoseSign1 {
+    const TAG: u64 = iana::CborTag::CoseSign1 as u64;
+}
+
+impl AsCborValue for CoseSign1 {
+    fn from_cbor_value(value: Value) -> Result<Self> {
+        let mut a = value.try_as_array()?;
+        if a.len() != 4 {
+            return Err(CoseError::UnexpectedItem("array", "array with 4 items"));
+        }
+
+        // Remove array elements in reverse order to avoid shifts.
+        Ok(Self {
+            signature: a.remove(3).try_as_bytes()?,
+            payload: match a.remove(2) {
+                Value::Bytes(b) => Some(b),
+                Value::Null => None,
+                v => return cbor_type_error(&v, "bstr or nil"),
+            },
+            unprotected: Header::from_cbor_value(a.remove(1))?,
+            protected: ProtectedHeader::from_cbor_bstr(a.remove(0))?,
+        })
+    }
+
+    fn to_cbor_value(self) -> Result<Value> {
+        Ok(Value::Array(vec![
+            self.protected.cbor_bstr()?,
+            self.unprotected.to_cbor_value()?,
+            match self.payload {
+                Some(b) => Value::Bytes(b),
+                None => Value::Null,
+            },
+            Value::Bytes(self.signature),
+        ]))
+    }
+}
+
+impl CoseSign1 {
+    /// Verify the signature value, using `verifier` on the signature value and serialized data (in
+    /// that order).
+    pub fn verify_signature<F, E>(&self, aad: &[u8], verifier: F) -> Result<(), E>
+    where
+        F: FnOnce(&[u8], &[u8]) -> Result<(), E>,
+    {
+        let tbs_data = self.tbs_data(aad);
+        verifier(&self.signature, &tbs_data)
+    }
+
+    /// Construct the to-be-signed data for this object.
+    fn tbs_data(&self, aad: &[u8]) -> Vec<u8> {
+        sig_structure_data(
+            SignatureContext::CoseSign1,
+            self.protected.clone(),
+            None,
+            aad,
+            self.payload.as_ref().unwrap_or(&vec![]),
+        )
+    }
+}
+
+/// Builder for [`CoseSign1`] objects.
+#[derive(Debug, Default)]
+pub struct CoseSign1Builder(CoseSign1);
+
+impl CoseSign1Builder {
+    builder! {CoseSign1}
+    builder_set_protected! {protected}
+    builder_set! {unprotected: Header}
+    builder_set! {signature: Vec<u8>}
+    builder_set_optional! {payload: Vec<u8>}
+
+    /// Calculate the signature value, using `signer` to generate the signature bytes.  Any
+    /// protected header values should be set before using this method.
+    #[must_use]
+    pub fn create_signature<F>(self, aad: &[u8], signer: F) -> Self
+    where
+        F: FnOnce(&[u8]) -> Vec<u8>,
+    {
+        let sig_data = signer(&self.0.tbs_data(aad));
+        self.signature(sig_data)
+    }
+
+    /// Calculate the signature value, using `signer` to generate the signature bytes.  Any
+    /// protected header values should be set before using this method.
+    pub fn try_create_signature<F, E>(self, aad: &[u8], signer: F) -> Result<Self, E>
+    where
+        F: FnOnce(&[u8]) -> Result<Vec<u8>, E>,
+    {
+        let sig_data = signer(&self.0.tbs_data(aad))?;
+        Ok(self.signature(sig_data))
+    }
+}
+
+/// Possible signature contexts.
+#[derive(Clone, Copy)]
+pub enum SignatureContext {
+    CoseSignature,
+    CoseSign1,
+    CounterSignature,
+}
+
+impl SignatureContext {
+    /// Return the context string as per RFC 8152 section 4.4.
+    fn text(&self) -> &'static str {
+        match self {
+            SignatureContext::CoseSignature => "Signature",
+            SignatureContext::CoseSign1 => "Signature1",
+            SignatureContext::CounterSignature => "CounterSignature",
+        }
+    }
+}
+
+/// Create a binary blob that will be signed.
+///
+/// ```cddl
+///   Sig_structure = [
+///       context : "Signature" / "Signature1" / "CounterSignature",
+///       body_protected : empty_or_serialized_map,
+///       ? sign_protected : empty_or_serialized_map,
+///       external_aad : bstr,
+///       payload : bstr
+///   ]
+/// ```
+pub fn sig_structure_data(
+    context: SignatureContext,
+    body: ProtectedHeader,
+    sign: Option<ProtectedHeader>,
+    aad: &[u8],
+    payload: &[u8],
+) -> Vec<u8> {
+    let mut arr = vec![
+        Value::Text(context.text().to_owned()),
+        body.cbor_bstr().expect("failed to serialize header"), // safe: always serializable
+    ];
+    if let Some(sign) = sign {
+        arr.push(sign.cbor_bstr().expect("failed to serialize header")); // safe: always
+                                                                         // serializable
+    }
+    arr.push(Value::Bytes(aad.to_vec()));
+    arr.push(Value::Bytes(payload.to_vec()));
+    let mut data = Vec::new();
+    cbor::ser::into_writer(&Value::Array(arr), &mut data).unwrap(); // safe: always serializable
+    data
+}
diff --git a/src/sign/tests.rs b/src/sign/tests.rs
new file mode 100644
index 0000000..d802b90
--- /dev/null
+++ b/src/sign/tests.rs
@@ -0,0 +1,1427 @@
+// 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::{
+    cbor::value::Value, iana, util::expect_err, Algorithm, CborSerializable, ContentType,
+    HeaderBuilder, RegisteredLabel, TaggedCborSerializable,
+};
+use alloc::{
+    format,
+    string::{String, ToString},
+    vec,
+};
+
+#[test]
+fn test_cose_signature_encode() {
+    let tests = vec![
+        (
+            CoseSignature::default(),
+            concat!(
+                "83", // 3-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+            ),
+        ),
+        (
+            CoseSignature {
+                signature: vec![1, 2, 3],
+                ..Default::default()
+            },
+            concat!(
+                "83",       // 3-tuple
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+            ),
+        ),
+        (
+            CoseSignature {
+                protected: ProtectedHeader {
+                    original_data: None,
+                    header: Header {
+                        alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+                        key_id: vec![1, 2, 3],
+                        partial_iv: vec![1, 2, 3],
+                        ..Default::default()
+                    },
+                },
+                signature: vec![1, 2, 3],
+                ..Default::default()
+            },
+            concat!(
+                "83", // 3-tuple
+                "4d", // 13-bstr
+                "a3", // 3-map
+                "01", "01", // 1 (alg) => A128GCM
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "06", "43", "010203",   // 6 (partial-iv) => 3-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+            ),
+        ),
+        (
+            CoseSignature {
+                unprotected: Header {
+                    alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+                    key_id: vec![1, 2, 3],
+                    partial_iv: vec![1, 2, 3],
+                    ..Default::default()
+                },
+                signature: vec![1, 2, 3],
+                ..Default::default()
+            },
+            concat!(
+                "83", // 3-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a3", // 3-map
+                "01", "01", // 1 (alg) => A128GCM
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "06", "43", "010203",   // 6 (partial-iv) => 3-bstr
+                "43010203", // 3-bstr
+            ),
+        ),
+    ];
+    for (i, (sig, sig_data)) in tests.iter().enumerate() {
+        let got = sig.clone().to_vec().unwrap();
+        assert_eq!(*sig_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseSignature::from_slice(&got).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*sig, got);
+    }
+}
+
+#[test]
+fn test_cose_signature_decode_noncanonical() {
+    // RFC8152 section 3: "Recipients MUST accept both a zero-length binary value and a zero-length
+    // map encoded in the binary value."
+    let sig_data = hex::decode(concat!(
+        "83",   // 3-tuple
+        "41a0", // 1-bstr holding 0-map (not a 0-bstr)
+        "a0",   // 0-map
+        "40",   // 0-bstr
+    ))
+    .unwrap();
+    let sig = CoseSignature::default();
+    let mut got = CoseSignature::from_slice(&sig_data).unwrap();
+    got.protected.original_data = None;
+    assert_eq!(sig, got);
+}
+
+#[test]
+fn test_cose_signature_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a2",       // 2-map
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "40",       // 0-bstr
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "83",       // 3-tuple
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40",       // 0-bstr (invalid: should be map)
+                "43010203", // 3-bstr
+            ),
+            "expected map",
+        ),
+        (
+            concat!(
+                "83",       // 3-tuple
+                "a0",       // 0-map (invalid: should be bstr)
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84",       // 4-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "43010203", // 3-bstr
+            ),
+            "expected array with 3 items",
+        ),
+        (
+            concat!(
+                "82", // 4-tuple
+                "40", // 0-bstr
+                "a0", // 0-map
+            ),
+            "expected array with 3 items",
+        ),
+        (
+            concat!(
+                "83",       // 3-tuple
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40",       // 0-bstr (invalid: should be map)
+                "63616263", // 3-tstr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "83", // 3-tuple
+                "45", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a1", // 1-map
+                "03", "81", "4101",     // 3 (content-type) => [bstr] (invalid value type)
+                "a0",       // 0-map
+                "43616263", // 0-bstr
+            ),
+            "expected int/tstr",
+        ),
+    ];
+    for (sig_data, err_msg) in tests.iter() {
+        let data = hex::decode(sig_data).unwrap();
+        let result = CoseSignature::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_cose_signature_builder() {
+    let tests = vec![
+        (
+            CoseSignatureBuilder::new().build(),
+            CoseSignature::default(),
+        ),
+        (
+            CoseSignatureBuilder::new().signature(vec![1, 2, 3]).build(),
+            CoseSignature {
+                signature: vec![1, 2, 3],
+                ..Default::default()
+            },
+        ),
+        (
+            CoseSignatureBuilder::new()
+                .signature(vec![1, 2, 3])
+                .protected(
+                    HeaderBuilder::new()
+                        .algorithm(iana::Algorithm::A128GCM)
+                        .key_id(vec![1, 2, 3])
+                        .iv(vec![1, 2, 3])
+                        .build(),
+                )
+                .build(),
+            CoseSignature {
+                protected: ProtectedHeader {
+                    original_data: None,
+                    header: Header {
+                        alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+                        key_id: vec![1, 2, 3],
+                        iv: vec![1, 2, 3],
+                        ..Default::default()
+                    },
+                },
+                signature: vec![1, 2, 3],
+                ..Default::default()
+            },
+        ),
+        (
+            CoseSignatureBuilder::new()
+                .signature(vec![1, 2, 3])
+                .unprotected(
+                    HeaderBuilder::new()
+                        .algorithm(iana::Algorithm::A128GCM)
+                        .key_id(vec![1, 2, 3])
+                        .partial_iv(vec![1, 2, 3])
+                        .build(),
+                )
+                .build(),
+            CoseSignature {
+                unprotected: Header {
+                    alg: Some(Algorithm::Assigned(iana::Algorithm::A128GCM)),
+                    key_id: vec![1, 2, 3],
+                    partial_iv: vec![1, 2, 3],
+                    ..Default::default()
+                },
+                signature: vec![1, 2, 3],
+                ..Default::default()
+            },
+        ),
+    ];
+    for (got, want) in tests {
+        assert_eq!(got, want);
+    }
+}
+
+#[test]
+fn test_cose_sign_encode() {
+    let tests = vec![
+        (
+            CoseSign::default(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "f6", // null
+                "80", // 0-tuple
+            ),
+        ),
+        (
+            CoseSignBuilder::new()
+                .add_signature(CoseSignature::default())
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "f6", // null
+                "81", // 1-tuple
+                "83", "40a040", // 3-tuple
+            ),
+        ),
+        (
+            CoseSignBuilder::new()
+                .protected(
+                    HeaderBuilder::new()
+                        .algorithm(iana::Algorithm::A128GCM)
+                        .key_id(vec![1, 2, 3])
+                        .build(),
+                )
+                .payload(vec![4, 5, 6])
+                .add_signature(
+                    CoseSignatureBuilder::new()
+                        .signature(vec![1, 2, 3])
+                        .protected(
+                            HeaderBuilder::new()
+                                .algorithm(iana::Algorithm::A128GCM)
+                                .key_id(vec![1, 2, 3])
+                                .iv(vec![1, 2, 3])
+                                .build(),
+                        )
+                        .build(),
+                )
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "48", // 8-bstr (protected)
+                "a2", // 2-map
+                "01", "01", // 1 (alg) => A128GCM
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "a0",     // 0-map (unprotected)
+                "43", "040506", // 3-bstr (payload)
+                "81",     // 1-tuple (signatures)
+                "83",     // 3-tuple (COSE_Signature)
+                "4d",     // 14-bstr (protected)
+                "a3",     // 3-map
+                "01", "01", // 1 (alg) => A128GCM
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "05", "43", "010203", // 5 (iv) => 3-bstr
+                "a0",     // 0-map (unprotected)
+                "43", "010203", // 0-bstr (signature)
+            ),
+        ),
+        (
+            CoseSignBuilder::new()
+                .unprotected(
+                    HeaderBuilder::new()
+                        .algorithm(iana::Algorithm::A128GCM)
+                        .key_id(vec![1, 2, 3])
+                        .build(),
+                )
+                .payload(vec![4, 5, 6])
+                .add_signature(
+                    CoseSignatureBuilder::new()
+                        .signature(vec![1, 2, 3])
+                        .protected(
+                            HeaderBuilder::new()
+                                .algorithm(iana::Algorithm::A128GCM)
+                                .key_id(vec![1, 2, 3])
+                                .iv(vec![1, 2, 3])
+                                .build(),
+                        )
+                        .build(),
+                )
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (protected)
+                "a2", // 2-map (unprotected)
+                "01", "01", // 1 (alg) => A128GCM
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "43", "040506", // 3-bstr (payload)
+                "81",     // 1-tuple (signatures)
+                "83",     // 3-tuple (COSE_Signature)
+                "4d",     // 14-bstr (protected)
+                "a3",     // 3-map
+                "01", "01", // 1 (alg) => A128GCM
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "05", "43", "010203", // 5 (iv) => 3-bstr
+                "a0",     // 0-map (unprotected)
+                "43", "010203", // 0-bstr (signature)
+            ),
+        ),
+    ];
+    for (i, (sign, sign_data)) in tests.iter().enumerate() {
+        let got = sign.clone().to_vec().unwrap();
+        assert_eq!(*sign_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseSign::from_slice(&got).unwrap();
+        got.protected.original_data = None;
+        for mut sig in &mut got.signatures {
+            sig.protected.original_data = None;
+        }
+        assert_eq!(*sign, got);
+
+        // Repeat with tagged variant.
+        let got = sign.clone().to_tagged_vec().unwrap();
+        let tagged_sign_data = format!("d862{}", sign_data);
+        assert_eq!(tagged_sign_data, hex::encode(&got), "tagged case {}", i);
+
+        let mut got = CoseSign::from_tagged_slice(&got).unwrap();
+        got.protected.original_data = None;
+        for mut sig in &mut got.signatures {
+            sig.protected.original_data = None;
+        }
+        assert_eq!(*sign, got);
+    }
+}
+
+#[test]
+fn test_cose_sign_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a2",       // 2-map
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40",       // 0-bstr (invalid: should be map)
+                "43010203", // 3-bstr
+                "80",       // 0-tuple
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "84",       // 4-tuple
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40",       // 0-bstr (invalid: should be map)
+                "43010203", // 3-bstr
+                "80",       // 0-tuple
+            ),
+            "expected map",
+        ),
+        (
+            concat!(
+                "84",       // 4-tuple
+                "a0",       // 0-map (invalid: should be bstr)
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "80",       // 0-tuple
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "85",       // 5-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "80",       // 0-tuple
+                "43010203", // 3-bstr
+            ),
+            "expected array with 4 items",
+        ),
+        (
+            concat!(
+                "83",       // 3-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+            ),
+            "expected array with 4 items",
+        ),
+        (
+            concat!(
+                "84",       // 4-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "43010203", // 3-bstr
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "84",       // 4-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "63616263", // 3-tstr
+                "80",       // 0-tuple
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84",       // 4-tuple
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "81",       // 1-tuple
+                "83",       // 3-tuple
+                "a0",       // 0-map (invalid: should be bstr)
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+            ),
+            "expected map for COSE_Signature",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "45", // 5-bstr
+                "a1", // 1-map
+                "03", "81", "4101",     // 3 (content-type) => [bstr] (invalid value type)
+                "a0",       // 0-map
+                "43616263", // 3-bstr
+                "80",       // 0-tuple
+            ),
+            "expected int/tstr",
+        ),
+    ];
+    for (sign_data, err_msg) in tests.iter() {
+        let data = hex::decode(sign_data).unwrap();
+        let result = CoseSign::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_cose_sign_tagged_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "d862",     // tag(98)
+                "84",       // 4-tuple
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40",       // 0-bstr (invalid: should be map)
+                "43010203", // 3-bstr
+                "80",       // 0-tuple
+            ),
+            "expected map",
+        ),
+        (
+            concat!(
+                "d862",     // tag(98)
+                "84",       // 4-tuple
+                "a0",       // 0-map (invalid: should be bstr)
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "80",       // 0-tuple
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "d862",     // tag(98)
+                "85",       // 5-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "80",       // 0-tuple
+                "43010203", // 3-bstr
+            ),
+            "expected array with 4 items",
+        ),
+        (
+            concat!(
+                "d862",     // tag(98)
+                "83",       // 3-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+            ),
+            "expected array with 4 items",
+        ),
+        (
+            concat!(
+                "1862",     // int instead of tag
+            ),
+            "expected tag",
+        ),
+        (
+            concat!(
+                "d861",     // tag(97) : wrong tag
+                "84",       // 4-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "80",       // 0-tuple
+            ),
+            "expected other tag",
+        ),
+        (
+            concat!(
+                "1861",     // int (97) : not a tag
+                "84",       // 4-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "80",       // 0-tuple
+            ),
+            "extraneous data",
+        ),
+        (
+            concat!(
+                "18",     // incomplete int
+            ),
+            "decode CBOR failure: Io(EndOfFile",
+        ),
+    ];
+    for (sign_data, err_msg) in tests.iter() {
+        let data = hex::decode(sign_data).unwrap();
+        let result = CoseSign::from_tagged_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_rfc8152_cose_sign_decode() {
+    // COSE_Sign structures from RFC 8152 section C.1.
+    let tests = vec![
+        (
+            CoseSignBuilder::new()
+                .payload(b"This is the content.".to_vec())
+                .add_signature(
+                    CoseSignatureBuilder::new()
+                        .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+                        .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+                        .signature(hex::decode(
+                            "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a"
+                        ).unwrap())
+                        .build()
+                )
+                .build(),
+            concat!(
+                "d862",
+                "84",
+                "40",
+                "a0",
+                "54", "546869732069732074686520636f6e74656e742e",
+                "81", "83",
+                "43", "a10126",
+                "a1", "04", "42", "3131",
+                "5840", "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a"
+            ),
+        ),
+        (
+            CoseSignBuilder::new()
+                .payload(b"This is the content.".to_vec())
+                .add_signature(
+                    CoseSignatureBuilder::new()
+                        .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+                        .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+                        .signature(hex::decode(
+                            "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a"
+                        ).unwrap())
+                        .build()
+                )
+                .add_signature(
+                    CoseSignatureBuilder::new()
+                        .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES512).build())
+                        .unprotected(HeaderBuilder::new().key_id(b"bilbo.baggins@hobbiton.example".to_vec()).build())
+                        .signature(hex::decode(
+                            "00a2d28a7c2bdb1587877420f65adf7d0b9a06635dd1de64bb62974c863f0b160dd2163734034e6ac003b01e8705524c5c4ca479a952f0247ee8cb0b4fb7397ba08d009e0c8bf482270cc5771aa143966e5a469a09f613488030c5b07ec6d722e3835adb5b2d8c44e95ffb13877dd2582866883535de3bb03d01753f83ab87bb4f7a0297"
+                        ).unwrap())
+                        .build()
+                )
+                .build(),
+            concat!(
+                "d862",
+                "84",
+                "40",
+                "a0", "54", "546869732069732074686520636f6e74656e742e",
+                "82",
+                "83",
+                "43", "a10126",
+                "a1", "04", "42", "3131",
+                "5840", "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a",
+                "83",
+                "44", "a1013823",
+                "a1", "04", "581e", "62696c626f2e62616767696e7340686f626269746f6e2e6578616d706c65",
+                "5884", "00a2d28a7c2bdb1587877420f65adf7d0b9a06635dd1de64bb62974c863f0b160dd2163734034e6ac003b01e8705524c5c4ca479a952f0247ee8cb0b4fb7397ba08d009e0c8bf482270cc5771aa143966e5a469a09f613488030c5b07ec6d722e3835adb5b2d8c44e95ffb13877dd2582866883535de3bb03d01753f83ab87bb4f7a0297",
+            )
+        ),
+        (
+            CoseSignBuilder::new()
+                .unprotected(HeaderBuilder::new()
+                             .add_counter_signature(
+                                 CoseSignatureBuilder::new()
+                                     .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+                                     .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+                                     .signature(hex::decode(
+                                         "5ac05e289d5d0e1b0a7f048a5d2b643813ded50bc9e49220f4f7278f85f19d4a77d655c9d3b51e805a74b099e1e085aacd97fc29d72f887e8802bb6650cceb2c"
+                                     ).unwrap())
+                                     .build()
+                             )
+                             .build())
+                .payload(b"This is the content.".to_vec())
+                .add_signature(
+                    CoseSignatureBuilder::new()
+                        .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+                        .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+                        .signature(hex::decode(
+                            "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a"
+                        ).unwrap())
+                        .build()
+                )
+                .build(),
+            concat!(
+                "d862",
+                "84",
+                "40",
+                "a1", "07",
+                "83",
+                "43", "a10126",
+                "a1", "04", "42", "3131",
+                "5840", "5ac05e289d5d0e1b0a7f048a5d2b643813ded50bc9e49220f4f7278f85f19d4a77d655c9d3b51e805a74b099e1e085aacd97fc29d72f887e8802bb6650cceb2c",
+                "54", "546869732069732074686520636f6e74656e742e",
+                "81",
+                "83",
+                "43", "a10126",
+                "a1", "04", "42", "3131",
+                "5840", "e2aeafd40d69d19dfe6e52077c5d7ff4e408282cbefb5d06cbf414af2e19d982ac45ac98b8544c908b4507de1e90b717c3d34816fe926a2b98f53afd2fa0f30a",
+            ),
+        ),
+        (
+            CoseSignBuilder::new()
+                .protected(HeaderBuilder::new()
+                           .text_value("reserved".to_owned(), Value::Bool(false))
+                           .add_critical_label(RegisteredLabel::Text("reserved".to_owned()))
+                           .build())
+                .payload(b"This is the content.".to_vec())
+                .add_signature(
+                    CoseSignatureBuilder::new()
+                        .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+                        .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+                        .signature(hex::decode(
+                            "3fc54702aa56e1b2cb20284294c9106a63f91bac658d69351210a031d8fc7c5ff3e4be39445b1a3e83e1510d1aca2f2e8a7c081c7645042b18aba9d1fad1bd9c"
+                        ).unwrap())
+                        .build()
+                )
+                .build(),
+            // Note: contents of protected header changed from RFC to be put in canonical order.
+            concat!(
+                "d862",
+                "84",
+                "56",
+                "a2",
+                "02", "81687265736572766564",
+                "687265736572766564", "f4",
+                "a0",
+                "54", "546869732069732074686520636f6e74656e742e",
+                "81", "83",
+                "43", "a10126",
+                "a1", "04", "42", "3131",
+                "5840", "3fc54702aa56e1b2cb20284294c9106a63f91bac658d69351210a031d8fc7c5ff3e4be39445b1a3e83e1510d1aca2f2e8a7c081c7645042b18aba9d1fad1bd9c",
+            ),
+        ),
+    ];
+
+    for (i, (sign, sign_data)) in tests.iter().enumerate() {
+        let got = sign.clone().to_tagged_vec().unwrap();
+        assert_eq!(
+            *sign_data,
+            hex::encode(&got),
+            "case {}: encode {:?}",
+            i,
+            sign
+        );
+
+        let mut got = CoseSign::from_tagged_slice(&got).unwrap();
+        got.protected.original_data = None;
+        for mut sig in &mut got.signatures {
+            sig.protected.original_data = None;
+        }
+        for mut sig in &mut got.unprotected.counter_signatures {
+            sig.protected.original_data = None;
+        }
+        assert_eq!(*sign, got);
+    }
+}
+
+#[test]
+fn test_cose_sign1_encode() {
+    let tests = vec![
+        (
+            CoseSign1Builder::new().payload(vec![]).build(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "40", // 0-bstr
+                "40", // 0-bstr
+            ),
+        ),
+        (
+            CoseSign1Builder::new().signature(vec![1, 2, 3]).build(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0", // 0-map
+                "f6", // null
+                "43", "010203", // 3-bstr
+            ),
+        ),
+        (
+            CoseSign1Builder::new()
+                .protected(
+                    HeaderBuilder::new()
+                        .algorithm(iana::Algorithm::A128GCM)
+                        .key_id(vec![1, 2, 3])
+                        .build(),
+                )
+                .payload(vec![])
+                .signature(vec![1, 2, 3])
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "48", // 8-bstr (protected)
+                "a2", // 2-map
+                "01", "01", // 1 (alg) => A128GCM
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "a0",     // 0-map
+                "40",     // 0-bstr
+                "43", "010203", // 3-bstr
+            ),
+        ),
+        (
+            CoseSign1Builder::new()
+                .unprotected(
+                    HeaderBuilder::new()
+                        .algorithm(iana::Algorithm::A128GCM)
+                        .key_id(vec![1, 2, 3])
+                        .build(),
+                )
+                .payload(vec![])
+                .signature(vec![1, 2, 3])
+                .build(),
+            concat!(
+                "84", // 4-tuple
+                "40", // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a2", // 2-map
+                "01", "01", // 1 (alg) => A128GCM
+                "04", "43", "010203", // 4 (kid) => 3-bstr
+                "40",     // 0-bstr
+                "43", "010203", // 3-bstr
+            ),
+        ),
+    ];
+    for (i, (sign, sign_data)) in tests.iter().enumerate() {
+        let got = sign.clone().to_vec().unwrap();
+        assert_eq!(*sign_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseSign1::from_slice(&got).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*sign, got);
+
+        // Repeat with tagged variant.
+        let got = sign.clone().to_tagged_vec().unwrap();
+        let want_hex = format!("d2{}", sign_data);
+        assert_eq!(want_hex, hex::encode(&got), "tagged case {}", i);
+
+        let mut got = CoseSign1::from_tagged_slice(&got).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*sign, got);
+    }
+}
+
+#[test]
+fn test_cose_sign1_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "a2",       // 2-map
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "40",       // 0-bstr
+            ),
+            "expected array",
+        ),
+        (
+            concat!(
+                "84",       // 4-tuple
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40",       // 0-bstr (invalid: should be map)
+                "43010203", // 3-bstr
+                "40",       // 0-bstr
+            ),
+            "expected map",
+        ),
+        (
+            concat!(
+                "84",       // 4-tuple
+                "a0",       // 0-map (invalid: should be bstr)
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "40",       // 0-bstr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84",       // 4-tuple
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "a0",       // 0-map
+                "63616263", // 3-tstr
+                "40",       // 0-bstr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "84",       // 4-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "80",       // 0-arr (invalid: should be bstr)
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "85",       // 5-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "40",       // 0-bstr
+                "43010203", // 3-bstr
+            ),
+            "expected array with 4 items",
+        ),
+        (
+            concat!(
+                "83",       // 3-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+            ),
+            "expected array with 4 items",
+        ),
+        (
+            concat!(
+                "84", // 4-tuple
+                "45", // 5-bstr
+                "a1", // 1-map
+                "03", "81", "4101",     // 3 (content-type) => [bstr] (invalid value type)
+                "a0",       // 0-map
+                "43616263", // 3-bstr
+                "40",       // 0-bstr
+            ),
+            "expected int/tstr",
+        ),
+    ];
+    for (sign_data, err_msg) in tests.iter() {
+        let data = hex::decode(sign_data).unwrap();
+        let result = CoseSign1::from_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_cose_sign1_decode_noncanonical() {
+    let tests = vec![(
+        CoseSign1Builder::new()
+            .protected(
+                HeaderBuilder::new()
+                    .algorithm(iana::Algorithm::ES256)
+                    .key_id(vec![0x31, 0x31])
+                    .build(),
+            )
+            .payload(vec![0x61, 0x61])
+            .build(),
+        concat!(
+            "84", // 4-tuple
+            "47", // bstr len 7 (protected)
+            concat!(
+                "a2", // 2-map
+                // The contents of the bstr-encoded header are not in canonical order.
+                "04", "42", "3131", // 4 (kid) => 2-bstr "11"
+                "01", "26", // 1 (alg) => ES256
+            ),
+            "a0",   // 0-map (unprotected)
+            "42",   // 2-bstr (payload)
+            "6161", // "aa"
+            "40",   // 0-bstr
+        ),
+    )];
+    for (sign, sign_data) in tests.iter() {
+        let data = hex::decode(sign_data).unwrap();
+        let mut got = CoseSign1::from_slice(&data).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*sign, got);
+
+        // Repeat with tagged variant.
+        let mut tagged_data = vec![0xd2];
+        tagged_data.extend_from_slice(&data);
+        let mut got = CoseSign1::from_tagged_slice(&tagged_data).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*sign, got);
+    }
+}
+
+#[test]
+fn test_cose_sign1_tagged_decode_fail() {
+    let tests = vec![
+        (
+            concat!(
+                "d2",       // tag(18)
+                "84",       // 4-tuple
+                "40",       // 0-bstr (special case for empty protected headers, rather than 41a0)
+                "40",       // 0-bstr (invalid: should be map)
+                "43010203", // 3-bstr
+                "40",       // 0-bstr
+            ),
+            "expected map",
+        ),
+        (
+            concat!(
+                "d2",       // tag(18)
+                "84",       // 4-tuple
+                "a0",       // 0-map (invalid: should be bstr)
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "40",       // 0-bstr
+            ),
+            "expected bstr",
+        ),
+        (
+            concat!(
+                "d2",       // tag(18)
+                "85",       // 5-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "80",       // 0-tuple
+                "40",       // 0-bstr
+            ),
+            "expected array with 4 items",
+        ),
+        (
+            concat!(
+                "d2",       // tag(18)
+                "83",       // 3-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+            ),
+            "expected array with 4 items",
+        ),
+        (
+            concat!(
+                "d1",       // tag(17) : wrong tag
+                "84",       // 4-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "40",       // 0-bstr
+            ),
+            "expected other tag",
+        ),
+        (
+            concat!(
+                "12",       // int (18) : not a tag
+                "84",       // 4-tuple
+                "40",       // 0-bstr
+                "a0",       // 0-map
+                "43010203", // 3-bstr
+                "40",       // 0-bstr
+            ),
+            "extraneous data",
+        ),
+        (
+            concat!(
+                "12",     // incomplete int
+            ),
+            "expected tag",
+        ),
+    ];
+    for (sign_data, err_msg) in tests.iter() {
+        let data = hex::decode(sign_data).unwrap();
+        let result = CoseSign1::from_tagged_slice(&data);
+        expect_err(result, err_msg);
+    }
+}
+
+#[test]
+fn test_rfc8152_cose_sign1_decode() {
+    // COSE_Sign1 structures from RFC 8152 section C.2.
+    let tests = vec![
+        (
+            CoseSign1Builder::new()
+                .protected(HeaderBuilder::new().algorithm(iana::Algorithm::ES256).build())
+                .unprotected(HeaderBuilder::new().key_id(b"11".to_vec()).build())
+                .payload(b"This is the content.".to_vec())
+                .signature(hex::decode(
+                    "8eb33e4ca31d1c465ab05aac34cc6b23d58fef5c083106c4d25a91aef0b0117e2af9a291aa32e14ab834dc56ed2a223444547e01f11d3b0916e5a4c345cacb36",
+                ).unwrap())
+                .build(),
+            concat!(
+                "d2",
+                "84",
+                "43", "a10126",
+                "a1", "04", "42", "3131",
+                "54", "546869732069732074686520636f6e74656e742e",
+                "5840", "8eb33e4ca31d1c465ab05aac34cc6b23d58fef5c083106c4d25a91aef0b0117e2af9a291aa32e14ab834dc56ed2a223444547e01f11d3b0916e5a4c345cacb36",
+            ),
+        ),
+    ];
+
+    for (i, (sign, sign_data)) in tests.iter().enumerate() {
+        let got = sign.clone().to_tagged_vec().unwrap();
+        assert_eq!(*sign_data, hex::encode(&got), "case {}", i);
+
+        let mut got = CoseSign1::from_tagged_slice(&got).unwrap();
+        got.protected.original_data = None;
+        assert_eq!(*sign, got);
+    }
+}
+
+#[derive(Copy, Clone)]
+struct FakeSigner {}
+
+extern crate std;
+impl FakeSigner {
+    fn sign(&self, data: &[u8]) -> Vec<u8> {
+        data.to_vec()
+    }
+
+    fn verify(&self, sig: &[u8], data: &[u8]) -> Result<(), String> {
+        if sig != self.sign(data) {
+            Err("failed to verify".to_owned())
+        } else {
+            Ok(())
+        }
+    }
+    fn try_sign(&self, data: &[u8]) -> Result<Vec<u8>, String> {
+        Ok(self.sign(data))
+    }
+
+    fn fail_sign(&self, _data: &[u8]) -> Result<Vec<u8>, String> {
+        Err("failed".to_string())
+    }
+}
+
+#[test]
+fn test_sign_roundtrip() {
+    let signer = FakeSigner {};
+    let verifier = signer;
+
+    let pt = b"This is the content";
+    let aad = b"this is additional data";
+
+    let protected = HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let sign = CoseSignBuilder::new()
+        .protected(protected.clone())
+        .payload(pt.to_vec())
+        .add_created_signature(
+            CoseSignatureBuilder::new().protected(protected).build(),
+            aad,
+            |pt| signer.sign(pt),
+        )
+        .build();
+
+    let sign_data = sign.to_vec().unwrap();
+    let mut sign = CoseSign::from_slice(&sign_data).unwrap();
+
+    assert!(sign
+        .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+        .is_ok());
+
+    // Changing an unprotected header leaves the signature valid.
+    sign.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+    assert!(sign
+        .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+        .is_ok());
+
+    // Providing a different `aad` means the signature won't validate.
+    assert!(sign
+        .verify_signature(0, b"not aad", |sig, data| verifier.verify(sig, data))
+        .is_err());
+
+    // Changing a protected header invalidates the signature.
+    let mut sign2 = sign.clone();
+    sign2.protected = ProtectedHeader::default();
+    assert!(sign2
+        .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+        .is_err());
+    let mut sign3 = sign;
+    sign3.signatures[0].protected = ProtectedHeader::default();
+    assert!(sign2
+        .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+        .is_err());
+}
+
+#[test]
+fn test_sign_noncanonical() {
+    let signer = FakeSigner {};
+    let verifier = signer;
+    let pt = b"aa";
+    let aad = b"bb";
+
+    let tests = vec![
+        // Non-canonical: empty map can just be an empty bstr, not a bstr holding an empty map.
+        ("a0", Header::default()),
+        // Non-canonical: the map length (of 0) is non-minimally encoded as the 0x00 following
+        // 0xb8; it is short enough that it would normally be folded into the type byte
+        // (0xa0).
+        ("b800", Header::default()),
+        // Non-canonical: map not in canonical order.
+        (
+            concat!(
+                "a2", // 2-map
+                // The contents of the bstr-encoded header are not in canonical order.
+                "04", "42", "3131", // 4 (kid) => 2-bstr "11"
+                "01", "26", // 1 (alg) => ES256
+            ),
+            HeaderBuilder::new()
+                .algorithm(iana::Algorithm::ES256)
+                .key_id(vec![0x31, 0x31])
+                .build(),
+        ),
+    ];
+
+    for (protected_data, header) in tests {
+        // Build a protected header from a non-canonically encoded input.
+        let protected_data = hex::decode(protected_data).unwrap();
+        let protected =
+            ProtectedHeader::from_cbor_bstr(Value::Bytes(protected_data.clone())).unwrap();
+        assert_eq!(protected.header, header);
+        assert_eq!(protected.original_data, Some(protected_data));
+
+        // Build a signature whose inputs include the non-canonically encoded protected header.
+        let mut sign = CoseSign {
+            payload: Some(pt.to_vec()),
+            protected: protected.clone(),
+            ..Default::default()
+        };
+        let mut sig = CoseSignature {
+            protected: protected.clone(),
+            ..Default::default()
+        };
+        sig.protected = protected.clone();
+        sig.signature = signer.sign(&sign.tbs_data(aad, &sig));
+        sign.signatures.push(sig.clone());
+        let sign_data = sign.to_vec().unwrap();
+
+        // Parsing and verifying this signature should still succeed, because the `ProtectedHeader`
+        // includes the wire data and uses it for building the signature input.
+        let sign = CoseSign::from_slice(&sign_data).unwrap();
+        assert!(sign
+            .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+            .is_ok());
+
+        // However, if we attempt to build the same signature inputs by hand (thus not including the
+        // non-canonical wire data)...
+        let recreated_sign = CoseSignBuilder::new()
+            .protected(protected.header)
+            .payload(pt.to_vec())
+            .add_signature(sig)
+            .build();
+
+        // ...then the transplanted signature will not verify, because the re-building of the
+        // signature inputs will use the canonical encoding of the protected header, which
+        // is not what was originally used for the signature input.
+        assert!(recreated_sign
+            .verify_signature(0, aad, |sig, data| verifier.verify(sig, data))
+            .is_err());
+    }
+}
+
+#[test]
+fn test_sign_create_result() {
+    let signer = FakeSigner {};
+
+    let pt = b"This is the content";
+    let aad = b"this is additional data";
+
+    let protected = HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let _sign = CoseSignBuilder::new()
+        .protected(protected.clone())
+        .payload(pt.to_vec())
+        .try_add_created_signature(
+            CoseSignatureBuilder::new()
+                .protected(protected.clone())
+                .build(),
+            aad,
+            |pt| signer.try_sign(pt),
+        )
+        .unwrap()
+        .build();
+
+    let result = CoseSignBuilder::new()
+        .protected(protected.clone())
+        .payload(pt.to_vec())
+        .try_add_created_signature(
+            CoseSignatureBuilder::new().protected(protected).build(),
+            aad,
+            |pt| signer.fail_sign(pt),
+        );
+    expect_err(result, "failed");
+}
+
+#[test]
+#[should_panic]
+fn test_sign_sig_index_invalid() {
+    let signer = FakeSigner {};
+    let verifier = signer;
+
+    let pt = b"This is the content";
+    let aad = b"this is additional data";
+
+    let protected = HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let sign = CoseSignBuilder::new()
+        .protected(protected)
+        .payload(pt.to_vec())
+        .add_created_signature(CoseSignatureBuilder::new().build(), aad, |pt| {
+            signer.sign(pt)
+        })
+        .build();
+
+    // Attempt to verify an out-of-range signature
+    let _ = sign.verify_signature(sign.signatures.len(), aad, |sig, data| {
+        verifier.verify(sig, data)
+    });
+}
+
+#[test]
+fn test_sign1_roundtrip() {
+    let signer = FakeSigner {};
+    let verifier = signer;
+
+    let pt = b"This is the content";
+    let aad = b"this is additional data";
+
+    let protected = HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let sign1 = CoseSign1Builder::new()
+        .protected(protected)
+        .payload(pt.to_vec())
+        .create_signature(aad, |pt| signer.sign(pt))
+        .build();
+
+    let sign1_data = sign1.to_vec().unwrap();
+    let mut sign1 = CoseSign1::from_slice(&sign1_data).unwrap();
+
+    assert!(sign1
+        .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+        .is_ok());
+
+    // Changing an unprotected header leaves the signature valid.
+    sign1.unprotected.content_type = Some(ContentType::Text("text/plain".to_owned()));
+    assert!(sign1
+        .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+        .is_ok());
+
+    // Providing a different `aad` means the signature won't validate.
+    assert!(sign1
+        .verify_signature(b"not aad", |sig, data| verifier.verify(sig, data))
+        .is_err());
+
+    // Changing a protected header invalidates the signature.
+    sign1.protected.original_data = None;
+    sign1.protected.header.content_type = Some(ContentType::Text("text/plain".to_owned()));
+    assert!(sign1
+        .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+        .is_err());
+}
+
+#[test]
+fn test_sign1_create_result() {
+    let signer = FakeSigner {};
+
+    let pt = b"This is the content";
+    let aad = b"this is additional data";
+
+    let protected = HeaderBuilder::new()
+        .algorithm(iana::Algorithm::ES256)
+        .key_id(b"11".to_vec())
+        .build();
+    let _sign = CoseSign1Builder::new()
+        .protected(protected.clone())
+        .payload(pt.to_vec())
+        .try_create_signature(aad, |pt| signer.try_sign(pt))
+        .unwrap()
+        .build();
+
+    let result = CoseSign1Builder::new()
+        .protected(protected)
+        .payload(pt.to_vec())
+        .try_create_signature(aad, |pt| signer.fail_sign(pt));
+    expect_err(result, "failed");
+}
+
+#[test]
+fn test_sign1_noncanonical() {
+    let signer = FakeSigner {};
+    let verifier = signer;
+    let pt = b"aa";
+    let aad = b"bb";
+
+    // Build an empty protected header from a non-canonical input of 41a0 rather than 40.
+    let protected = ProtectedHeader::from_cbor_bstr(Value::Bytes(vec![0xa0])).unwrap();
+    assert_eq!(protected.header, Header::default());
+    assert_eq!(protected.original_data, Some(vec![0xa0]));
+
+    // Build a signature whose inputs include the non-canonically encoded protected header.
+    let mut sign1 = CoseSign1::default();
+    sign1.payload = Some(pt.to_vec());
+    sign1.protected = protected.clone();
+    sign1.signature = signer.sign(&sign1.tbs_data(aad));
+    let sign1_data = sign1.to_vec().unwrap();
+
+    // Parsing and verifying this signature should still succeed, because the `ProtectedHeader`
+    // includes the wire data and uses it for building the signature input.
+    let sign1 = CoseSign1::from_slice(&sign1_data).unwrap();
+    assert!(sign1
+        .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+        .is_ok());
+
+    // However, if we attempt to build the same signature inputs by hand (thus not including the
+    // non-canonical wire data)...
+    let recreated_sign1 = CoseSign1Builder::new()
+        .protected(protected.header)
+        .payload(pt.to_vec())
+        .signature(sign1.signature)
+        .build();
+
+    // ...then the transplanted signature will not verify, because the re-building of the signature
+    // inputs will use the canonical encoding of the protected header, which is not what was
+    // originally used for the signature input.
+    assert!(recreated_sign1
+        .verify_signature(aad, |sig, data| verifier.verify(sig, data))
+        .is_err());
+}
diff --git a/src/util/mod.rs b/src/util/mod.rs
new file mode 100644
index 0000000..b24ae7b
--- /dev/null
+++ b/src/util/mod.rs
@@ -0,0 +1,229 @@
+// 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 crate::{
+    cbor::value::{Integer, Value},
+    CoseError, Result,
+};
+use alloc::{boxed::Box, vec::Vec};
+
+#[cfg(test)]
+mod tests;
+
+/// Return an error indicating that an unexpected CBOR type was encountered.
+pub(crate) fn cbor_type_error<T>(value: &Value, want: &'static str) -> Result<T> {
+    let got = match value {
+        Value::Integer(_) => "int",
+        Value::Bytes(_) => "bstr",
+        Value::Float(_) => "float",
+        Value::Text(_) => "tstr",
+        Value::Bool(_) => "bool",
+        Value::Null => "nul",
+        Value::Tag(_, _) => "tag",
+        Value::Array(_) => "array",
+        Value::Map(_) => "map",
+        _ => "other",
+    };
+    Err(CoseError::UnexpectedItem(got, want))
+}
+
+/// Trait which augments the [`Value`] type with methods for convenient conversions to contained
+/// types which throw a [`CoseError`] if the Value is not of the expected type.
+pub(crate) trait ValueTryAs
+where
+    Self: Sized,
+{
+    /// Extractor for [`Value::Integer`]
+    fn try_as_integer(self) -> Result<Integer>;
+
+    /// Extractor for [`Value::Bytes`]
+    fn try_as_bytes(self) -> Result<Vec<u8>>;
+
+    /// Extractor for [`Value::Bytes`] which also throws an error if the byte string is zero length
+    fn try_as_nonempty_bytes(self) -> Result<Vec<u8>>;
+
+    /// Extractor for [`Value::Array`]
+    fn try_as_array(self) -> Result<Vec<Self>>;
+
+    /// Extractor for [`Value::Array`] which applies `f` to each item to build a new [`Vec`]
+    fn try_as_array_then_convert<F, T>(self, f: F) -> Result<Vec<T>>
+    where
+        F: Fn(Value) -> Result<T>;
+
+    /// Extractor for [`Value::Map`]
+    fn try_as_map(self) -> Result<Vec<(Self, Self)>>;
+
+    /// Extractor for [`Value::Tag`]
+    fn try_as_tag(self) -> Result<(u64, Box<Value>)>;
+}
+
+impl ValueTryAs for Value {
+    fn try_as_integer(self) -> Result<Integer> {
+        if let Value::Integer(i) = self {
+            Ok(i)
+        } else {
+            cbor_type_error(&self, "int")
+        }
+    }
+
+    fn try_as_bytes(self) -> Result<Vec<u8>> {
+        if let Value::Bytes(b) = self {
+            Ok(b)
+        } else {
+            cbor_type_error(&self, "bstr")
+        }
+    }
+
+    fn try_as_nonempty_bytes(self) -> Result<Vec<u8>> {
+        let v = self.try_as_bytes()?;
+        if v.is_empty() {
+            return Err(CoseError::UnexpectedItem("empty bstr", "non-empty bstr"));
+        }
+        Ok(v)
+    }
+
+    fn try_as_array(self) -> Result<Vec<Self>> {
+        if let Value::Array(a) = self {
+            Ok(a)
+        } else {
+            cbor_type_error(&self, "array")
+        }
+    }
+
+    fn try_as_array_then_convert<F, T>(self, f: F) -> Result<Vec<T>>
+    where
+        F: Fn(Value) -> Result<T>,
+    {
+        self.try_as_array()?
+            .into_iter()
+            .map(f)
+            .collect::<Result<Vec<_>, _>>()
+    }
+
+    fn try_as_map(self) -> Result<Vec<(Self, Self)>> {
+        if let Value::Map(a) = self {
+            Ok(a)
+        } else {
+            cbor_type_error(&self, "map")
+        }
+    }
+
+    fn try_as_tag(self) -> Result<(u64, Box<Value>)> {
+        if let Value::Tag(a, v) = self {
+            Ok((a, v))
+        } else {
+            cbor_type_error(&self, "tag")
+        }
+    }
+}
+
+/// Trait for types that can be converted to/from a [`Value`].
+pub trait AsCborValue: Sized {
+    /// Convert a [`Value`] into an instance of the type.
+    fn from_cbor_value(value: Value) -> Result<Self>;
+    /// Convert the object into a [`Value`], consuming it along the way.
+    fn to_cbor_value(self) -> Result<Value>;
+}
+
+/// Convert each item of an iterator to CBOR, and wrap the lot in
+/// a [`Value::Array`]
+pub fn to_cbor_array<C>(c: C) -> Result<Value>
+where
+    C: IntoIterator,
+    C::Item: AsCborValue,
+{
+    Ok(Value::Array(
+        c.into_iter()
+            .map(|e| e.to_cbor_value())
+            .collect::<Result<Vec<_>, _>>()?,
+    ))
+}
+
+/// Check for an expected error.
+#[cfg(test)]
+pub fn expect_err<T: core::fmt::Debug, E: core::fmt::Debug>(result: Result<T, E>, err_msg: &str) {
+    use alloc::format;
+    assert!(
+        result.is_err(),
+        "expected error containing '{}', got success {:?}",
+        err_msg,
+        result
+    );
+    let err = result.err();
+    assert!(
+        format!("{:?}", err).contains(err_msg),
+        "unexpected error {:?}, doesn't contain '{}'",
+        err,
+        err_msg
+    );
+}
+
+// Macros to reduce boilerplate when creating `CoseSomethingBuilder` structures.
+
+/// Add `new()` and `build()` methods to the builder.
+macro_rules! builder {
+    ( $otype: ty ) => {
+        /// Constructor for builder.
+        pub fn new() -> Self {
+            Self(<$otype>::default())
+        }
+        /// Build the completed object.
+        pub fn build(self) -> $otype {
+            self.0
+        }
+    };
+}
+
+/// Add a setter function for a field to the builder.
+macro_rules! builder_set {
+    ( $name:ident: $ftype:ty ) => {
+        /// Set the associated field.
+        #[must_use]
+        pub fn $name(mut self, $name: $ftype) -> Self {
+            self.0.$name = $name;
+            self
+        }
+    };
+}
+
+/// Add a setter function for an optional field to the builder.
+macro_rules! builder_set_optional {
+    ( $name:ident: $ftype:ty ) => {
+        /// Set the associated field.
+        #[must_use]
+        pub fn $name(mut self, $name: $ftype) -> Self {
+            self.0.$name = Some($name);
+            self
+        }
+    };
+}
+
+/// Add a setter function that fills out a `ProtectedHeader` from `Header` contents.
+macro_rules! builder_set_protected {
+    ( $name:ident ) => {
+        /// Set the associated field.
+        #[must_use]
+        pub fn $name(mut self, hdr: $crate::Header) -> Self {
+            self.0.$name = $crate::ProtectedHeader {
+                original_data: None,
+                header: hdr,
+            };
+            self
+        }
+    };
+}
diff --git a/src/util/tests.rs b/src/util/tests.rs
new file mode 100644
index 0000000..b75d248
--- /dev/null
+++ b/src/util/tests.rs
@@ -0,0 +1,40 @@
+// 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::{cbor::value::Value, util::expect_err};
+use alloc::{borrow::ToOwned, boxed::Box, vec};
+
+#[test]
+fn test_cbor_type_error() {
+    let cases = vec![
+        (Value::Null, "nul"),
+        (Value::Bool(true), "bool"),
+        (Value::Bool(false), "bool"),
+        (Value::from(128), "int"),
+        (Value::from(-1), "int"),
+        (Value::Bytes(vec![1, 2]), "bstr"),
+        (Value::Text("string".to_owned()), "tstr"),
+        (Value::Array(vec![Value::from(0)]), "array"),
+        (Value::Map(vec![]), "map"),
+        (Value::Tag(1, Box::new(Value::from(0))), "tag"),
+        (Value::Float(1.054571817), "float"),
+    ];
+    for (val, want) in cases {
+        let e = cbor_type_error::<()>(&val, "a");
+        expect_err(e, want);
+    }
+}