diff --git a/src/decoding.rs b/src/decoding.rs index 8d87f03d..ea4a8bc8 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}; @@ -286,3 +287,58 @@ 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
{ + if validation.validate_signature && validation.algorithms.is_empty() { + return Err(new_error(ErrorKind::MissingAlgorithm)); + } + + if validation.validate_signature { + for alg in &validation.algorithms { + if key.family != alg.family() { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + } + } + + let header = Header::from_encoded(&jws.protected)?; + + if validation.validate_signature && !validation.algorithms.contains(&header.alg) { + return Err(new_error(ErrorKind::InvalidAlgorithm)); + } + + let message = [jws.protected.as_str(), jws.payload.as_str()].join("."); + + if validation.validate_signature + && !verify(&jws.signature, message.as_bytes(), key, header.alg)? + { + return Err(new_error(ErrorKind::InvalidSignature)); + } + + 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..6a414434 100644 --- a/src/header.rs +++ b/src/header.rs @@ -64,6 +64,16 @@ pub struct Header { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "x5t#S256")] pub x5t_s256: 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 +90,8 @@ impl Header { x5c: None, x5t: None, x5t_s256: None, + url: None, + nonce: None, } } 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};