Skip to content

Commit dd34b25

Browse files
author
andrew
committed
Merge branch 'acme-and-jws' into jwk-from-encoding-key
2 parents f2f070b + dbc9dbd commit dd34b25

File tree

6 files changed

+249
-13
lines changed

6 files changed

+249
-13
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,20 @@ let token = decode::<Claims>(&token, &DecodingKey::from_rsa_components(jwk["n"],
146146
If your key is in PEM format, it is better performance wise to generate the `DecodingKey` once in a `lazy_static` or
147147
something similar and reuse it.
148148

149+
### Encoding and decoding JWS
150+
151+
JWS is handled the same way as JWT, but using `encode_jws` and `decode_jws`:
152+
153+
```rust
154+
let encoded = encode_jws(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?;
155+
my_claims = decode_jws(&encoded, &DecodingKey::from_secret("secret".as_ref()), &Validation::default())?.claims;
156+
```
157+
158+
`encode_jws` returns a `Jws<C>` struct which can be placed in other structs or serialized/deserialized from JSON directly.
159+
160+
The generic parameter in `Jws<C>` indicates the claims type and prevents accidentally encoding or decoding the wrong claims type
161+
when the Jws is nested in another struct.
162+
149163
### Convert SEC1 private key to PKCS8
150164
`jsonwebtoken` currently only supports PKCS8 format for private EC keys. If your key has `BEGIN EC PRIVATE KEY` at the top,
151165
this is a SEC1 type and can be converted to PKCS8 like so:

src/decoding.rs

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use crate::crypto::verify;
66
use crate::errors::{new_error, ErrorKind, Result};
77
use crate::header::Header;
88
use crate::jwk::{AlgorithmParameters, Jwk};
9+
use crate::jws::Jws;
910
#[cfg(feature = "use_pem")]
1011
use crate::pem::decoder::PemEncodedKey;
1112
use crate::serialization::{b64_decode, DecodedJwtPartClaims};
@@ -201,14 +202,13 @@ impl DecodingKey {
201202
}
202203
}
203204

204-
/// Verify signature of a JWT, and return header object and raw payload
205-
///
206-
/// If the token or its signature is invalid, it will return an error.
207-
fn verify_signature<'a>(
208-
token: &'a str,
205+
fn verify_signature_body(
206+
header: &Header,
207+
message: &str,
208+
signature: &str,
209209
key: &DecodingKey,
210210
validation: &Validation,
211-
) -> Result<(Header, &'a str)> {
211+
) -> Result<()> {
212212
if validation.validate_signature && validation.algorithms.is_empty() {
213213
return Err(new_error(ErrorKind::MissingAlgorithm));
214214
}
@@ -221,10 +221,6 @@ fn verify_signature<'a>(
221221
}
222222
}
223223

224-
let (signature, message) = expect_two!(token.rsplitn(2, '.'));
225-
let (payload, header) = expect_two!(message.rsplitn(2, '.'));
226-
let header = Header::from_encoded(header)?;
227-
228224
if validation.validate_signature && !validation.algorithms.contains(&header.alg) {
229225
return Err(new_error(ErrorKind::InvalidAlgorithm));
230226
}
@@ -233,6 +229,23 @@ fn verify_signature<'a>(
233229
return Err(new_error(ErrorKind::InvalidSignature));
234230
}
235231

232+
Ok(())
233+
}
234+
235+
/// Verify signature of a JWT, and return header object and raw payload
236+
///
237+
/// If the token or its signature is invalid, it will return an error.
238+
fn verify_signature<'a>(
239+
token: &'a str,
240+
key: &DecodingKey,
241+
validation: &Validation,
242+
) -> Result<(Header, &'a str)> {
243+
let (signature, message) = expect_two!(token.rsplitn(2, '.'));
244+
let (payload, header) = expect_two!(message.rsplitn(2, '.'));
245+
let header = Header::from_encoded(header)?;
246+
247+
verify_signature_body(&header, message, signature, key, validation)?;
248+
236249
Ok((header, payload))
237250
}
238251

@@ -286,3 +299,37 @@ pub fn decode_header(token: &str) -> Result<Header> {
286299
let (_, header) = expect_two!(message.rsplitn(2, '.'));
287300
Header::from_encoded(header)
288301
}
302+
303+
/// Verify signature of a JWS, and return the header object
304+
///
305+
/// If the token or its signature is invalid, it will return an error.
306+
fn verify_jws_signature<T>(
307+
jws: &Jws<T>,
308+
key: &DecodingKey,
309+
validation: &Validation,
310+
) -> Result<Header> {
311+
let header = Header::from_encoded(&jws.protected)?;
312+
let message = [jws.protected.as_str(), jws.payload.as_str()].join(".");
313+
314+
verify_signature_body(&header, &message, &jws.signature, key, validation)?;
315+
316+
Ok(header)
317+
}
318+
319+
/// Validate a received JWS and decode into the header and claims.
320+
pub fn decode_jws<T: DeserializeOwned>(
321+
jws: &Jws<T>,
322+
key: &DecodingKey,
323+
validation: &Validation,
324+
) -> Result<TokenData<T>> {
325+
match verify_jws_signature(jws, key, validation) {
326+
Err(e) => Err(e),
327+
Ok(header) => {
328+
let decoded_claims = DecodedJwtPartClaims::from_jwt_part_claims(&jws.payload)?;
329+
let claims = decoded_claims.deserialize()?;
330+
validate(decoded_claims.deserialize()?, validation)?;
331+
332+
Ok(TokenData { header, claims })
333+
}
334+
}
335+
}

