Skip to content

Commit cff04ea

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 0de1342 commit cff04ea

File tree

2 files changed

+204
-23
lines changed

2 files changed

+204
-23
lines changed

src/main.rs

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -252,13 +252,13 @@ fn config_options<'a, 'b>() -> App<'a, 'b> {
252252
.index(1)
253253
.required(true),
254254
).arg(
255-
Arg::with_name("algorithm")
256-
.help("the algorithm to use for signing the JWT")
255+
Arg::with_name("algorithms")
256+
.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).")
257+
.require_delimiter(true)
257258
.takes_value(true)
258-
.long("alg")
259+
.long("algs")
259260
.short("A")
260261
.possible_values(&SupportedAlgorithms::variants())
261-
.default_value("HS256"),
262262
).arg(
263263
Arg::with_name("iso_dates")
264264
.help("display unix timestamps as ISO 8601 dates")
@@ -270,7 +270,7 @@ fn config_options<'a, 'b>() -> App<'a, 'b> {
270270
.takes_value(true)
271271
.long("secret")
272272
.short("S")
273-
.default_value(""),
273+
.requires("algorithms")
274274
).arg(
275275
Arg::with_name("json")
276276
.help("render decoded JWT as JSON")
@@ -330,10 +330,10 @@ fn create_header(alg: Algorithm, kid: Option<&str>) -> Header {
330330
header
331331
}
332332

333-
fn create_validations(alg: Algorithm) -> Validation {
333+
fn create_validations(algs: Vec<Algorithm>) -> Validation {
334334
Validation {
335335
leeway: 1000,
336-
algorithms: vec![alg],
336+
algorithms: algs,
337337
..Default::default()
338338
}
339339
}
@@ -469,13 +469,6 @@ fn decode_token(
469469
JWTResult<TokenData<Payload>>,
470470
OutputFormat,
471471
) {
472-
let algorithm = translate_algorithm(SupportedAlgorithms::from_string(
473-
matches.value_of("algorithm").unwrap(),
474-
));
475-
let secret = match matches.value_of("secret").map(|s| (s, !s.is_empty())) {
476-
Some((secret, true)) => Some(decoding_key_from_secret(&algorithm, &secret)),
477-
_ => None,
478-
};
479472
let jwt = matches
480473
.value_of("jwt")
481474
.map(|value| {
@@ -495,7 +488,7 @@ fn decode_token(
495488
.trim()
496489
.to_owned();
497490

498-
let secret_validator = create_validations(algorithm);
491+
// decode token without signature verification
499492
let token_data = dangerous_insecure_decode::<Payload>(&jwt).map(|mut token| {
500493
if matches.is_present("iso_dates") {
501494
token.claims.convert_timestamps();
@@ -504,6 +497,26 @@ fn decode_token(
504497
token
505498
});
506499

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

tests/jwt-cli.rs

Lines changed: 176 additions & 8 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);
@@ -385,7 +433,15 @@ mod tests {
385433
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
386434
let encoded_token = encode_token(&encode_matches).unwrap();
387435
let decode_matcher = config_options()
388-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
436+
.get_matches_from_safe(vec![
437+
"jwt",
438+
"decode",
439+
"-S",
440+
"1234567890",
441+
"-A",
442+
"HS256",
443+
&encoded_token,
444+
])
389445
.unwrap();
390446
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
391447
let (decoded_token, _, _) = decode_token(&decode_matches);
@@ -421,7 +477,15 @@ mod tests {
421477
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
422478
let encoded_token = encode_token(&encode_matches).unwrap();
423479
let decode_matcher = config_options()
424-
.get_matches_from_safe(vec!["jwt", "decode", "-S", "1234567890", &encoded_token])
480+
.get_matches_from_safe(vec![
481+
"jwt",
482+
"decode",
483+
"-S",
484+
"1234567890",
485+
"-A",
486+
"HS256",
487+
&encoded_token,
488+
])
425489
.unwrap();
426490
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
427491
let (decoded_token, _, _) = decode_token(&decode_matches);
@@ -558,6 +622,108 @@ mod tests {
558622
assert!(result.is_ok());
559623
}
560624

625+
#[test]
626+
fn encodes_and_decodes_a_token_with_multiple_algorithms() {
627+
let body: String = "{\"field\":\"value\"}".to_string();
628+
let encode_matcher = config_options()
629+
.get_matches_from_safe(vec![
630+
"jwt",
631+
"encode",
632+
"-A",
633+
"HS256",
634+
"--exp",
635+
"-S",
636+
"1234567890",
637+
&body,
638+
])
639+
.unwrap();
640+
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
641+
let encoded_token = encode_token(&encode_matches).unwrap();
642+
let decode_matcher = config_options()
643+
.get_matches_from_safe(vec![
644+
"jwt",
645+
"decode",
646+
"-S",
647+
"1234567890",
648+
"-A",
649+
"HS256,HS384,HS512",
650+
&encoded_token,
651+
])
652+
.unwrap();
653+
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
654+
let (result, _, _) = decode_token(&decode_matches);
655+
656+
assert!(result.is_ok());
657+
}
658+
659+
#[test]
660+
fn encodes_and_decodes_a_token_with_invalid_algorithms_family() {
661+
let body: String = "{\"field\":\"value\"}".to_string();
662+
let encode_matcher = config_options()
663+
.get_matches_from_safe(vec![
664+
"jwt",
665+
"encode",
666+
"-A",
667+
"HS256",
668+
"--exp",
669+
"-S",
670+
"1234567890",
671+
&body,
672+
])
673+
.unwrap();
674+
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
675+
let encoded_token = encode_token(&encode_matches).unwrap();
676+
let decode_matcher = config_options()
677+
.get_matches_from_safe(vec![
678+
"jwt",
679+
"decode",
680+
"-S",
681+
"1234567890",
682+
"-A",
683+
"RS256,RS384,RS512", // invalid algorithm family
684+
&encoded_token,
685+
])
686+
.unwrap();
687+
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
688+
let (result, _, _) = decode_token(&decode_matches);
689+
690+
assert!(result.is_err());
691+
}
692+
693+
#[test]
694+
fn encodes_and_decodes_a_token_with_mixed_algorithms_family() {
695+
let body: String = "{\"field\":\"value\"}".to_string();
696+
let encode_matcher = config_options()
697+
.get_matches_from_safe(vec![
698+
"jwt",
699+
"encode",
700+
"-A",
701+
"HS256",
702+
"--exp",
703+
"-S",
704+
"1234567890",
705+
&body,
706+
])
707+
.unwrap();
708+
let encode_matches = encode_matcher.subcommand_matches("encode").unwrap();
709+
let encoded_token = encode_token(&encode_matches).unwrap();
710+
let decode_matcher = config_options()
711+
.get_matches_from_safe(vec![
712+
"jwt",
713+
"decode",
714+
"-S",
715+
"1234567890",
716+
"-A",
717+
"HS256,RS512", // algorithms from incompatible algorithm families
718+
&encoded_token,
719+
])
720+
.unwrap();
721+
let decode_matches = decode_matcher.subcommand_matches("decode").unwrap();
722+
let (result, _, _) = decode_token(&decode_matches);
723+
724+
assert!(result.is_err());
725+
}
726+
561727
#[test]
562728
fn encodes_and_decodes_an_rsa_token_using_key_from_file() {
563729
let body: String = "{\"field\":\"value\"}".to_string();
@@ -652,6 +818,8 @@ mod tests {
652818
"decode",
653819
"-S",
654820
"1234567890",
821+
"-A",
822+
"HS256",
655823
"--iso8601",
656824
&encoded_token,
657825
])

0 commit comments

Comments
 (0)