gph
gph

Reputation: 490

How to convert JSON Web Key Set (JWKS) public keys to PEM file using BASH?

Let's say you use AWS and you want to use ID based authentication using Cognito.

Then aws provides you with a public key you can verify the cognito payload with.

Let's also assume you don't want or cannot use any fancy libraries like jose since you are locked in a highly constrained environment.

The way to go is a BASH script that would make good old Brian Kernighan proud. You have to understand the encoding first. Base64Url needs to be translated to Base64.

This is achieved using padding characters = If the number of characters is divisible by 4 you don't need padding. This relates to the binary digit representation. Once that is taken care of you can translate Base64 to binary.

But how do I convert a JWKS/JWT to a PEM file this using BASH and BASH-programs only?

Upvotes: 3

Views: 3315

Answers (2)

gph
gph

Reputation: 490

This is the solution I came up with following the officual documentation and sources listed below.

https://aws.amazon.com/premiumsupport/knowledge-center/decode-verify-cognito-json-token/

Please adapt input url or use json token directly and make sure you have jq installed.

Descriptive help is given in the function as comments.

Tested successfully on Ubuntu 18.04 and AmazonLinux2 (CentOS) as of 2020-04-28

#!/usr/bin/env bash
set -e

# FUNCTIONS
decodeBase64UrlUInt() { #input:base64UrlUnsignedInteger
    local binaryDigits paddedStr
    case $(( ${#1} % 4 )) in
        2) paddedStr="$1=="   ;;
        3) paddedStr="$1="    ;;
        *) paddedStr="$1"     ;;
    esac
    binaryDigits=$(             \
        echo -n "$paddedStr"    \
        | tr '_-' '/+'          \
        | openssl enc -d -a -A  \
        | xxd -b -g 0           \
        | cut -d ' ' -f 2       \
        | paste -s -d ''        \
    )
    echo "ibase=2; obase=A; $binaryDigits" | bc
    # openssl   enc:encoding; -d=decrypt; -a=-base64; -A=singleLineBuffer
    # xxd       "make-hexdump": -b=bits; -g=groupsize
    # cut       -d=delimiter; -f=field
    # paste     -s=serial|singleFile; -d=delimiter
}

base64UrlToHex() { #input:base64UrlString
    local hexStr paddedStr
    case $(( ${#1} % 4 )) in
        2) paddedStr="$1=="   ;;
        3) paddedStr="$1="    ;;
        *) paddedStr="$1"     ;;
    esac
    hexStr=$(                   \
        echo -n "$paddedStr"    \
        | tr '_-' '/+'          \
        | base64 -d             \
        | xxd -p -u             \
        | tr -d '\n'            \
    )
    echo "$hexStr"
    # base64    -d=decode
    # xxd       -p=-plain=continuousHexDump; -u=upperCase
    # tr        -d=delete
}

asn1Conf() { #input:hexStrPlainUpperCase
    local e="$1"
    local n="$2"
    echo "
        asn1 = SEQUENCE:pubkeyinfo
        [pubkeyinfo]
        algorithm = SEQUENCE:rsa_alg
        pubkey = BITWRAP,SEQUENCE:rsapubkey
        [rsa_alg]
        algorithm = OID:rsaEncryption
        parameter = NULL
        [rsapubkey]
        n = INTEGER:0x$n
        e = INTEGER:0x$e
    " | sed '/^$/d ; s/^ *//g'              \
    | openssl asn1parse                     \
        -genconf    /dev/stdin              \
        -out        /dev/stdout             \
    | openssl rsa                           \
        -pubin                              \
            -inform     DER                 \
            -outform    PEM                 \
            -in         /dev/stdin          \
            -out        /dev/
    # sed       /^$/d=removeEmptyLines; /^ */=removeLeadingSpaces
}

main() {
    local e n hexArr
    local jwksUrl="$1"
    local jwkJson=$(curl -sSSL $jwksUrl)
    local kidList=$(jq -r '.keys[].kid' <<< "$jwkJson")
    for keyId in $kidList; do
        n=$(jq -r ".keys[] | select(.kid == \"$keyId\") | .n" <<< "$jwkJson")
        e=$(jq -r ".keys[] | select(.kid == \"$keyId\") | .e" <<< "$jwkJson")
        echo -e "\n$keyId"
        # decodeBase64UrlUInt "$e"
        # decodeBase64UrlUInt "$n"
        asn1Conf $(base64UrlToHex "$e") $(base64UrlToHex "$n")
    done
}

# MAIN
main 'https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json'
exit 0

Special thanks to:

Yury Oparin https://www.yuryoparin.com/2014/05/base64url-in-bash.html

Cédric Deltheil https://github.com/Moodstocks/moodstocks-api-clients/blob/master/bash/base64url.sh

Alvis Tang https://gist.github.com/alvis/89007e96f7958f2686036d4276d28e47

Upvotes: 2

L&#233;a Gris
L&#233;a Gris

Reputation: 19625

Here are some options:

Either ignore base64 -d complaining of truncated input:

<<<'SGVsbG8geW91Cg' base64 -d 2>/dev/null ||:

Or fix the base64 padding with Bash before decoding:

base64URL='SGVsbG8geW91Cg'

printf -v pad_space '%*s' $((${#base64URL}%4)) ''

padded_base64="$base64URL${pad_space// /=}"

<<<"$padded_base64" base64 -d

Upvotes: 0

Related Questions