src/encoding.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::algorithms::AlgorithmFamily;
55
use crate::crypto;
66
use crate::errors::{new_error, ErrorKind, Result};
77
use crate::header::Header;
8+
use crate::jws::Jws;
89
#[cfg(feature = "use_pem")]
910
use crate::pem::decoder::PemEncodedKey;
1011
use crate::serialization::b64_encode_part;
@@ -129,3 +130,30 @@ pub fn encode<T: Serialize>(header: &Header, claims: &T, key: &EncodingKey) -> R
129130

130131
Ok([message, signature].join("."))
131132
}
133+
134+
/// Encode the header and claims given and sign the payload using the algorithm from the header and the key.
135+
/// If the algorithm given is RSA or EC, the key needs to be in the PEM format. This produces a JWS instead of
136+
/// a JWT -- usage is similar to `encode`, see that for more details.
137+
pub fn encode_jws<T: Serialize>(
138+
header: &Header,
139+
claims: Option<&T>,
140+
key: &EncodingKey,
141+
) -> Result<Jws<T>> {
142+
if key.family != header.alg.family() {
143+
return Err(new_error(ErrorKind::InvalidAlgorithm));
144+
}
145+
let encoded_header = b64_encode_part(header)?;
146+
let encoded_claims = match claims {
147+
Some(claims) => b64_encode_part(claims)?,
148+
None => "".to_string(),
149+
};
150+
let message = [encoded_header.as_str(), encoded_claims.as_str()].join(".");
151+
let signature = crypto::sign(message.as_bytes(), key, header.alg)?;
152+
153+
Ok(Jws {
154+
protected: encoded_header,
155+
payload: encoded_claims,
156+
signature,
157+
_pd: Default::default(),
158+
})
159+
}

src/header.rs

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,109 @@
11
use std::result;
22

33
use base64::{engine::general_purpose::STANDARD, Engine};
4-
use serde::{Deserialize, Serialize};
4+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
55

66
use crate::algorithms::Algorithm;
77
use crate::errors::Result;
88
use crate::jwk::Jwk;
99
use crate::serialization::b64_decode;
1010

