Skip to content

Add method to generate JWK from EncodingKey #2

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

Merged
merged 7 commits into from
Jan 22, 2024
Merged
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
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.

### 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)
}

/// 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 })
}
}
}
30 changes: 29 additions & 1 deletion 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 All @@ -14,7 +15,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 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,
_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>,
}

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