diff --git a/.envrc b/.envrc index 3550a30..5bf8fc1 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,3 @@ -use flake +source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0=" + +use devenv \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 165523e..106bb7d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -92,3 +92,5 @@ jobs: uses: codecov/codecov-action@v3 with: fail_ci_if_error: true + env: + CODECOV_TOKEN: ${{ secrets. CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 72c9dc8..f95db5f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,12 @@ /result .direnv .envrc +# Devenv +.devenv* +devenv.local.nix + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml diff --git a/Cargo.toml b/Cargo.toml index 57233ca..a4c9461 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ name = "jose" version = "0.1.0" edition = "2021" license = "MIT OR Apache-2.0" +rust-version = "1.65" [features] default = [] diff --git a/deny.toml b/deny.toml index f24245a..8505b04 100644 --- a/deny.toml +++ b/deny.toml @@ -1,45 +1,18 @@ -targets = [] - -# This section is considered when running `cargo deny check advisories` -# More documentation for the advisories section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html [advisories] -# The path where the advisory database is cloned/fetched into +version = 2 + db-path = "~/.cargo/advisory-db" -# The url(s) of the advisory databases to use db-urls = ["https://github.com/rustsec/advisory-db"] -# The lint level for security vulnerabilities -vulnerability = "deny" -# The lint level for unmaintained crates -unmaintained = "warn" -# The lint level for crates that have been yanked from their source registry -yanked = "warn" -# The lint level for crates with security notices. Note that as of -# 2019-12-17 there are no security notice advisories in -# https://github.com/rustsec/advisory-db -notice = "warn" -# A list of advisory IDs to ignore. Note that ignored advisories will still -# output a note when they are encountered. -ignore = [] -# Threshold for security vulnerabilities, any vulnerability with a CVSS score -# lower than the range specified will be ignored. Note that ignored advisories -# will still output a note when they are encountered. -# * None - CVSS Score 0.0 -# * Low - CVSS Score 0.1 - 3.9 -# * Medium - CVSS Score 4.0 - 6.9 -# * High - CVSS Score 7.0 - 8.9 -# * Critical - CVSS Score 9.0 - 10.0 -#severity-threshold = +yanked = "deny" +ignore = [ + # There is no patch yet + "RUSTSEC-2023-0071" +] -# This section is considered when running `cargo deny check licenses` -# More documentation for the licenses section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html [licenses] -# The lint level for crates which do not have a detectable license -unlicensed = "deny" -# List of explictly allowed licenses -# See https://spdx.org/licenses/ for list of possible licenses -# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +version = 2 + +# List of explictly allowed licenses, all other licenses are denied allow = [ "MIT", "Apache-2.0", @@ -52,167 +25,24 @@ allow = [ # "ISC", # "CC0-1.0", ] -# List of explictly disallowed licenses -# See https://spdx.org/licenses/ for list of possible licenses -# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. -deny = [ - "AFL-1.1", - "AFL-1.2", - "AFL-2.0", - "AFL-2.1", - "AFL-3.0", - "AGPL-1.0", - "Apache-1.0", - "Apache-1.1", - "APSL-1.0", - "APSL-1.1", - "APSL-1.2", - "APSL-2.0", - "BSD-4-Clause", - "CDDL-1.0", - "CDDL-1.1", - "CPL-1.0", - "EPL-1.0", - "EPL-2.0", - "EUPL-1.0", - "EUPL-1.1", - "EUPL-1.2", - "IPL-1.0", - "LPPL-1.0", - "LPPL-1.1", - "LPPL-1.2", - "LPPL-1.3a", - "LPPL-1.3c", - "MS-PL", - "MS-RL", - "Nokia", - "OpenSSL", - "OSL-1.0", - "OSL-1.1", - "OSL-2.0", - "OSL-2.1", - "OSL-3.0", - "Python-2.0", - "QPL-1.0", - "SISSL", - "SISSL-1.2", - "Zend-2.0", - "ZPL-1.1", - "ZPL-2.0", - "ZPL-2.1", -] -# Lint level for licenses considered copyleft -copyleft = "allow" -# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses -# * both - The license will be approved if it is both OSI-approved *AND* FSF -# * either - The license will be approved if it is either OSI-approved *OR* FSF -# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF -# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved -# * neither - This predicate is ignored and the default lint level is used -allow-osi-fsf-free = "neither" -# Lint level used when no other predicates are matched -# 1. License isn't in the allow or deny lists -# 2. License isn't copyleft -# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" -default = "deny" -# The confidence threshold for detecting a license from license text. -# The higher the value, the more closely the license text must be to the -# canonical license text of a valid SPDX license file. -# [possible values: any between 0.0 and 1.0]. confidence-threshold = 0.8 -# Allow 1 or more licenses on a per-crate basis, so that particular licenses -# aren't accepted for every possible crate as with the normal allow list exceptions = [] -# Some crates don't have (easily) machine readable licensing information, -# adding a clarification entry for it allows you to manually specify the -# licensing information -#[[licenses.clarify]] -# The name of the crate the clarification applies to -#name = "ring" -# The optional version constraint for the crate -#version = "*" -# The SPDX expression for the license requirements of the crate -#expression = "MIT AND ISC AND OpenSSL" -# One or more files in the crate's source used as the "source of truth" for -# the license expression. If the contents match, the clarification will be used -# when running the license check, otherwise the clarification will be ignored -# and the crate will be checked normally, which may produce warnings or errors -# depending on the rest of your configuration -#license-files = [ -# Each entry is a crate relative path, and the (opaque) hash of its contents -#{ path = "LICENSE", hash = 0xbd0eed23 } -#] -[[licenses.clarify]] -name = "encoding_rs" -version = "*" -expression = "(Apache-2.0 OR MIT) AND BSD-3-Clause" -license-files = [{ path = "COPYRIGHT", hash = 0x39f8ad31 }] - [licenses.private] -# If true, ignores workspace crates that aren't published, or are only -# published to private registries ignore = false -# One or more private registries that you might publish crates to, if a crate -# is only published to private registries, and ignore is true, the crate will -# not have its license(s) checked registries = [] -# This section is considered when running `cargo deny check bans`. -# More documentation about the 'bans' section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html [bans] -# Lint level for when multiple versions of the same crate are detected multiple-versions = "warn" -# Lint level for when a crate version requirement is `*` wildcards = "deny" -# The graph highlighting used when creating dotgraphs for crates -# with multiple versions -# * lowest-version - The path to the lowest versioned duplicate is highlighted -# * simplest-path - The path to the version with the fewest edges is highlighted -# * all - Both lowest-version and simplest-path are used highlight = "all" -# List of crates that are allowed. Use with care! allow = [] -# List of crates to deny deny = [ { name = "ring", version = "*" }, - # Each entry the name of a crate and a version range. If version is - # not specified, all versions will be matched. - #{ name = "ansi_term", version = "=0.11.0" }, - # - # Wrapper crates can optionally be specified to allow the crate when it - # is a direct dependency of the otherwise banned crate - #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, ] -# Certain crates/versions that will be skipped when doing duplicate detection. -skip = [] -# Similarly to `skip` allows you to skip certain crates during duplicate -# detection. Unlike skip, it also includes the entire tree of transitive -# dependencies starting at the specified crate, up to a certain depth, which is -# by default infinite -skip-tree = [] -# This section is considered when running `cargo deny check sources`. -# More documentation about the 'sources' section can be found here: -# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html [sources] -# Lint level for what to happen when a crate from a crate registry that is not -# in the allow list is encountered unknown-registry = "warn" -# Lint level for what to happen when a crate from a git repository that is not -# in the allow list is encountered unknown-git = "warn" -# List of URLs for allowed crate registries. Defaults to the crates.io index -# if not specified. If it is specified but empty, no registries are allowed. allow-registry = ["https://github.com/rust-lang/crates.io-index"] -# List of URLs for allowed Git repositories allow-git = [] - -[sources.allow-org] -# 1 or more github.com organizations to allow git sources for -github = [] -# 1 or more gitlab.com organizations to allow git sources for -gitlab = [] -# 1 or more bitbucket.org organizations to allow git sources for -bitbucket = [] diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 0000000..7089a3a --- /dev/null +++ b/devenv.lock @@ -0,0 +1,195 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1712996253, + "owner": "cachix", + "repo": "devenv", + "rev": "de824c66bce7a07f3f454062aef67471038905ad", + "treeHash": "a286cc367f3fd3a80e5f70921b71df5d86ba35ce", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1712903033, + "owner": "nix-community", + "repo": "fenix", + "rev": "c739f83545e625227f4d0af7fe2a71e69931fa4c", + "treeHash": "a532b83dc2ebc9245577c1e75d6ada24027f0da9", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "treeHash": "2addb7b71a20a25ea74feeaf5c2f6a6b30898ecb", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "treeHash": "bd263f021e345cb4a39d80c126ab650bebc3c10c", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "treeHash": "ca14199cabdfe1a06a7b1654c76ed49100a689f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1710796454, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "06fb0f1c643aee3ae6838dda3b37ef0abc3c763b", + "treeHash": "9bb13f7f39e825a5d91bbe4139fbc129243b907d", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "nixpkgs-stable": { + "locked": { + "lastModified": 1712867921, + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "51651a540816273b67bc4dedea2d37d116c5f7fe", + "treeHash": "13ccd7f7add28a1c4d0f1f47adc4342a5b097b0b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-23.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "flake-utils": "flake-utils", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-stable": "nixpkgs-stable" + }, + "locked": { + "lastModified": 1712897695, + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "40e6053ecb65fcbf12863338a6dcefb3f55f1bf8", + "treeHash": "9ba338feee8e6b2193c305f46b65b0fef49816b7", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "fenix": "fenix", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1712818880, + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "657b33b0cb9bd49085202e91ad5b4676532c9140", + "treeHash": "3fb639207b9c21d29104bb4168e8be92f191c283", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "treeHash": "cce81f2a0f0743b2eb61bc2eb6c7adbe2f2c6beb", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 0000000..236c938 --- /dev/null +++ b/devenv.nix @@ -0,0 +1,17 @@ +{pkgs, ...}: { + packages = with pkgs; [ + step-cli + cargo-deny + ]; + + enterTest = '' + cargo clippy + cargo test + ''; + + languages.rust = { + enable = true; + channel = "nightly"; + components = ["rustc" "cargo" "clippy" "rustfmt" "rust-src"]; + }; +} diff --git a/devenv.yaml b/devenv.yaml new file mode 100644 index 0000000..2bbabf4 --- /dev/null +++ b/devenv.yaml @@ -0,0 +1,8 @@ +inputs: + nixpkgs: + url: github:cachix/devenv-nixpkgs/rolling + fenix: + url: github:nix-community/fenix + inputs: + nixpkgs: + follows: nixpkgs diff --git a/flake.lock b/flake.lock deleted file mode 100644 index f7ce1b4..0000000 --- a/flake.lock +++ /dev/null @@ -1,130 +0,0 @@ -{ - "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1694529238, - "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "flake-utils_2": { - "inputs": { - "systems": "systems_2" - }, - "locked": { - "lastModified": 1681202837, - "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "cfacdce06f30d2b68473a46042957675eebb3401", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1698318101, - "narHash": "sha256-gUihHt3yPD7bVqg+k/UVHgngyaJ3DMEBchbymBMvK1E=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "63678e9f3d3afecfeafa0acead6239cdb447574c", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs_2": { - "locked": { - "lastModified": 1681358109, - "narHash": "sha256-eKyxW4OohHQx9Urxi7TQlFBTDWII+F+x2hklDOQPB50=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "96ba1c52e54e74c3197f4d43026b3f3d92e83ff9", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs", - "rust-overlay": "rust-overlay" - } - }, - "rust-overlay": { - "inputs": { - "flake-utils": "flake-utils_2", - "nixpkgs": "nixpkgs_2" - }, - "locked": { - "lastModified": 1698458995, - "narHash": "sha256-nF8E8Ur5NggwPQNp3w/fddWmQrNEwCm0dgz6tk8Ew6E=", - "owner": "oxalica", - "repo": "rust-overlay", - "rev": "571fee291b386dd6fe0d125bc20a7c7b3ad042ac", - "type": "github" - }, - "original": { - "owner": "oxalica", - "repo": "rust-overlay", - "type": "github" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - }, - "systems_2": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } - } - }, - "root": "root", - "version": 7 -} diff --git a/flake.nix b/flake.nix deleted file mode 100644 index 5a32cd1..0000000 --- a/flake.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - rust-overlay.url = "github:oxalica/rust-overlay"; - flake-utils.url = "github:numtide/flake-utils"; - }; - - outputs = { - nixpkgs, - rust-overlay, - flake-utils, - ... - }: - flake-utils.lib.eachDefaultSystem ( - system: let - overlays = [(import rust-overlay)]; - pkgs = import nixpkgs { - inherit system overlays; - }; - in - with pkgs; { - devShells.default = mkShell { - buildInputs = [ - rust-bin.nightly.latest.default - step-cli - cargo-audit - cargo-outdated - ]; - }; - } - ); -} diff --git a/src/base64_url.rs b/src/base64_url.rs index 9942532..a8a7fdf 100644 --- a/src/base64_url.rs +++ b/src/base64_url.rs @@ -24,7 +24,7 @@ impl std::error::Error for NoBase64UrlString {} /// A wrapper around a [`String`] that guarantees that the inner string is a /// valid Base64Url string. -#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Default)] #[repr(transparent)] #[serde(transparent)] pub struct Base64UrlString(String); @@ -61,6 +61,12 @@ impl FromStr for Base64UrlString { } impl Base64UrlString { + /// Creates a new, empty Base64Url string. + #[inline] + pub const fn new() -> Self { + Self(String::new()) + } + /// Encode the given bytes using Base64Url format. #[inline] pub fn encode(x: impl AsRef<[u8]>) -> Self { diff --git a/src/format.rs b/src/format.rs index 13a371b..f6fde0a 100644 --- a/src/format.rs +++ b/src/format.rs @@ -22,7 +22,7 @@ pub(crate) mod sealed { use crate::{ header::{self, JoseHeaderBuilder, JoseHeaderBuilderError}, - jws::{PayloadKind, SignError, Signer}, + jws::{PayloadData, SignError, Signer}, }; // We put all methods, types, etc into a sealed trait, so @@ -48,7 +48,7 @@ pub(crate) mod sealed { fn finalize( header: Self::SerializedJwsHeader, - payload: PayloadKind, + payload: Option, signature: &[u8], ) -> Result; diff --git a/src/format/compact.rs b/src/format/compact.rs index eacb635..67b4afb 100644 --- a/src/format/compact.rs +++ b/src/format/compact.rs @@ -5,7 +5,7 @@ use super::{sealed, Format}; use crate::{ base64_url::NoBase64UrlString, header, - jws::{PayloadKind, SignError, Signer}, + jws::{PayloadData, SignError, Signer}, Base64UrlString, JoseHeader, }; @@ -52,14 +52,17 @@ impl sealed::SealedFormat for Compact { fn finalize( header: Self::SerializedJwsHeader, - payload: PayloadKind, + payload: Option, signature: &[u8], ) -> Result { let mut compact = Compact::with_capacity(3); compact.push_base64url(header); - let PayloadKind::Standard(payload) = payload; + let payload = match payload { + Some(PayloadData::Standard(b64)) => b64, + None => Base64UrlString::new(), + }; compact.parts.push(payload); compact.push(signature); diff --git a/src/format/json_flattened.rs b/src/format/json_flattened.rs index 442130f..dd799e8 100644 --- a/src/format/json_flattened.rs +++ b/src/format/json_flattened.rs @@ -7,14 +7,14 @@ use serde_json::Value; use super::{sealed, Format}; use crate::{ header, - jws::{PayloadKind, SignError}, + jws::{PayloadData, SignError}, Base64UrlString, JoseHeader, }; /// The flattened json serialization format. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct JsonFlattened { - pub(crate) payload: Base64UrlString, + pub(crate) payload: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) protected: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -74,10 +74,10 @@ impl sealed::SealedFormat for JsonFlattened { fn finalize( (protected, unprotected): Self::SerializedJwsHeader, - payload: PayloadKind, + payload: Option, signature: &[u8], ) -> Result { - let PayloadKind::Standard(payload) = payload; + let payload = payload.map(|PayloadData::Standard(b64)| b64); let signature = Base64UrlString::encode(signature); diff --git a/src/format/json_general.rs b/src/format/json_general.rs index 9c7bf5a..6951398 100644 --- a/src/format/json_general.rs +++ b/src/format/json_general.rs @@ -7,7 +7,7 @@ use serde_json::Value; use super::{sealed, Format}; use crate::{ header::{self, JoseHeaderBuilder, JoseHeaderBuilderError}, - jws::{PayloadKind, SignError, Signer}, + jws::{PayloadData, SignError, Signer}, Base64UrlString, JoseHeader, }; @@ -26,7 +26,7 @@ pub(crate) struct Signature { /// [Section 7.2.1]: https://datatracker.ietf.org/doc/html/rfc7515#section-7.2.1 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct JsonGeneral { - pub(crate) payload: Base64UrlString, + pub(crate) payload: Option, pub(crate) signatures: Vec, } @@ -91,10 +91,10 @@ impl sealed::SealedFormat for JsonGeneral { fn finalize( header: Self::SerializedJwsHeader, - payload: PayloadKind, + payload: Option, signature: &[u8], ) -> Result { - let PayloadKind::Standard(payload) = payload; + let payload = payload.map(|PayloadData::Standard(b64)| b64); let signature = Base64UrlString::encode(signature); diff --git a/src/jws.rs b/src/jws.rs index 623d704..68c58de 100644 --- a/src/jws.rs +++ b/src/jws.rs @@ -4,7 +4,6 @@ use alloc::{format, string::String, vec, vec::Vec}; -use base64ct::{Base64UrlUnpadded, Encoding}; use thiserror_no_std::Error; use crate::{ @@ -20,13 +19,36 @@ mod verify; pub use {builder::*, sign::*, verify::*}; // FIXME: check section 5.3. (string comparison) and verify correctness -// FIXME: Appendix F: Detached Content // FIXME: protected headers +// FIXME: unencoded payload (IMPORTANT: check that string is all ascii, except +// `.` character) -/// Different interpretations of a JWS payload. -// FIXME: unencoded payload (IMPORTANT: check that string is all ascii, except `.` character) +/// The kind of payload used in a JWS. +/// +/// Kind means that a payload data is either attached, or detached. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum PayloadKind { + /// Attached payload. + /// + /// The payload data will be put into the JWS. + /// This is the standard kind. + Attached(PayloadData), + + /// Detached payload. + /// + /// Detached payload is a special payload + /// representation of a JWS, specified + /// in [Appendix F](https://datatracker.ietf.org/doc/html/rfc7515#appendix-F) + /// of the JWS RFC. + /// + /// Essentially, the payload is not put into the JWS, + /// instead it's only used for signing. + Detached(PayloadData), +} + +/// The raw payload data that should be stored in the JWS. +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub enum PayloadData { /// The given base64 string will just be used as the payload. Standard(Base64UrlString), } @@ -43,7 +65,7 @@ pub enum PayloadKind { /// # use alloc::string::{FromUtf8Error, String}; /// # use core::convert::Infallible; /// # use jose::Base64UrlString; -/// # use jose::jws::{FromRawPayload, IntoPayload, PayloadKind}; +/// # use jose::jws::{FromRawPayload, IntoPayload, PayloadKind, PayloadData}; /// /// #[derive(Debug, PartialEq, Eq)] /// struct StringPayload(String); @@ -53,7 +75,7 @@ pub enum PayloadKind { /// /// fn into_payload(self) -> Result { /// let s = Base64UrlString::encode(self.0); -/// Ok(PayloadKind::Standard(s)) +/// Ok(PayloadKind::Attached(PayloadData::Standard(s))) /// } /// } /// ``` @@ -78,15 +100,44 @@ pub trait IntoPayload { /// This is required to be implemented when trying to decoe a JWS, or encrypt a /// JWE, from it's format representation. pub trait FromRawPayload: Sized { - /// The error that can occurr in the [`Self::from_raw_payload`] method. + /// The error that can occurr in any of the `from_*` methods. type Error; - /// Converts a raw [`PayloadKind`] enum into this payload type. + /// Converts a standard, attached [`PayloadData`] into this payload type. /// /// # Errors /// /// Returns an error if the operation failed. - fn from_raw_payload(payload: PayloadKind) -> Result; + fn from_attached(payload: PayloadData) -> Result; + + /// Construts this payload type from a detached content. + /// + /// For context, the header of the JWS will be provided, + /// to get / construct the payload. + /// + /// In addition to `Self`, the raw payload data must be + /// returned, in order to verify the signature. + /// + /// # Errors + /// + /// Returns an error if the operation failed. + fn from_detached(header: &JoseHeader) -> Result<(Self, PayloadData), Self::Error>; + + /// Construts this payload type from a detached content. + /// + /// This method is only used when verifying JWS in JSON General format. + /// For context, all the header of the JWS will be provided, + /// to get / construct the payload. + /// + /// In addition to `Self`, the raw payload data must be + /// returned, in order to verify the signature. + /// + /// # Errors + /// + /// Returns an error if the operation failed. + fn from_detached_many( + headers: &[JoseHeader], + ) -> Result<(Self, PayloadData), Self::Error>; } /// Different kinds of errors that can occurr while signing a JWS. @@ -215,9 +266,13 @@ impl JsonWebSignature { msg.push(b'.'); let payload = self.payload.into_payload().map_err(SignError::Payload)?; - match payload { - PayloadKind::Standard(ref b64) => msg.extend(b64.as_bytes()), - } + let payload = match payload { + PayloadKind::Attached(PayloadData::Standard(b64)) => { + msg.extend(b64.as_bytes()); + Some(PayloadData::Standard(b64)) + } + PayloadKind::Detached(_) => None, + }; let signature = signer.sign(&msg).map_err(SignError::Sign)?; @@ -257,7 +312,8 @@ impl JsonWebSignature { let payload = self.payload.into_payload().map_err(SignError::Payload)?; let payload_msg = match payload { - PayloadKind::Standard(ref b64) => b64.as_bytes(), + PayloadKind::Attached(PayloadData::Standard(ref b64)) => b64.as_bytes(), + PayloadKind::Detached(_) => todo!(), }; let mut signatures = vec![]; @@ -298,7 +354,10 @@ impl JsonWebSignature { }); } - let PayloadKind::Standard(payload) = payload; + let payload = match payload { + PayloadKind::Attached(PayloadData::Standard(s)) => Some(s), + PayloadKind::Detached(_) => None, + }; Ok(Signed { value: JsonGeneral { @@ -360,15 +419,25 @@ impl DecodeFormat for JsonWebSignature { let (payload, raw_payload) = { let raw = input.part(1).expect("`len()` is checked above to be 3"); - let payload = PayloadKind::Standard(raw.clone()); - let payload = T::from_raw_payload(payload).map_err(ParseCompactError::Payload)?; - (payload, raw.decode()) + + // if payload is empty, detached payload + let (payload, raw) = if raw.is_empty() { + T::from_detached(&header).map_err(ParseCompactError::Payload)? + } else { + let data = PayloadData::Standard(raw.clone()); + + ( + T::from_attached(data.clone()).map_err(ParseCompactError::Payload)?, + data, + ) + }; + + (payload, raw) }; + let PayloadData::Standard(raw_payload) = raw_payload; let signature = input.part(2).expect("`len()` is checked above to be 3"); - let raw_payload = Base64UrlUnpadded::encode_string(&raw_payload); - let msg = format!("{}.{}", raw_header, raw_payload); Ok(Unverified { @@ -380,7 +449,7 @@ impl DecodeFormat for JsonWebSignature { } fn parse_json_header( - protected: Option, + protected: Option<&Base64UrlString>, header: Option>, ) -> Result, ParseJsonError> { let protected = match protected { @@ -433,18 +502,20 @@ impl DecodeFormat for JsonWebSignature Result, Self::Error> { - let msg = match protected { - Some(ref protected) => format!("{}.{}", protected, payload), - None => format!(".{}", payload), - }; - - let header = parse_json_header(protected, header)?; - - let payload = { - let payload_kind = PayloadKind::Standard(payload); - T::from_raw_payload(payload_kind).map_err(ParseJsonError::Payload)? + let protected_str = protected.clone().unwrap_or_default().into_inner(); + let header = parse_json_header(protected.as_ref(), header)?; + + let (payload, raw_payload) = match payload { + Some(b64) => ( + T::from_attached(PayloadData::Standard(b64.clone())) + .map_err(ParseJsonError::Payload)?, + PayloadData::Standard(b64), + ), + None => T::from_detached(&header).map_err(ParseJsonError::Payload)?, }; + let PayloadData::Standard(raw_payload) = raw_payload; + let msg = format!("{}.{}", protected_str, raw_payload); Ok(Unverified { value: JsonWebSignature { header, payload }, signature: signature.decode(), @@ -467,25 +538,34 @@ impl DecodeFormat for JsonWebSignature format!("{}.{}", protected, payload), - None => format!(".{}", payload), - }; - - let header = parse_json_header(sig.protected, sig.header)?; + let header = parse_json_header(sig.protected.as_ref(), sig.header)?; headers.push(header); - unverified_signatures.push((msg.into_bytes(), sig.signature.decode())); + sigs.push((sig.protected.unwrap_or_default(), sig.signature.decode())); } - let payload = { - let payload_kind = PayloadKind::Standard(payload); - T::from_raw_payload(payload_kind).map_err(ParseJsonError::Payload)? + let (payload, raw_payload) = match payload { + Some(b64) => ( + T::from_attached(PayloadData::Standard(b64.clone())) + .map_err(ParseJsonError::Payload)?, + PayloadData::Standard(b64), + ), + None => T::from_detached_many(&headers).map_err(ParseJsonError::Payload)?, }; + let PayloadData::Standard(raw_payload) = raw_payload; + + let unverified_signatures = sigs + .into_iter() + .map(|(protected, signature)| { + let msg = format!("{}.{}", protected, raw_payload); + + (msg.into_bytes(), signature) + }) + .collect(); Ok(ManyUnverified { value: JsonWebSignature { diff --git a/tests/jws.rs b/tests/jws.rs index fc55c50..dc81b6d 100644 --- a/tests/jws.rs +++ b/tests/jws.rs @@ -3,7 +3,7 @@ // - additional (private, public) headers // - for supporting all MUST BE UNDERSTOOD params -use std::{convert::Infallible, str::FromStr, string::FromUtf8Error}; +use std::{convert::Infallible, str::FromStr}; use jose::{ format::{Compact, JsonFlattened, JsonGeneral}, @@ -15,8 +15,8 @@ use jose::{ JwkSigner, JwkVerifier, }, jws::{ - FromRawPayload, IntoPayload, IntoSigner, IntoVerifier, ManyUnverified, PayloadKind, Signer, - Unverified, Verifier, + FromRawPayload, IntoPayload, IntoSigner, IntoVerifier, ManyUnverified, PayloadData, + PayloadKind, Signer, Unverified, Verifier, }, policy::{Checkable, StandardPolicy}, Base64UrlString, JsonWebKey, Jws, @@ -41,13 +41,25 @@ impl From<&str> for StringPayload { } impl FromRawPayload for StringPayload { - type Error = FromUtf8Error; + type Error = String; - fn from_raw_payload(payload: PayloadKind) -> Result { + fn from_attached(payload: PayloadData) -> Result { match payload { - PayloadKind::Standard(s) => String::from_utf8(s.decode()).map(StringPayload), + PayloadData::Standard(s) => String::from_utf8(s.decode()) + .map(StringPayload) + .map_err(|e| e.to_string()), } } + + fn from_detached(_: &jose::JoseHeader) -> Result<(Self, PayloadData), Self::Error> { + Err(String::from("detached payload not supported")) + } + + fn from_detached_many( + _: &[jose::JoseHeader], + ) -> Result<(Self, PayloadData), Self::Error> { + Err(String::from("detached payload not supported")) + } } impl IntoPayload for StringPayload { @@ -55,7 +67,7 @@ impl IntoPayload for StringPayload { fn into_payload(self) -> Result { let s = Base64UrlString::encode(self.0); - Ok(PayloadKind::Standard(s)) + Ok(PayloadKind::Attached(PayloadData::Standard(s))) } }