11+
const ZIP_SERIAL_DEFLATE: &str = "DEF";
12+
const ENC_A128CBC_HS256: &str = "A128CBC-HS256";
13+
const ENC_A192CBC_HS384: &str = "A192CBC-HS384";
14+
const ENC_A256CBC_HS512: &str = "A256CBC-HS512";
15+
const ENC_A128GCM: &str = "A128GCM";
16+
const ENC_A192GCM: &str = "A192GCM";
17+
const ENC_A256GCM: &str = "A256GCM";
18+
19+
/// Encryption algorithm for encrypted payloads.
20+
///
21+
/// Defined in [RFC7516#4.1.2](https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.2).
22+
///
23+
/// Values defined in [RFC7518#5.1](https://datatracker.ietf.org/doc/html/rfc7518#section-5.1).
24+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
25+
#[allow(clippy::upper_case_acronyms, non_camel_case_types)]
26+
pub enum Enc {
27+
A128CBC_HS256,
28+
A192CBC_HS384,
29+
A256CBC_HS512,
30+
A128GCM,
31+
A192GCM,
32+
A256GCM,
33+
Other(String),
34+
}
35+
36+
impl Serialize for Enc {
37+
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
38+
where
39+
S: Serializer,
40+
{
41+
match self {
42+
Enc::A128CBC_HS256 => ENC_A128CBC_HS256,
43+
Enc::A192CBC_HS384 => ENC_A192CBC_HS384,
44+
Enc::A256CBC_HS512 => ENC_A256CBC_HS512,
45+
Enc::A128GCM => ENC_A128GCM,
46+
Enc::A192GCM => ENC_A192GCM,
47+
Enc::A256GCM => ENC_A256GCM,
48+
Enc::Other(v) => v,
49+
}
50+
.serialize(serializer)
51+
}
52+
}
53+
54+
impl<'de> Deserialize<'de> for Enc {
55+
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
56+
where
57+
D: Deserializer<'de>,
58+
{
59+
let s = String::deserialize(deserializer)?;
60+
match s.as_str() {
61+
ENC_A128CBC_HS256 => return Ok(Enc::A128CBC_HS256),
62+
ENC_A192CBC_HS384 => return Ok(Enc::A192CBC_HS384),
63+
ENC_A256CBC_HS512 => return Ok(Enc::A256CBC_HS512),
64+
ENC_A128GCM => return Ok(Enc::A128GCM),
65+
ENC_A192GCM => return Ok(Enc::A192GCM),
66+
ENC_A256GCM => return Ok(Enc::A256GCM),
67+
_ => (),
68+
}
69+
Ok(Enc::Other(s))
70+
}
71+
}
72+
/// Compression applied to plaintext.
73+
///
74+
/// Defined in [RFC7516#4.1.3](https://datatracker.ietf.org/doc/html/rfc7516#section-4.1.3).
75+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
76+
pub enum Zip {
77+
Deflate,
78+
Other(String),
79+
}
80+
81+
impl Serialize for Zip {
82+
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
83+
where
84+
S: Serializer,
85+
{
86+
match self {
87+
Zip::Deflate => ZIP_SERIAL_DEFLATE,
88+
Zip::Other(v) => v,
89+
}
90+
.serialize(serializer)
91+
}
92+
}
93+
94+
impl<'de> Deserialize<'de> for Zip {
95+
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
96+
where
97+
D: Deserializer<'de>,
98+
{
99+
let s = String::deserialize(deserializer)?;
100+
match s.as_str() {
101+
ZIP_SERIAL_DEFLATE => Ok(Zip::Deflate),
102+
_ => Ok(Zip::Other(s)),
103+
}
104+
}
105+
}
106+
11107
/// A basic JWT header, the alg defaults to HS256 and typ is automatically
12108
/// set to `JWT`. All the other fields are optional.
13109
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
@@ -64,6 +160,27 @@ pub struct Header {
64160
#[serde(skip_serializing_if = "Option::is_none")]
65161
#[serde(rename = "x5t#S256")]
66162
pub x5t_s256: Option<String>,
163+
/// Critical - indicates header fields that must be understood by the receiver.
164+
///
165+
/// Defined in [RFC7515#4.1.6](https://tools.ietf.org/html/rfc7515#section-4.1.6).
166+
#[serde(skip_serializing_if = "Option::is_none")]
167+
pub crit: Option<Vec<String>>,
168+
/// See `Enc` for description.
169+
#[serde(skip_serializing_if = "Option::is_none")]
170+
pub enc: Option<Enc>,
171+
/// See `Zip` for description.
172+
#[serde(skip_serializing_if = "Option::is_none")]
173+
pub zip: Option<Zip>,
174+
/// ACME: The URL to which this JWS object is directed
175+
///
176+
/// Defined in [RFC8555#6.4](https://datatracker.ietf.org/doc/html/rfc8555#section-6.4).
177+
#[serde(skip_serializing_if = "Option::is_none")]
178+
pub url: Option<String>,
179+
/// ACME: Random data for preventing replay attacks.
180+
///
181+
/// Defined in [RFC8555#6.5.2](https://datatracker.ietf.org/doc/html/rfc8555#section-6.5.2).
182+
#[serde(skip_serializing_if = "Option::is_none")]
183+
pub nonce: Option<String>,
67184
}
68185

69186
impl Header {
@@ -80,6 +197,11 @@ impl Header {
80197
x5c: None,
81198
x5t: None,
82199
x5t_s256: None,
200+
crit: None,
201+
enc: None,
202+
zip: None,
203+
url: None,
204+
nonce: None,
83205
}
84206
}
85207

src/jws.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//! JSON Web Signatures data type.
2+
use std::marker::PhantomData;
3+
4+
use serde::{Deserialize, Serialize};
5+
6+
/// This is a serde-compatible JSON Web Signature structure.
7+
#[derive(Debug, Clone, Serialize, Deserialize)]
8+
pub struct Jws<C> {
9+
/// The base64 encoded header data.
10+
///
11+
/// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2).
12+
pub protected: String,
13+
/// The base64 encoded claims data.
14+
///
15+
/// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2).
16+
pub payload: String,
17+
/// The signature on the other fields.
18+
///
19+
/// Defined in [RFC7515#3.2](https://tools.ietf.org/html/rfc7515#section-3.2).
20+
pub signature: String,
21+
/// Unused, for associating type metadata.
22+
#[serde(skip)]
23+
pub _pd: PhantomData<C>,
24+
}

src/lib.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ mod encoding;
1212
pub mod errors;
1313
mod header;
1414
pub mod jwk;
15+
pub mod jws;
1516
#[cfg(feature = "use_pem")]
1617
mod pem;
1718
mod serialization;
1819
mod validation;
1920

2021
pub use algorithms::Algorithm;
21-
pub use decoding::{decode, decode_header, DecodingKey, TokenData};
22-
pub use encoding::{encode, EncodingKey};
22+
pub use decoding::{decode, decode_header, decode_jws, DecodingKey, TokenData};
23+
pub use encoding::{encode, encode_jws, EncodingKey};
2324
pub use header::Header;
2425
pub use validation::{get_current_timestamp, Validation};

0 commit comments

Comments
 (0)