Skip to content

Commit ab5764f

Browse files
committed
Changes to support ACME, including JWS
From #359 and @andrewbaxter
1 parent ca09d64 commit ab5764f

File tree

10 files changed

+542
-16
lines changed

10 files changed

+542
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- BREAKING: now using traits for crypto backends, you have to choose between `aws_lc_rs` and `rust_crypto`
66
- Add `Clone` bound to `decode`
77
- Support decoding byte slices
8+
- Support JWS
89

910
## 9.3.1 (2024-02-06)
1011

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,28 @@ RSA/EC, the key should always be the content of the private key in PEM or DER fo
111111
If your key is in PEM format, it is better performance wise to generate the `EncodingKey` once in a `lazy_static` or
112112
something similar and reuse it.
113113

114+
### Encoding and decoding JWS
115+
116+
JWS is handled the same way as JWT, but using `encode_jws` and `decode_jws`:
117+
118+
```rust
119+
let encoded = encode_jws(&Header::default(), &my_claims, &EncodingKey::from_secret("secret".as_ref()))?;
120+
my_claims = decode_jws(&encoded, &DecodingKey::from_secret("secret".as_ref()), &Validation::default())?.claims;
121+
```
122+
123+
`encode_jws` returns a `Jws<C>` struct which can be placed in other structs or serialized/deserialized from JSON directly.
124+
125+
The generic parameter in `Jws<C>` indicates the claims type and prevents accidentally encoding or decoding the wrong claims type
126+
when the Jws is nested in another struct.
127+
128+
### JWK Thumbprints
129+
130+
If you have a JWK object, you can generate a thumbprint like
131+
132+
```
133+
let tp = my_jwk.thumbprint(&jsonwebtoken::DIGEST_SHA256);
134+
```
135+
114136
### Decoding
115137

116138
```rust

src/decoding.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -332,14 +332,13 @@ pub fn decode_header(token: impl AsRef<[u8]>) -> Result<Header> {
332332
Header::from_encoded(header)
333333
}
334334

335-
/// Verify the signature of a JWT, and return a header object and raw payload.
336-
///
337-
/// If the token or its signature is invalid, it will return an error.
338-
fn verify_signature<'a>(
339-
token: &'a [u8],
335+
pub(crate) fn verify_signature_body(
336+
message: &[u8],
337+
signature: &[u8],
338+
header: &Header,
340339
validation: &Validation,
341340
verifying_provider: Box<dyn JwtVerifier>,
342-
) -> Result<(Header, &'a [u8])> {
341+
) -> Result<()> {
343342
if validation.validate_signature && validation.algorithms.is_empty() {
344343
return Err(new_error(ErrorKind::MissingAlgorithm));
345344
}
@@ -352,10 +351,6 @@ fn verify_signature<'a>(
352351
}
353352
}
354353

355-
let (signature, message) = expect_two!(token.rsplitn(2, |b| *b == b'.'));
356-
let (payload, header) = expect_two!(message.rsplitn(2, |b| *b == b'.'));
357-
let header = Header::from_encoded(header)?;
358-
359354
if validation.validate_signature && !validation.algorithms.contains(&header.alg) {
360355
return Err(new_error(ErrorKind::InvalidAlgorithm));
361356
}
@@ -366,5 +361,21 @@ fn verify_signature<'a>(
366361
return Err(new_error(ErrorKind::InvalidSignature));
367362
}
368363

364+
Ok(())
365+
}
366+
367+
/// Verify the signature of a JWT, and return a header object and raw payload.
368+
///
369+
/// If the token or its signature is invalid, it will return an error.
370+
fn verify_signature<'a>(
371+
token: &'a [u8],
372+
validation: &Validation,
373+
verifying_provider: Box<dyn JwtVerifier>,
374+
) -> Result<(Header, &'a [u8])> {
375+
let (signature, message) = expect_two!(token.rsplitn(2, |b| *b == b'.'));
376+
let (payload, header) = expect_two!(message.rsplitn(2, |b| *b == b'.'));
377+
let header = Header::from_encoded(header)?;
378+
verify_signature_body(message, signature, &header, validation, verifying_provider)?;
379+
369380
Ok((header, payload))
370381
}

src/encoding.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::fmt::{Debug, Formatter};
22

3-
use base64::{engine::general_purpose::STANDARD, Engine};
3+
use base64::{
4+
engine::general_purpose::{STANDARD, URL_SAFE},
5+
Engine,
6+
};
47
use serde::ser::Serialize;
58

69
use crate::algorithms::AlgorithmFamily;
@@ -36,7 +39,7 @@ use crate::crypto::rust_crypto::{
3639
#[derive(Clone)]
3740
pub struct EncodingKey {
3841
pub(crate) family: AlgorithmFamily,
39-
content: Vec<u8>,
42+
pub(crate) content: Vec<u8>,
4043
}
4144

4245
impl EncodingKey {
@@ -56,6 +59,12 @@ impl EncodingKey {
5659
Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out })
5760
}
5861

62+
/// For loading websafe base64 HMAC secrets, ex: ACME EAB credentials.
63+
pub fn from_urlsafe_base64_secret(secret: &str) -> Result<Self> {
64+
let out = URL_SAFE.decode(secret)?;
65+
Ok(EncodingKey { family: AlgorithmFamily::Hmac, content: out })
66+
}
67+
5968
/// If you are loading a RSA key from a .pem file.
6069
/// This errors if the key is not a valid RSA key.
6170
/// Only exists if the feature `use_pem` is enabled.

src/header.rs

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,110 @@ use std::collections::HashMap;
22
use std::result;
33

44
use base64::{engine::general_purpose::STANDARD, Engine};
5-
use serde::{Deserialize, Serialize};
5+
use serde::{Deserialize, Deserializer, Serialize, Serializer};
66

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

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

0 commit comments

Comments
 (0)