Skip to content

Commit

Permalink
Merge branch 'acme-and-jws' into jwk-thumbprint
Browse files Browse the repository at this point in the history
  • Loading branch information
andrew committed Jan 17, 2024
2 parents a1cf7b7 + b0072fd commit 9709b81
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 13 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,20 @@ let token = decode::<Claims>(&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<C>` struct which can be placed in other structs or serialized/deserialized from JSON directly.

The generic parameter in `Jws<C>` 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
Expand Down
67 changes: 57 additions & 10 deletions src/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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));
}
Expand All @@ -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));
}
Expand All @@ -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))
}

Expand Down Expand Up @@ -286,3 +299,37 @@ pub fn decode_header(token: &str) -> Result<Header> {
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<T>(
jws: &Jws<T>,
key: &DecodingKey,
validation: &Validation,
) -> Result<Header> {
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<T: DeserializeOwned>(
jws: &Jws<T>,
key: &DecodingKey,
validation: &Validation,
) -> Result<TokenData<T>> {
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 })
}
}
}
28 changes: 28 additions & 0 deletions src/encoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -129,3 +130,30 @@ pub fn encode<T: Serialize>(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<T: Serialize>(
header: &Header,
claims: Option<&T>,
key: &EncodingKey,
) -> Result<Jws<T>> {
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(),
})
}
125 changes: 124 additions & 1 deletion src/header.rs
Original file line number Diff line number Diff line change
@@ -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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
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<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Zip::Deflate => ZIP_SERIAL_DEFLATE,
Zip::Other(v) => v,
}
.serialize(serializer)
}
}

impl<'de> Deserialize<'de> for Zip {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
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)]
Expand Down Expand Up @@ -64,6 +161,27 @@ pub struct Header {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x5t#S256")]
pub x5t_s256: Option<String>,
/// 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<Vec<String>>,
/// See `Enc` for description.
#[serde(skip_serializing_if = "Option::is_none")]
pub enc: Option<Enc>,
/// See `Zip` for description.
#[serde(skip_serializing_if = "Option::is_none")]
pub zip: Option<Zip>,
/// 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<String>,
/// 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<String>,
}

impl Header {
Expand All @@ -80,6 +198,11 @@ impl Header {
x5c: None,
x5t: None,
x5t_s256: None,
crit: None,
enc: None,
zip: None,
url: None,
nonce: None,
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/jws.rs
Original file line number Diff line number Diff line change
@@ -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<C> {
/// 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<C>,
}
5 changes: 3 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

0 comments on commit 9709b81

Please sign in to comment.