Skip to content

Commit 3076f03

Browse files
committed
Changes to support ACME, including JWS Keats#359
1 parent 87bbe49 commit 3076f03

File tree

9 files changed

+481
-26
lines changed

9 files changed

+481
-26
lines changed

README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jsonwebtoken = "9"
1414
serde = {version = "1.0", features = ["derive"] }
1515
```
1616

17-
The minimum required Rust version (MSRV) is specified in the `rust-version` field in this project's [Cargo.toml](Cargo.toml).
17+
The minimum required Rust version (MSRV) is 1.67.
1818

1919
## Algorithms
2020
This library currently supports the following:
@@ -146,6 +146,28 @@ 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+
163+
### JWK Thumbprints
164+
165+
If you have a JWK object, you can generate a thumbprint like
166+
167+
```
168+
let tp = my_jwk.thumbprint(&jsonwebtoken::DIGEST_SHA256);
169+
```
170+
149171
### Convert SEC1 private key to PKCS8
150172
`jsonwebtoken` currently only supports PKCS8 format for private EC keys. If your key has `BEGIN EC PRIVATE KEY` at the top,
151173
this is a SEC1 type and can be converted to PKCS8 like so:
@@ -160,11 +182,9 @@ This library automatically validates the `exp` claim, and `nbf` is validated if
160182
those require setting the expected values in the `Validation` struct. In the case of `aud`, if there is a value set in the token but
161183
not in the `Validation`, the token will be rejected.
162184

163-
Validation is only made on present fields in the claims. It is possible to define the required claims, hence verifying that a JWT has a value for each of these claims before it is considered for validation. The required claims can be set in the `Validation` struct. The default option requires the `exp` claim to be present.
164-
165185
Since validating time fields is always a bit tricky due to clock skew,
166186
you can add some leeway to the `iat`, `exp`, and `nbf` validation by setting the `leeway` field.
167187

168188
Last but not least, you will need to set the algorithm(s) allowed for this token if you are not using `HS256`.
169189

170-
Look at `examples/validation.rs` for a full working example.
190+
Look at `examples/validation.rs` for a full working example.

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: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
use base64::{engine::general_purpose::STANDARD, Engine};
1+
use base64::{
2+
engine::general_purpose::{STANDARD, URL_SAFE},
3+
Engine,
4+
};
25
use serde::ser::Serialize;
36

47
use crate::algorithms::AlgorithmFamily;
58
use crate::crypto;
69
use crate::errors::{new_error, ErrorKind, Result};
710
use crate::header::Header;
11+
use crate::jws::Jws;
812
#[cfg(feature = "use_pem")]
913
use crate::pem::decoder::PemEncodedKey;
1014
use crate::serialization::b64_encode_part;
@@ -14,7 +18,7 @@ use crate::serialization::b64_encode_part;
1418
#[derive(Clone)]
1519
pub struct EncodingKey {
1620
pub(crate) family: AlgorithmFamily,
17-
content: Vec<u8>,
21+
pub(crate) content: Vec<u8>,
1822
}
1923

2024
impl EncodingKey {
@@ -29,6 +33,12 @@ impl EncodingKey {
2933
Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out })
3034
}
3135

36+
/// For loading websafe base64 HMAC secrets, ex: ACME EAB credentials.
37+
pub fn from_urlsafe_base64_secret(secret: &str) -> Result<Self> {
38+
let out = URL_SAFE.decode(secret)?;
39+
Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out })
40+
}
41+
3242
/// If you are loading a RSA key from a .pem file.
3343
/// This errors if the key is not a valid RSA key.
3444
/// Only exists if the feature `use_pem` is enabled.
@@ -129,3 +139,30 @@ pub fn encode<T: Serialize>(header: &Header, claims: &T, key: &EncodingKey) -> R
129139

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

src/header.rs

Lines changed: 124 additions & 2 deletions
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

@@ -106,4 +228,4 @@ impl Default for Header {
106228
fn default() -> Self {
107229
Header::new(Algorithm::default())
108230
}
109-
}
231+
}

0 commit comments

Comments
 (0)