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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,28 @@ 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

```
let tp = my_jwk.thumbprint(&jsonwebtoken::DIGEST_SHA256);
```

### Convert SEC1 private key to PKCS8
`jsonwebtoken` currently only supports PKCS8 format for private EC keys. If your key has `BEGIN EC PRIVATE KEY` at the top,
this is a SEC1 type and can be converted to PKCS8 like so:
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));
}

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)
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 })
}
}
}
41 changes: 39 additions & 2 deletions src/encoding.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
use base64::{engine::general_purpose::STANDARD, Engine};
use base64::{
engine::general_purpose::{STANDARD, URL_SAFE},
Engine,
};
use serde::ser::Serialize;

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 All @@ -14,7 +18,7 @@ use crate::serialization::b64_encode_part;
#[derive(Clone)]
pub struct EncodingKey {
pub(crate) family: AlgorithmFamily,
content: Vec<u8>,
pub(crate) content: Vec<u8>,
}

impl EncodingKey {
Expand All @@ -29,6 +33,12 @@ impl EncodingKey {
Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out })
}

/// For loading websafe base64 HMAC secrets, ex: ACME EAB credentials.
pub fn from_urlsafe_base64_secret(secret: &str) -> Result<Self> {
let out = URL_SAFE.decode(secret)?;
Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out })
}

/// If you are loading a RSA key from a .pem file.
/// This errors if the key is not a valid RSA key.
/// Only exists if the feature `use_pem` is enabled.
Expand Down Expand Up @@ -129,3 +139,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,
_pd: Default::default(),
})
}
124 changes: 123 additions & 1 deletion src/header.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,109 @@
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: &str = "DEF";
const ENC_A128CBC_HS256: &str = "A128CBC-HS256";
const ENC_A192CBC_HS384: &str = "A192CBC-HS384";
const ENC_A256CBC_HS512: &str = "A256CBC-HS512";
const ENC_A128GCM: &str = "A128GCM";
const ENC_A192GCM: &str = "A192GCM";
const ENC_A256GCM: &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, non_camel_case_types)]
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 => 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 +160,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>,
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 +197,11 @@ impl Header {
x5c: None,
x5t: None,
x5t_s256: None,
crit: None,
enc: None,
zip: None,
url: None,
nonce: None,
}
}

Expand Down
Loading