Skip to content

Commit 58f6124

Browse files
committed
feat: Require list of expected algorithms when secret/publicKey is given
Instead of allowing only one single algorithm during signature validation, one can now specify a comma-separated list of algorithms using the `--algs` command line parameter. In order to encourage users to be aware of the choice of algorithms and safely define a subset of the supported algorithms, the `--algs` parameter is now required when the `-S` parameter is set.
1 parent 783c91a commit 58f6124

File tree

2 files changed

+218
-28
lines changed

2 files changed

+218
-28
lines changed

src/main.rs

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,13 @@ fn config_options<'a, 'b>() -> App<'a, 'b> {
246246
.index(1)
247247
.required(true),
248248
).arg(
249-
Arg::with_name("algorithm")
250-
.help("the algorithm to use for signing the JWT")
249+
Arg::with_name("algorithms")
250+
.help("a comma-separated list of algorithms to be used for signature validation. All algorithms need to be of the same family (HMAC, RSA, EC).")
251+
.require_delimiter(true)
251252
.takes_value(true)
252-
.long("alg")
253+
.long("algs")
253254
.short("A")
254255
.possible_values(&SupportedAlgorithms::variants())
255-
.default_value("HS256"),
256256
).arg(
257257
Arg::with_name("iso_dates")
258258
.help("display unix timestamps as ISO 8601 dates")
@@ -264,7 +264,7 @@ fn config_options<'a, 'b>() -> App<'a, 'b> {
264264
.takes_value(true)
265265
.long("secret")
266266
.short("S")
267-
.default_value(""),
267+
.requires("algorithms")
268268
).arg(
269269
Arg::with_name("json")
270270
.help("render decoded JWT as JSON")
@@ -465,13 +465,6 @@ fn decode_token(
465465
JWTResult<TokenData<Payload>>,
466466
OutputFormat,
467467
) {
468-
let algorithm = translate_algorithm(SupportedAlgorithms::from_string(
469-
matches.value_of("algorithm").unwrap(),
470-
));
471-
let secret = match matches.value_of("secret").map(|s| (s, !s.is_empty())) {
472-
Some((secret, true)) => Some(decoding_key_from_secret(&algorithm, &secret)),
473-
_ => None,
474-
};
475468
let jwt = matches
476469
.value_of("jwt")
477470
.map(|value| {
@@ -491,13 +484,7 @@ fn decode_token(
491484
.trim()
492485
.to_owned();
493486

494-
let secret_validator = Validation {
495-
leeway: 1000,
496-
algorithms: vec![algorithm],
497-
validate_exp: !matches.is_present("ignore_exp"),
498-
..Default::default()
499-
};
500-
487+
// decode token without signature verification
501488
let token_data = dangerous_insecure_decode::<Payload>(&jwt).map(|mut token| {
502489
if matches.is_present("iso_dates") {
503490
token.claims.convert_timestamps();
@@ -506,6 +493,31 @@ fn decode_token(
506493
token
507494
});
508495

496+
// get vector of allowed algorithms from command line argument
497+
let algorithms: Vec<Algorithm> = match matches.values_of("algorithms") {
498+
Some(algorithms) => algorithms
499+
.map(|x| translate_algorithm(SupportedAlgorithms::from_string(x)))
500+
.collect(),
501+
None => vec![],
502+
};
503+
504+
let secret_validator = Validation {
505+
leeway: 1000,
506+
algorithms: algorithms,
507+
validate_exp: !matches.is_present("ignore_exp"),
508+
..Default::default()
509+
};
510+
511+
// get the shared secret/public key to be used for signature validation
512+
let secret = match matches.value_of("secret").map(|s| (s, !s.is_empty())) {
513+
Some((secret, true)) => Some(decoding_key_from_secret(
514+
&token_data.as_ref().unwrap().header.alg, // decode key according to algorithm used in the JWT
515+
&secret,
516+
)),
517+
_ => None,
518+
};
519+
520+
// return validated token, non-validated token data and output format
509521
(
510522
match secret {
511523
Some(secret_key) => decode::<Payload>(&jwt, &secret_key.unwrap(), &secret_validator),

tests/jwt-cli.rs

Lines changed: 187 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,15 @@ mod tests {
223223
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
224224
let encoded_token = encode_token(&encode_matches).unwrap();
225225
let decode_matcher = config_options()
226-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
226+
.get_matches_from_safe(vec![
227+
"jwt",
228+
"decode",
229+
"-S",
230+
"1234567890",
231+
"-A",
232+
"HS256",
233+
&encoded_token,
234+
])
227235
.unwrap();
228236
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
229237
let (decoded_token, _, _) = decode_token(&decode_matches);
@@ -257,7 +265,15 @@ mod tests {
257265
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
258266
let encoded_token = encode_token(&encode_matches).unwrap();
259267
let decode_matcher = config_options()
260-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
268+
.get_matches_from_safe(vec![
269+
"jwt",
270+
"decode",
271+
"-S",
272+
"1234567890",
273+
"-A",
274+
"HS256",
275+
&encoded_token,
276+
])
261277
.unwrap();
262278
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
263279
let (decoded_token, _, _) = decode_token(&decode_matches);
@@ -279,7 +295,15 @@ mod tests {
279295
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
280296
let encoded_token = encode_token(&encode_matches).unwrap();
281297
let decode_matcher = config_options()
282-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
298+
.get_matches_from_safe(vec![
299+
"jwt",
300+
"decode",
301+
"-S",
302+
"1234567890",
303+
"-A",
304+
"HS256",
305+
&encoded_token,
306+
])
283307
.unwrap();
284308
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
285309
let (decoded_token, token_data, _) = decode_token(&decode_matches);
@@ -299,7 +323,15 @@ mod tests {
299323
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
300324
let encoded_token = encode_token(&encode_matches).unwrap();
301325
let decode_matcher = config_options()
302-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
326+
.get_matches_from_safe(vec![
327+
"jwt",
328+
"decode",
329+
"-S",
330+
"1234567890",
331+
"-A",
332+
"HS256",
333+
&encoded_token,
334+
])
303335
.unwrap();
304336
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
305337
let (decoded_token, _, _) = decode_token(&decode_matches);
@@ -328,7 +360,15 @@ mod tests {
328360
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
329361
let encoded_token = encode_token(&encode_matches).unwrap();
330362
let decode_matcher = config_options()
331-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
363+
.get_matches_from_safe(vec![
364+
"jwt",
365+
"decode",
366+
"-S",
367+
"1234567890",
368+
"-A",
369+
"HS256",
370+
&encoded_token,
371+
])
332372
.unwrap();
333373
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
334374
let (decoded_token, _, _) = decode_token(&decode_matches);
@@ -356,7 +396,15 @@ mod tests {
356396
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
357397
let encoded_token = encode_token(&encode_matches).unwrap();
358398
let decode_matcher = config_options()
359-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
399+
.get_matches_from_safe(vec![
400+
"jwt",
401+
"decode",
402+
"-S",
403+
"1234567890",
404+
"-A",
405+
"HS256",
406+
&encoded_token,
407+
])
360408
.unwrap();
361409
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
362410
let (decoded_token, _, _) = decode_token(&decode_matches);
@@ -378,7 +426,15 @@ mod tests {
378426
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
379427
let encoded_token = encode_token(&encode_matches).unwrap();
380428
let decode_matcher = config_options()
381-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
429+
.get_matches_from_safe(vec![
430+
"jwt",
431+
"decode",
432+
"-S",
433+
"1234567890",
434+
"-A",
435+
"HS512",
436+
&encoded_token,
437+
])
382438
.unwrap();
383439
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
384440
let (decoded_token, _, _) = decode_token(&decode_matches);
@@ -399,6 +455,8 @@ mod tests {
399455
"decode",
400456
"-S",
401457
"1234567890",
458+
"-A",
459+
"HS256",
402460
"--ignore-exp",
403461
&encoded_token,
404462
])
@@ -424,7 +482,15 @@ mod tests {
424482
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
425483
let encoded_token = encode_token(&encode_matches).unwrap();
426484
let decode_matcher = config_options()
427-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
485+
.get_matches_from_safe(vec![
486+
"jwt",
487+
"decode",
488+
"-S",
489+
"1234567890",
490+
"-A",
491+
"HS256",
492+
&encoded_token,
493+
])
428494
.unwrap();
429495
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
430496
let (decoded_token, _, _) = decode_token(&decode_matches);
@@ -460,7 +526,15 @@ mod tests {
460526
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
461527
let encoded_token = encode_token(&encode_matches).unwrap();
462528
let decode_matcher = config_options()
463-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
529+
.get_matches_from_safe(vec![
530+
"jwt",
531+
"decode",
532+
"-S",
533+
"1234567890",
534+
"-A",
535+
"HS256",
536+
&encoded_token,
537+
])
464538
.unwrap();
465539
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
466540
let (decoded_token, _, _) = decode_token(&decode_matches);
@@ -597,6 +671,108 @@ mod tests {
597671
assert!(result.is_ok());
598672
}
599673

674+
#[test]
675+
fn encodes_and_decodes_a_token_with_multiple_algorithms() {
676+
let body: String = "{\"field\":\"value\"}".to_string();
677+
let encode_matcher = config_options()
678+
.get_matches_from_safe(vec![
679+
"jwt",
680+
"encode",
681+
"-A",
682+
"HS256",
683+
"--exp",
684+
"-S",
685+
"1234567890",
686+
&body,
687+
])
688+
.unwrap();
689+
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
690+
let encoded_token = encode_token(&encode_matches).unwrap();
691+
let decode_matcher = config_options()
692+
.get_matches_from_safe(vec![
693+
"jwt",
694+
"decode",
695+
"-S",
696+
"1234567890",
697+
"-A",
698+
"HS256,HS384,HS512",
699+
&encoded_token,
700+
])
701+
.unwrap();
702+
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
703+
let (result, _, _) = decode_token(&decode_matches);
704+
705+
assert!(result.is_ok());
706+
}
707+
708+
#[test]
709+
fn encodes_and_decodes_a_token_with_invalid_algorithms_family() {
710+
let body: String = "{\"field\":\"value\"}".to_string();
711+
let encode_matcher = config_options()
712+
.get_matches_from_safe(vec![
713+
"jwt",
714+
"encode",
715+
"-A",
716+
"HS256",
717+
"--exp",
718+
"-S",
719+
"1234567890",
720+
&body,
721+
])
722+
.unwrap();
723+
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
724+
let encoded_token = encode_token(&encode_matches).unwrap();
725+
let decode_matcher = config_options()
726+
.get_matches_from_safe(vec![
727+
"jwt",
728+
"decode",
729+
"-S",
730+
"1234567890",
731+
"-A",
732+
"RS256,RS384,RS512", // invalid algorithm family
733+
&encoded_token,
734+
])
735+
.unwrap();
736+
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
737+
let (result, _, _) = decode_token(&decode_matches);
738+
739+
assert!(result.is_err());
740+
}
741+
742+
#[test]
743+
fn encodes_and_decodes_a_token_with_mixed_algorithms_family() {
744+
let body: String = "{\"field\":\"value\"}".to_string();
745+
let encode_matcher = config_options()
746+
.get_matches_from_safe(vec![
747+
"jwt",
748+
"encode",
749+
"-A",
750+
"HS256",
751+
"--exp",
752+
"-S",
753+
"1234567890",
754+
&body,
755+
])
756+
.unwrap();
757+
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
758+
let encoded_token = encode_token(&encode_matches).unwrap();
759+
let decode_matcher = config_options()
760+
.get_matches_from_safe(vec![
761+
"jwt",
762+
"decode",
763+
"-S",
764+
"1234567890",
765+
"-A",
766+
"HS256,RS512", // algorithms from incompatible algorithm families
767+
&encoded_token,
768+
])
769+
.unwrap();
770+
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
771+
let (result, _, _) = decode_token(&decode_matches);
772+
773+
assert!(result.is_err());
774+
}
775+
600776
#[test]
601777
fn encodes_and_decodes_an_rsa_token_using_key_from_file() {
602778
let body: String = "{\"field\":\"value\"}".to_string();
@@ -705,6 +881,8 @@ mod tests {
705881
"decode",
706882
"-S",
707883
"1234567890",
884+
"-A",
885+
"HS256",
708886
"--iso8601",
709887
&encoded_token,
710888
])

0 commit comments

Comments
 (0)