diff --git a/dist/jwt/bin/jwt b/dist/jwt/bin/jwt index 8a353a7..7dd8c54 100755 --- a/dist/jwt/bin/jwt +++ b/dist/jwt/bin/jwt @@ -178,7 +178,6 @@ base64url_to_base64() { local remainder=$((${#output} % 4)) case $remainder in - 1) printf '%s===' "$output" ;; 2) printf '%s==' "$output" ;; 3) printf '%s=' "$output" ;; *) printf '%s' "$output" ;; @@ -255,15 +254,18 @@ jwt_decode_payload() { # --- end: scripts/jwt/decode.sh --- # --- begin: scripts/jwt/verify.sh --- +# @start-kcov-exclude - only called when version warnings trigger (OpenSSL < 3.x) jwt_warn() { [[ -n "${JWT_QUIET:-}" ]] && return 0 echo "jwt: warning: $1" >&2 return 0 } +# @end-kcov-exclude check_dependencies() { local alg=${1:-} + # @start-kcov-exclude - can't mock PATH to test missing dependencies if ! command -v openssl &>/dev/null; then echo "jwt: error: openssl not found" >&2 return 1 @@ -277,15 +279,18 @@ check_dependencies() { fi ;; esac + # @end-kcov-exclude } get_openssl_major_version() { local version_string version version_string=$(openssl version 2>/dev/null) + # @start-kcov-exclude - only triggers on LibreSSL systems if [[ "$version_string" == LibreSSL* ]]; then echo "0" return fi + # @end-kcov-exclude version=$(echo "$version_string" | awk '{print $2}') echo "${version%%.*}" } @@ -297,10 +302,12 @@ check_algorithm_support() { case $alg in PS256 | PS384 | PS512 | EdDSA) + # @start-kcov-exclude - only triggers on OpenSSL < 3.x or LibreSSL if [[ "$major_version" -lt 3 ]]; then jwt_warn "algorithm '$alg' requires OpenSSL 3.x (found: $(openssl version))" return 1 fi + # @end-kcov-exclude ;; esac return 0 @@ -311,11 +318,7 @@ get_openssl_digest() { HS256 | RS256 | ES256 | PS256) echo "sha256" ;; HS384 | RS384 | ES384 | PS384) echo "sha384" ;; HS512 | RS512 | ES512 | PS512) echo "sha512" ;; - EdDSA) echo "" ;; # EdDSA uses built-in hash - *) - echo "jwt: error: unsupported algorithm '$JWT_ALG'" >&2 - return 1 - ;; + EdDSA) echo "" ;; # @kcov-ignore - EdDSA requires OpenSSL 3.x esac } @@ -367,7 +370,6 @@ jwt_sig_to_der() { 256) r_len=64 ;; # 32 bytes = 64 hex chars 384) r_len=96 ;; # 48 bytes = 96 hex chars 512) r_len=132 ;; # 66 bytes = 132 hex chars (P-521) - *) return 1 ;; esac r_hex=${sig_hex:0:$r_len} @@ -410,11 +412,7 @@ verify_ecdsa() { local sig_hex der_hex sig_hex=$(base64url_decode "$JWT_SIG_B64" | xxd -p | tr -d '\n') - der_hex=$(jwt_sig_to_der "$sig_hex" "$key_bits") || { - echo "jwt: error: failed to convert ECDSA signature" >&2 - rm -f "$sig_file" "$key_file" - return 1 - } + der_hex=$(jwt_sig_to_der "$sig_hex" "$key_bits") echo "$der_hex" | xxd -r -p >"$sig_file" printf '%s\n' "$key" >"$key_file" @@ -537,8 +535,10 @@ if [[ $# -gt 0 ]]; then elif [[ ! -t 0 ]]; then read -r token else + # @start-kcov-exclude - TTY + no args can't be tested in ShellSpec (uses pipes) echo "jwt: error: no token provided" >&2 exit 1 + # @end-kcov-exclude fi jwt_split "$token" || exit $? diff --git a/dist/jwt/man/man1/jwt.1 b/dist/jwt/man/man1/jwt.1 index 4b495e2..8a17d28 100644 --- a/dist/jwt/man/man1/jwt.1 +++ b/dist/jwt/man/man1/jwt.1 @@ -314,17 +314,17 @@ jwt \-\-all eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\|.\|. .sp Verify HMAC signature with secret .RS 4 -jwt \-\-verify=secret123 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\|.\|. +jwt \-k secret123 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\|.\|. .RE .sp Verify RSA signature using public key file .RS 4 -jwt \-\-verify=@/path/to/public.pem eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.\|.\|. +jwt \-\-key=@/path/to/public.pem eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.\|.\|. .RE .sp Verify and show header .RS 4 -jwt \-\-verify=secret123 \-\-header eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\|.\|. +jwt \-k secret123 \-\-header eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\|.\|. .RE .sp Read token from stdin diff --git a/scripts/jwt/decode.sh b/scripts/jwt/decode.sh index e4be28b..f67dc96 100644 --- a/scripts/jwt/decode.sh +++ b/scripts/jwt/decode.sh @@ -11,9 +11,9 @@ base64url_to_base64() { output=${output//_/\/} # Add padding if needed (base64 requires length % 4 == 0) + # Note: remainder=1 is impossible for valid base64 (3 bytes → 4 chars) local remainder=$((${#output} % 4)) case $remainder in - 1) printf '%s===' "$output" ;; 2) printf '%s==' "$output" ;; 3) printf '%s=' "$output" ;; *) printf '%s' "$output" ;; diff --git a/scripts/jwt/docs/jwt.adoc b/scripts/jwt/docs/jwt.adoc index e3e93f7..b21f6aa 100644 --- a/scripts/jwt/docs/jwt.adoc +++ b/scripts/jwt/docs/jwt.adoc @@ -116,15 +116,15 @@ Display all parts as JSON:: Verify HMAC signature with secret:: - jwt --verify=secret123 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + jwt -k secret123 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... Verify RSA signature using public key file:: - jwt --verify=@/path/to/public.pem eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... + jwt --key=@/path/to/public.pem eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... Verify and show header:: - jwt --verify=secret123 --header eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + jwt -k secret123 --header eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... Read token from stdin:: diff --git a/scripts/jwt/main.sh b/scripts/jwt/main.sh index acb3893..5122caf 100644 --- a/scripts/jwt/main.sh +++ b/scripts/jwt/main.sh @@ -56,8 +56,10 @@ elif [[ ! -t 0 ]]; then # Read from stdin read -r token else + # @start-kcov-exclude - TTY + no args can't be tested in ShellSpec (uses pipes) echo "jwt: error: no token provided" >&2 exit 1 + # @end-kcov-exclude fi # Split and decode token diff --git a/scripts/jwt/main_spec.sh b/scripts/jwt/main_spec.sh index 8145132..d5710d7 100644 --- a/scripts/jwt/main_spec.sh +++ b/scripts/jwt/main_spec.sh @@ -410,6 +410,32 @@ Describe 'jwt' The stderr should include "invalid" End + It 'rejects token with empty header part' + When run script "$BIN" ".eyJzdWIiOiJ0ZXN0In0.sig" + The status should be failure + The stderr should include "invalid JWT format" + End + + It 'rejects token with empty payload part' + When run script "$BIN" "eyJhbGciOiJIUzI1NiJ9..sig" + The status should be failure + The stderr should include "invalid JWT format" + End + + It 'rejects token with empty signature part' + When run script "$BIN" "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0." + The status should be failure + The stderr should include "invalid JWT format" + End + + It 'rejects invalid base64 in payload' + # Valid header: {"alg":"HS256"} = eyJhbGciOiJIUzI1NiJ9 + # Invalid base64 in payload + When run script "$BIN" "eyJhbGciOiJIUzI1NiJ9.!!invalid!!.sig" + The status should be failure + The stderr should include "failed to decode payload" + End + It 'rejects unsupported algorithm during verification' # {"alg":"XX99","typ":"JWT"} = eyJhbGciOiJYWDk5IiwidHlwIjoiSldUIn0 # {"sub":"test"} = eyJzdWIiOiJ0ZXN0In0 diff --git a/scripts/jwt/verify.sh b/scripts/jwt/verify.sh index 65935c8..521782b 100644 --- a/scripts/jwt/verify.sh +++ b/scripts/jwt/verify.sh @@ -2,17 +2,20 @@ # JWT verification functions # Warning helper +# @start-kcov-exclude - only called when version warnings trigger (OpenSSL < 3.x) jwt_warn() { [[ -n "${JWT_QUIET:-}" ]] && return 0 echo "jwt: warning: $1" >&2 return 0 } +# @end-kcov-exclude # Check required dependencies are available # $1: algorithm (optional) - if ECDSA, also checks for xxd check_dependencies() { local alg=${1:-} + # @start-kcov-exclude - can't mock PATH to test missing dependencies if ! command -v openssl &>/dev/null; then echo "jwt: error: openssl not found" >&2 return 1 @@ -27,6 +30,7 @@ check_dependencies() { fi ;; esac + # @end-kcov-exclude } # Get OpenSSL major version number @@ -35,10 +39,12 @@ get_openssl_major_version() { local version_string version version_string=$(openssl version 2>/dev/null) # LibreSSL returns "LibreSSL x.y.z" - not compatible with PS/EdDSA + # @start-kcov-exclude - only triggers on LibreSSL systems if [[ "$version_string" == LibreSSL* ]]; then echo "0" return fi + # @end-kcov-exclude version=$(echo "$version_string" | awk '{print $2}') echo "${version%%.*}" } @@ -53,10 +59,12 @@ check_algorithm_support() { case $alg in PS256 | PS384 | PS512 | EdDSA) # These require OpenSSL 3.x + # @start-kcov-exclude - only triggers on OpenSSL < 3.x or LibreSSL if [[ "$major_version" -lt 3 ]]; then jwt_warn "algorithm '$alg' requires OpenSSL 3.x (found: $(openssl version))" return 1 fi + # @end-kcov-exclude ;; esac return 0 @@ -66,15 +74,13 @@ check_algorithm_support() { # Uses: JWT_ALG # Returns empty string for EdDSA (no separate digest step) get_openssl_digest() { + # Note: No default case needed - verify_signature validates algorithm + # before calling verify_* functions that call this case $JWT_ALG in HS256 | RS256 | ES256 | PS256) echo "sha256" ;; HS384 | RS384 | ES384 | PS384) echo "sha384" ;; HS512 | RS512 | ES512 | PS512) echo "sha512" ;; - EdDSA) echo "" ;; # EdDSA uses built-in hash - *) - echo "jwt: error: unsupported algorithm '$JWT_ALG'" >&2 - return 1 - ;; + EdDSA) echo "" ;; # @kcov-ignore - EdDSA requires OpenSSL 3.x esac } @@ -137,11 +143,11 @@ jwt_sig_to_der() { local r_len r_hex s_hex # R and S are each half the signature + # Note: No default case needed - only called with valid key_bits from verify_ecdsa case $key_bits in 256) r_len=64 ;; # 32 bytes = 64 hex chars 384) r_len=96 ;; # 48 bytes = 96 hex chars 512) r_len=132 ;; # 66 bytes = 132 hex chars (P-521) - *) return 1 ;; esac r_hex=${sig_hex:0:$r_len} @@ -197,11 +203,7 @@ verify_ecdsa() { # Decode signature to hex, convert to DER local sig_hex der_hex sig_hex=$(base64url_decode "$JWT_SIG_B64" | xxd -p | tr -d '\n') - der_hex=$(jwt_sig_to_der "$sig_hex" "$key_bits") || { - echo "jwt: error: failed to convert ECDSA signature" >&2 - rm -f "$sig_file" "$key_file" - return 1 - } + der_hex=$(jwt_sig_to_der "$sig_hex" "$key_bits") # Write DER signature as binary and key to temp files echo "$der_hex" | xxd -r -p >"$sig_file"