Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes to support ACME, including JWS #359

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
56 changes: 56 additions & 0 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 @@ -286,3 +287,58 @@ 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> {
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)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably macro it out to avoid duplicating the code from verify_signature

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried factoring out a function, let me know if this works for you.

}

/// 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(),
})
}
12 changes: 12 additions & 0 deletions src/header.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ pub struct Header {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "x5t#S256")]
pub x5t_s256: Option<String>,
/// 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>,
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add #347 (comment) while you're there?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added crit, enc, and zip -- I think I have the values there right, but I'm not familiar with their use so double checking it would probably be good.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. @inferiorhumanorgans can you have a look?

}

impl Header {
Expand All @@ -80,6 +90,8 @@ impl Header {
x5c: None,
x5t: None,
x5t_s256: 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};
Loading