diff --git a/README.md b/README.md index 043ef059..4e691999 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,20 @@ let token = decode::(&token, &DecodingKey::from_rsa_components(jwk["n"], If your key is in PEM format, it is better performance wise to generate the `DecodingKey` once in a `lazy_static` or something similar and reuse it. +### Encoding and decoding JWS + +JWS is handled the same way as JWT, but using `encode_jws` and `decode_jws`: + +```rust +let encoded = encode_jws(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?; +my_claims = decode_jws(&encoded, &DecodingKey::from_secret("secret".as_ref()), &Validation::default())?.claims; +``` + +`encode_jws` returns a `Jws` struct which can be placed in other structs or serialized/deserialized from JSON directly. + +The generic parameter in `Jws` indicates the claims type and prevents accidentally encoding or decoding the wrong claims type +when the Jws is nested in another struct. + ### JWK Thumbprints If you have a JWK object, you can generate a thumbprint like diff --git a/src/decoding.rs b/src/decoding.rs index 8d87f03d..9974b42f 100644 --- a/src/decoding.rs +++ b/src/decoding.rs @@ -6,6 +6,7 @@ use crate::crypto::verify; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; use crate::jwk::{AlgorithmParameters, Jwk}; +use crate::jws::Jws; #[cfg(feature = "use_pem")] use crate::pem::decoder::PemEncodedKey; use crate::serialization::{b64_decode, DecodedJwtPartClaims}; @@ -201,14 +202,13 @@ impl DecodingKey { } } -/// Verify signature of a JWT, and return header object and raw payload -/// -/// If the token or its signature is invalid, it will return an error. -fn verify_signature<'a>( - token: &'a str, +fn verify_signature_body( + header: &Header, + message: &str, + signature: &str, key: &DecodingKey, validation: &Validation, -) -> Result<(Header, &'a str)> { +) -> Result<()> { if validation.validate_signature && validation.algorithms.is_empty() { return Err(new_error(ErrorKind::MissingAlgorithm)); } @@ -221,10 +221,6 @@ fn verify_signature<'a>( } } - let (signature, message) = expect_two!(token.rsplitn(2, '.')); - let (payload, header) = expect_two!(message.rsplitn(2, '.')); - let header = Header::from_encoded(header)?; - if validation.validate_signature && !validation.algorithms.contains(&header.alg) { return Err(new_error(ErrorKind::InvalidAlgorithm)); } @@ -233,6 +229,23 @@ fn verify_signature<'a>( return Err(new_error(ErrorKind::InvalidSignature)); } + return Ok(()); +} + +/// Verify signature of a JWT, and return header object and raw payload +/// +/// If the token or its signature is invalid, it will return an error. +fn verify_signature<'a>( + token: &'a str, + key: &DecodingKey, + validation: &Validation, +) -> Result<(Header, &'a str)> { + let (signature, message) = expect_two!(token.rsplitn(2, '.')); + let (payload, header) = expect_two!(message.rsplitn(2, '.')); + let header = Header::from_encoded(header)?; + + verify_signature_body(&header, message, signature, key, validation)?; + Ok((header, payload)) } @@ -286,3 +299,37 @@ pub fn decode_header(token: &str) -> Result
{ let (_, header) = expect_two!(message.rsplitn(2, '.')); Header::from_encoded(header) } + +/// Verify signature of a JWS, and return the header object +/// +/// If the token or its signature is invalid, it will return an error. +fn verify_jws_signature( + jws: &Jws, + key: &DecodingKey, + validation: &Validation, +) -> Result
{ + let header = Header::from_encoded(&jws.protected)?; + let message = [jws.protected.as_str(), jws.payload.as_str()].join("."); + + verify_signature_body(&header, &message, &jws.signature, key, validation)?; + + Ok(header) +} + +/// Validate a received JWS and decode into the header and claims. +pub fn decode_jws( + jws: &Jws, + key: &DecodingKey, + validation: &Validation, +) -> Result> { + match verify_jws_signature(jws, key, validation) { + Err(e) => Err(e), + Ok(header) => { + let decoded_claims = DecodedJwtPartClaims::from_jwt_part_claims(&jws.payload)?; + let claims = decoded_claims.deserialize()?; + validate(decoded_claims.deserialize()?, validation)?; + + Ok(TokenData { header, claims }) + } + } +} diff --git a/src/encoding.rs b/src/encoding.rs index 26f5c4c3..d57ec541 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -5,6 +5,7 @@ use crate::algorithms::AlgorithmFamily; use crate::crypto; use crate::errors::{new_error, ErrorKind, Result}; use crate::header::Header; +use crate::jws::Jws; #[cfg(feature = "use_pem")] use crate::pem::decoder::PemEncodedKey; use crate::serialization::b64_encode_part; @@ -129,3 +130,30 @@ pub fn encode(header: &Header, claims: &T, key: &EncodingKey) -> R Ok([message, signature].join(".")) } + +/// Encode the header and claims given and sign the payload using the algorithm from the header and the key. +/// If the algorithm given is RSA or EC, the key needs to be in the PEM format. This produces a JWS instead of +/// a JWT -- usage is similar to `encode`, see that for more details. +pub fn encode_jws( + header: &Header, + claims: Option<&T>, + key: &EncodingKey, +) -> Result> { + if key.family != header.alg.family() { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + let encoded_header = b64_encode_part(header)?; + let encoded_claims = match claims { + Some(claims) => b64_encode_part(claims)?, + None => "".to_string(), + }; + let message = [encoded_header.as_str(), encoded_claims.as_str()].join("."); + let signature = crypto::sign(message.as_bytes(), key, header.alg)?; + + Ok(Jws { + protected: encoded_header, + payload: encoded_claims, + signature: signature, + _pd: Default::default(), + }) +} diff --git a/src/header.rs b/src/header.rs index 220f0fa4..4ec2dfd1 100644 --- a/src/header.rs +++ b/src/header.rs @@ -1,13 +1,110 @@ use std::result; use base64::{engine::general_purpose::STANDARD, Engine}; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; use crate::algorithms::Algorithm; use crate::errors::Result; use crate::jwk::Jwk; use crate::serialization::b64_decode; +const ZIP_SERIAL_DEFLATE: &'static str = "DEF"; +const ENC_A128CBC_HS256: &'static str = "A128CBC-HS256"; +const ENC_A192CBC_HS384: &'static str = "A192CBC-HS384"; +const ENC_A256CBC_HS512: &'static str = "A256CBC-HS512"; +const ENC_A128GCM: &'static str = "A128GCM"; +const ENC_A192GCM: &'static str = "A192GCM"; +const ENC_A256GCM: &'static str = "A256GCM"; + +/// Encryption algorithm for encrypted payloads. +/// +/// Defined in [RFC7516#4.1.2](https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.2). +/// +/// Values defined in [RFC7518#5.1](https://datatracker.ietf.org/doc/html/rfc7518#section-5.1). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[allow(clippy::upper_case_acronyms)] +pub enum Enc { + A128CBC_HS256, + A192CBC_HS384, + A256CBC_HS512, + A128GCM, + A192GCM, + A256GCM, + Other(String), +} + +impl Serialize for Enc { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + match self { + Enc::A128CBC_HS256 => ENC_A128CBC_HS256, + Enc::A192CBC_HS384 => ENC_A192CBC_HS384, + Enc::A256CBC_HS512 => ENC_A256CBC_HS512, + Enc::A128GCM => ENC_A128GCM, + Enc::A192GCM => ENC_A192GCM, + Enc::A256GCM => ENC_A256GCM, + Enc::Other(v) => v, + } + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Enc { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + ENC_A128CBC_HS256 => return Ok(Enc::A128CBC_HS256), + ENC_A192CBC_HS384 => return Ok(Enc::A192CBC_HS384), + ENC_A256CBC_HS512 => return Ok(Enc::A256CBC_HS512), + ENC_A128GCM => return Ok(Enc::A128GCM), + ENC_A192GCM => return Ok(Enc::A192GCM), + ENC_A256GCM => return Ok(Enc::A256GCM), + _ => (), + } + Ok(Enc::Other(s)) + } +} +/// Compression applied to plaintext. +/// +/// Defined in [RFC7516#4.1.3](https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.3). +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Zip { + Deflate, + Other(String), +} + +impl Serialize for Zip { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + match self { + Zip::Deflate => ZIP_SERIAL_DEFLATE, + Zip::Other(v) => v, + } + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for Zip { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + match s.as_str() { + ZIP_SERIAL_DEFLATE => return Ok(Zip::Deflate), + _ => (), + } + Ok(Zip::Other(s)) + } +} + /// A basic JWT header, the alg defaults to HS256 and typ is automatically /// set to `JWT`. All the other fields are optional. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] @@ -64,6 +161,27 @@ pub struct Header { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "x5t#S256")] pub x5t_s256: Option, + /// Critical - indicates header fields that must be understood by the receiver. + /// + /// Defined in [RFC7515#4.1.6](https://tools.ietf.org/html/rfc7515#section-4.1.6). + #[serde(skip_serializing_if = "Option::is_none")] + pub crit: Option>, + /// See `Enc` for description. + #[serde(skip_serializing_if = "Option::is_none")] + pub enc: Option, + /// See `Zip` for description. + #[serde(skip_serializing_if = "Option::is_none")] + pub zip: Option, + /// ACME: The URL to which this JWS object is directed + /// + /// Defined in [RFC8555#6.4](https://datatracker.ietf.org/doc/html/rfc8555#section-6.4). + #[serde(skip_serializing_if = "Option::is_none")] + pub url: Option, + /// ACME: Random data for preventing replay attacks. + /// + /// Defined in [RFC8555#6.5.2](https://datatracker.ietf.org/doc/html/rfc8555#section-6.5.2). + #[serde(skip_serializing_if = "Option::is_none")] + pub nonce: Option, } impl Header { @@ -80,6 +198,11 @@ impl Header { x5c: None, x5t: None, x5t_s256: None, + crit: None, + enc: None, + zip: None, + url: None, + nonce: None, } } diff --git a/src/jws.rs b/src/jws.rs new file mode 100644 index 00000000..0d1328f2 --- /dev/null +++ b/src/jws.rs @@ -0,0 +1,24 @@ +//! JSON Web Signatures data type. +use std::marker::PhantomData; + +use serde::{Deserialize, Serialize}; + +/// This is a serde-compatible JSON Web Signature structure. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Jws { + /// The base64 encoded header data. + /// + /// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2). + pub protected: String, + /// The base64 encoded claims data. + /// + /// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2). + pub payload: String, + /// The signature on the other fields. + /// + /// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2). + pub signature: String, + /// Unused, for associating type metadata. + #[serde(skip)] + pub _pd: PhantomData, +} diff --git a/src/lib.rs b/src/lib.rs index 0c8664bf..c7195e66 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,13 +12,14 @@ mod encoding; pub mod errors; mod header; pub mod jwk; +pub mod jws; #[cfg(feature = "use_pem")] mod pem; mod serialization; mod validation; pub use algorithms::Algorithm; -pub use decoding::{decode, decode_header, DecodingKey, TokenData}; -pub use encoding::{encode, EncodingKey}; +pub use decoding::{decode, decode_header, decode_jws, DecodingKey, TokenData}; +pub use encoding::{encode, encode_jws, EncodingKey}; pub use header::Header; pub use validation::{get_current_timestamp, Validation};