Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions dist/jwt/bin/jwt
Original file line number Diff line number Diff line change
Expand Up @@ -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" ;;
Expand Down Expand Up @@ -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
Expand All @@ -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%%.*}"
}
Expand All @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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 $?
Expand Down
6 changes: 3 additions & 3 deletions dist/jwt/man/man1/jwt.1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/jwt/decode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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" ;;
Expand Down
6 changes: 3 additions & 3 deletions scripts/jwt/docs/jwt.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down
2 changes: 2 additions & 0 deletions scripts/jwt/main.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
26 changes: 26 additions & 0 deletions scripts/jwt/main_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 13 additions & 11 deletions scripts/jwt/verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +30,7 @@ check_dependencies() {
fi
;;
esac
# @end-kcov-exclude
}

# Get OpenSSL major version number
Expand All @@ -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%%.*}"
}
Expand All @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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"
Expand Down