diff --git a/dev_utils/certfixer/make_certs.sh b/dev_utils/certfixer/make_certs.sh index 88f4e14..d9c0d91 100644 --- a/dev_utils/certfixer/make_certs.sh +++ b/dev_utils/certfixer/make_certs.sh @@ -5,8 +5,7 @@ set -e out_dir="/cert_gen" # install openssl if it's missing -if [ ! "$(command -v openssl)" ]; -then +if [ ! "$(command -v openssl)" ]; then apk add openssl fi @@ -18,24 +17,22 @@ s3_certs="/s3_certs/CAs/public.crt /s3_certs/public.crt /s3_certs/private.key" mq_certs="/mq_certs/ca.crt /mq_certs/mq.crt /mq_certs/mq.key" pub_cert="/pubcert/public.crt" proxy_certs="/proxy_certs/ca.crt /proxy_certs/client.crt /proxy_certs/client.key /proxy_certs/proxy.crt /proxy_certs/proxy.key" -targets="$s3_certs $mq_certs $pub_cert $proxy_certs" +keys="/keys/jwt.key /keys/jwt.pub" +targets="$s3_certs $mq_certs $pub_cert $proxy_certs $keys" echo "" echo "Checking certificates" recreate="false" # check if certificates exist -for target in $targets -do - if [ ! -f "$target" ] - then +for target in $targets; do + if [ ! -f "$target" ]; then recreate="true" break fi done # only recreate certificates if any certificate is missing -if [ "$recreate" = "false" ] -then +if [ "$recreate" = "false" ]; then echo "certificates already exists" exit 0 fi @@ -60,6 +57,10 @@ openssl x509 -req -in "$out_dir/s3.csr" -days 1200 -CA "$out_dir/ca.crt" -CAkey openssl req -config "$script_dir/ssl.cnf" -new -nodes -newkey rsa:4096 -keyout "$out_dir/client.key" -out "$out_dir/client.csr" -extensions client_cert -subj "/CN=admin" openssl x509 -req -in "$out_dir/client.csr" -days 1200 -CA "$out_dir/ca.crt" -CAkey "$out_dir/ca-key.pem" -set_serial 01 -out "$out_dir/client.crt" -extensions client_cert -extfile "$script_dir/ssl.cnf" +# create EC256 key for signing the JWT tokens +openssl ecparam -genkey -name prime256v1 -noout -out $out_dir/jwt.key +openssl ec -in $out_dir/jwt.key -outform PEM -pubout >$out_dir/jwt.pub + # fix permissions chmod 644 "$out_dir"/* chown -R root:root "$out_dir"/* @@ -82,3 +83,5 @@ cp -p "$out_dir/client.crt" /proxy_certs/client.crt cp -p "$out_dir/client.key" /proxy_certs/client.key cp -p "$out_dir/proxy.crt" /proxy_certs/proxy.crt cp -p "$out_dir/proxy.key" /proxy_certs/proxy.key +cp -p "$out_dir/jwt.pub" /keys/sda-sda-svc-auth.pub +cp -p "$out_dir/jwt.key" /keys/jwt.key diff --git a/dev_utils/docker-compose.yml b/dev_utils/docker-compose.yml index 95ed3ed..1d1430a 100644 --- a/dev_utils/docker-compose.yml +++ b/dev_utils/docker-compose.yml @@ -9,6 +9,7 @@ services: - s3_certs:/s3_certs - mq_certs:/mq_certs - proxy_certs:/proxy_certs + - keys:/keys s3: image: minio/minio:RELEASE.2022-09-25T15-44-53Z @@ -119,11 +120,10 @@ services: - SERVER_CERT=/certs/proxy.crt - SERVER_KEY=/certs/proxy.key - SERVER_JWTPUBKEYPATH=/keys/ - - SERVER_JWTPUBEYURL=https://login.elixir-czech.org/oidc/jwk - LOG_FORMAT=json volumes: - proxy_certs:/certs - - ./keys:/keys + - keys:/keys ports: - "8000:8000" - "8001:8001" @@ -163,7 +163,6 @@ services: condition: service_completed_successfully volumes: - proxy_certs:/certs - - ./users.csv:/users.csv - ..:/app integration_tests: @@ -188,10 +187,11 @@ services: condition: service_completed_successfully volumes: - proxy_certs:/certs - - ./users.csv:/users.csv - ..:/app + - keys:/keys volumes: + keys: pubcert: s3_certs: mq_certs: diff --git a/dev_utils/keys/dummy.ega.nbis.se.pub b/dev_utils/keys/dummy.ega.nbis.se.pub deleted file mode 100644 index a3ff940..0000000 --- a/dev_utils/keys/dummy.ega.nbis.se.pub +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwKQEKxpNhaMCtyln5cre -7rLyj5H5Rl3cldZFGiPT1clR5dZrRQ+WDIfbHBPYLG7esa9ExZ82D1mUvPshU79Y -ZfAZChhteijVe4/jpCyFtfKkv+7DnuSR+DpH+uN4t3bqwIIHpMhox8sybMeijrc9 -HULxQHNf7j5AWPZoeSnmi6p/LmA73Nu5zVNXAVwMs7XD6Qa753GUormhNWgHFVop -7hvflewHWtkDs0IQOn9F6SawH3jpAIDg+kk9+b8RTefVRV0IKijPouHUIh63tD39 -4VYo5xbW8wOwCYFBbnyfU7JOB9OAxTPKZXAro4L2knlIh9kblnWhZwje3RTsyGrF -3YrY/VgSVlPQHLzKZAKQwc8lgoASkvevaeKVm5z3qkqGkj4YFBV85OYe8qXWy36d -pYzouZKiY3ks/60ML98Ob+/Qo2q44LJSHIPY5ut2KLktuN517gxBZdEV3ozsjt+e -HYQpJzjQb3b3DqosOt6pKD/9VhiTOXxU89OA3zPMJZXsuo5c0FiaGsvDCPoIB+1c -LQVZnY+Rsja7V0M7gG6zlJgvk3i4BOg+jpYpUMz/baUQ6aoJ161Fjbx44sdLMhnU -XMTd1p8nl3dGDY5Co4qKxZME7Zw1PBcHQ5ZRJR1UKcDsFgFzKwztbaULyDE0JRun -hBIOZTvO6JrVXqA+Ix/O4e0CAwEAAQ== ------END PUBLIC KEY----- diff --git a/dev_utils/keys/sign_jwt.sh b/dev_utils/keys/sign_jwt.sh new file mode 100644 index 0000000..976ae34 --- /dev/null +++ b/dev_utils/keys/sign_jwt.sh @@ -0,0 +1,71 @@ +#!/usr/bin/bash + +# Inspired by implementation by Will Haley at: +# http://willhaley.com/blog/generate-jwt-with-bash/ + +set -o pipefail + +# Shared content to use as template +header_template='{ + "typ": "JWT", + "kid": "0001" +}' + +build_header() { + jq -c \ + --arg iat_str "$(date +%s)" \ + --arg alg "${1}" \ + ' + ($iat_str | tonumber) as $iat + | .alg = $alg + | .iat = $iat + | .exp = ($iat + 86400) + ' <<<"$header_template" | tr -d '\n' +} + +b64enc() { openssl enc -base64 -A | tr '+/' '-_' | tr -d '='; } +json() { jq -c . | LC_CTYPE=C tr -d '\n'; } +rs_sign() { openssl dgst -binary -sha"${1}" -sign <(printf '%s\n' "$2"); } +es_sign() { openssl dgst -binary -sha"${1}" -sign <(printf '%s\n' "$2") | openssl asn1parse -inform DER | grep INTEGER | cut -d ':' -f 4 | xxd -p -r; } + +sign() { + if [ -n "$2" ]; then + rsa_secret=$(<"$2") + else + echo "no signing key supplied" + exit 1 + fi + local algo payload header sig secret=$rsa_secret + algo=${1:-RS256} + algo=${algo^^} + header=$(build_header "$algo") || return + payload=${4:-$test_payload} + signed_content="$(json <<<"$header" | b64enc).$(json <<<"$payload" | b64enc)" + case $algo in + RS*) sig=$(printf %s "$signed_content" | rs_sign "${algo#RS}" "$secret" | b64enc) ;; + ES*) sig=$(printf %s "$signed_content" | es_sign "${algo#ES}" "$secret" | b64enc) ;; + *) + echo "Unknown algorithm" >&2 + return 1 + ;; + esac + printf '%s.%s\n' "${signed_content}" "${sig}" +} + +iat=$(date +%s) +exp=$(date --date="${3:-tomorrow}" +%s) + +test_payload='{ + "at_hash": "J_fA458SPsXFV6lJQL1l-w", + "aud": "XC56EL11xx", + "email": "dummy.tester@example.org", + "exp": '"$exp"', + "iat": '"$iat"', + "iss": "http://sda-sda-svc-auth", + "kid": "d87f2d01d1a4abb16e1eb88f6561e5067f3a6430174b8fcd0b6bf61434d6c5c8", + "name": "Dummy Tester", + "sid": "1ad14eb5-9b51-40c0-a52a-154a5a3792d5", + "sub": "dummy" +}' + +sign "$@" diff --git a/dev_utils/proxyS3 b/dev_utils/proxyS3 index 426786b..d0d601b 100644 --- a/dev_utils/proxyS3 +++ b/dev_utils/proxyS3 @@ -1,7 +1,7 @@ [default] access_key=dummy secret_key=dummy -access_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2R1bW15LmVnYS5uYmlzLnNlIiwic3ViIjoiZHVtbXkiLCJleHAiOjE3MTk2NzY5ODAsInBpbG90IjoicGlsb3QifQ.ChqAtCVO3OUyVwOMdRGH_GIC_C1Vl5ZBkBt37At6ryhKdscIVPT067a9A4RoksqRSmBRoimcbaLS0JuDAVVqABRNbqfcmSmZyL2Fk8i3rdWMo_QzvGwHFdNvcwKpVBbSthmMLBNRkSdBtOV1QFLG3qvXq-cWJJQTvMMETPD2COlYFqmoVMVH2LorPAPdQhEHrTpHNmh2BO0VeTS5hRhcZaIWOvzdJ59Uk0P7I4NNjCF6FNQQAh8P8veLPwgFB9Jw0_Ej9K3mEZTpbcIyk2law6I0Nku4JUFefH9_08Ff16hQkVUOAylL9_hrWwf6coza_ETiIvjEGGY8ImUFyWLHq86_GmNWo-tqalGpnfTPlz9derX65t5lafhPMe_cDl86vtmeIf26AObrqJhVu6bJezNk80igIk06VwjmzB7VPM_PygIG3VYk02x4PkFctmSmly-XhYDWLvtVVw0cBkT0ITIVPlehh-U__2ITI-wJewg7TDji7ohkxIGzMLGiLpy-sXXFdbssZJNWS7Gaq1-SfYlwp-DScQYmbG_gzLP7bkijIwPUzU74JCVy9NqoUHdGqXao1hdJgUQ9FmPVIxanm7VtEi7e3vn4tVQNLxTnYmredMU5QKDizS3hHbuJ0fwAMr5Mreokkx6FmYphtq7UMx17TAAdQuantc49ee4pVuo +access_token=TOKEN check_ssl_certificate = False check_ssl_hostname = False encoding = UTF-8 @@ -13,4 +13,4 @@ human_readable_sizes = true multipart_chunk_size_mb = 5 use_https = True socket_timeout = 30 -ca_certs_file = certs/ca.crt +ca_certs_file = /certs/ca.crt diff --git a/helper/helper.go b/helper/helper.go index 47c2e16..3d30884 100644 --- a/helper/helper.go +++ b/helper/helper.go @@ -49,17 +49,6 @@ var ( "jti": "5e6c6d24-42eb-408e-ba20-203904e388a1", } - ExpiredAndWrongUserClaims = map[string]interface{}{ - "sub": "c5773f41d17d27bd53b1e6794aedc32d7906e779@elixir-europe.org", - "aud": "15137645-3153-4d49-9ddb-594027cd4ca7", - "azp": "15137645-3153-4d49-9ddb-594027cd4ca7", - "scope": "ga4gh_passport_v1 openid", - "iss": "https://dummy.ega.nbis.se", - "exp": time.Now().Add(-time.Hour * 2).Unix(), - "iat": time.Now().Unix(), - "jti": "5e6c6d24-42eb-408e-ba20-203904e388a1", - } - NonValidClaims = map[string]interface{}{ "sub": "c5773f41d17d27bd53b1e6794aedc32d7906e779@elixir-europe.org", "aud": "15137645-3153-4d49-9ddb-594027cd4ca7", diff --git a/tests/tests.sh b/tests/tests.sh index 5557c12..c66c29d 100644 --- a/tests/tests.sh +++ b/tests/tests.sh @@ -1,8 +1,12 @@ #!/bin/bash +if [ $UID -eq 0 ]; then + apt-get -qq update && apt-get -qq install -y jq xxd +fi + # Function checking that a file was uploaded to the S3 backend function check_output_status() { - if [[ $1 -eq 0 ]]; then + if [ "$1" -eq 0 ]; then echo -e "\u2705 Test passed, expected response found" else echo -e "\u274c Test failed, expected response not found" @@ -12,106 +16,117 @@ function check_output_status() { cd dev_utils || exit 1 +token="$(bash keys/sign_jwt.sh ES256 /keys/jwt.key)" +sed -i "s/TOKEN/$token/" proxyS3 + s3cmd -c directS3 put README.md s3://test/some_user/ >/dev/null 2>&1 || exit 1 echo "- Testing allowed actions" # Put file into bucket echo "Trying to upload a file to user's bucket" -output=$(s3cmd -c proxyS3 put README.md s3://dummy/ >/dev/null 2>&1) -check_output_status "$output" +s3cmd -c proxyS3 put README.md s3://dummy/ >/dev/null 2>&1 +check_output_status "$?" # List objects echo "Trying to list user's bucket" -output=$(s3cmd -c proxyS3 ls s3://dummy 2>&1 | grep -q "README.md") -check_output_status "$output" +s3cmd -c proxyS3 ls s3://dummy/ 2>&1 | grep -q "README.md" +check_output_status "$?" # ---------- Test forbidden actions ---------- forbidden="Forbidden" unauthorized="Unauthorized" nobucket="NoSuchBucket" -notfound="Not Found" echo "- Testing forbidden actions" # Make bucket echo "Trying to create bucket" -output=$(s3cmd -c proxyS3 mb s3://test_bucket 2>&1 | grep -q $forbidden) -check_output_status "$output" +s3cmd -c proxyS3 mb s3://test_bucket 2>&1 | grep -q "$forbidden" +check_output_status "$?" # Remove bucket echo "Trying to remove bucket" -output=$(s3cmd -c proxyS3 rb s3://test 2>&1 | grep -q $forbidden) -check_output_status "$output" +s3cmd -c proxyS3 rb s3://test 2>&1 | grep -q "$forbidden" +check_output_status "$?" # List buckets echo "Trying to list all buckets" -output=$(s3cmd -c proxyS3 ls s3:// 2>&1 | grep -q $forbidden) -check_output_status "$output" +s3cmd -c proxyS3 ls s3:// 2>&1 | grep -q "$forbidden" +check_output_status "$?" # List all objects in all buckets echo "Trying to list all objects in all buckets" -output=$(s3cmd -c proxyS3 la s3:// 2>&1 | grep -q $forbidden) -check_output_status "$output" +s3cmd -c proxyS3 la s3:// 2>&1 | grep -q "$forbidden" +check_output_status "$?" # Put file into another user's bucket echo "Trying to upload a file to another user's bucket" -output=$(s3cmd -c proxyS3 put README.md s3://some_user/ 2>&1 | grep -q $unauthorized) -check_output_status "$output" +s3cmd -c proxyS3 put README.md s3://some_user/ 2>&1 | grep -q "$unauthorized" +check_output_status "$?" # Get file from another user's bucket echo "Trying to get a file from another user's bucket" -output=$(s3cmd -c proxyS3 get s3://some_user/README.md local_file.md 2>&1 | grep -q $unauthorized) -check_output_status "$output" +s3cmd -c proxyS3 get s3://some_user/README.md local_file.md 2>&1 | grep -q "$unauthorized" +check_output_status "$?" # Get file from own bucket echo "Trying to get a file from user's bucket" -output=$(s3cmd -c proxyS3 get s3://dummy/README.md local_file.md 2>&1 | grep -q $nobucket) -check_output_status "$output" +echo "This is skipped due to being non functional" +# s3cmd -c proxyS3 get s3://dummy/README.md local_file.md 2>&1 | grep -q "does not exist" +# check_output_status "$?" # Delete file from bucket echo "Trying to delete a file from user's bucket" -output=$(s3cmd -c proxyS3 del s3://dummy/README.md 2>&1 | grep -q $forbidden) -check_output_status "$output" +s3cmd -c proxyS3 del s3://dummy/README.md 2>&1 | grep -q "$forbidden" +check_output_status "$?" # Disk usage by buckets echo "Trying to get disk usage for user's bucket" -output=$(s3cmd -c proxyS3 du s3://dummy 2>&1 | grep -q $forbidden) -check_output_status "$output" +s3cmd -c proxyS3 du s3://dummy 2>&1 | grep -q "$forbidden" +check_output_status "$?" # Get various information about user's bucket echo "Trying to get information about for user's bucket" -output=$(s3cmd -c proxyS3 info s3://dummy 2>&1 | grep -q $forbidden) -check_output_status "$output" +s3cmd -c proxyS3 info s3://dummy 2>&1 | grep -q "$forbidden" +check_output_status "$?" # Get various information about user's file echo "Trying to get information about user's file" -output=$(s3cmd -c proxyS3 info s3://dummy/README.md 2>&1 | grep -q "$notfound") -check_output_status "$output" +s3cmd -c proxyS3 info s3://dummy/README.md 2>&1 | grep -q "NoSuchKey" +check_output_status "$?" # Move object echo "Trying to move file to another location" -output=$(s3cmd -c proxyS3 mv s3://dummy/README.md s3://dummy/test 2>&1 | grep -q $forbidden) -check_output_status "$output" +s3cmd -c proxyS3 mv s3://dummy/README.md s3://dummy/test 2>&1 | grep -q "$forbidden" +check_output_status "$?" # Copy object echo "Trying to copy file to another location" -output=$(s3cmd -c proxyS3 cp s3://dummy/README.md s3://dummy/test 2>&1 | grep -q $forbidden) -check_output_status "$output" +s3cmd -c proxyS3 cp s3://dummy/README.md s3://dummy/test 2>&1 | grep -q "$forbidden" +check_output_status "$?" # Modify access control list for file echo "Trying to modify acl for user's file" -output=$(s3cmd -c proxyS3 setacl s3://dummy/README.md --acl-public 2>&1 | grep -q $forbidden) -check_output_status "$output" +s3cmd -c proxyS3 setacl s3://dummy/README.md --acl-public 2>&1 | grep -q "$forbidden" +check_output_status "$?" # Show multipart uploads - when multipart enabled, add all relevant tests echo "Trying to list multipart uploads" -output=$(s3cmd -c proxyS3 multipart s3://dummy/ 2>&1 | grep -q $nobucket) -check_output_status "$output" +s3cmd -c proxyS3 multipart s3://dummy/ 2>&1 | grep -q "$nobucket" +check_output_status "$?" # Enable/disable bucket access logging echo "Trying to change the access logging for a bucket" -output=$(s3cmd -c proxyS3 accesslog s3://dummy/ 2>&1 | grep -q $nobucket) -check_output_status "$output" +s3cmd -c proxyS3 accesslog s3://dummy/ 2>&1 | grep -q "$nobucket" +check_output_status "$?" + +token="$(bash keys/sign_jwt.sh ES256 /keys/jwt.key yesterday)" +sed -i "s/^access_token=.*/access_token=$token/" proxyS3 + +# Test access with expired token +echo "Test access with expired token" +s3cmd -c proxyS3 ls s3://dummy/README.md 2>&1 | grep -q "$unauthorized" +check_output_status "$?" echo "All tests have passed" diff --git a/userauth.go b/userauth.go index a492261..784e0f0 100644 --- a/userauth.go +++ b/userauth.go @@ -81,10 +81,7 @@ func (u *ValidateFromToken) Authenticate(r *http.Request) (claims jwt.MapClaims, return nil, fmt.Errorf("failed to parse EC public key (%v)", err) } _, err = jwt.Parse(tokenStr, func(tokenStr *jwt.Token) (interface{}, error) { return key, nil }) - // Validate the error - v, _ := err.(*jwt.ValidationError) - // If error is for expired token continue - if err != nil && v.Errors != jwt.ValidationErrorExpired { + if err != nil { return nil, fmt.Errorf("signed token (ES256) not valid: %v, (token was %s)", err, tokenStr) } case "RS256": @@ -93,10 +90,7 @@ func (u *ValidateFromToken) Authenticate(r *http.Request) (claims jwt.MapClaims, return nil, fmt.Errorf("failed to parse RSA256 public key (%v)", err) } _, err = jwt.Parse(tokenStr, func(tokenStr *jwt.Token) (interface{}, error) { return key, nil }) - // Validate the error - v, _ := err.(*jwt.ValidationError) - // If error is for expired token continue - if err != nil && v.Errors != jwt.ValidationErrorExpired { + if err != nil { return nil, fmt.Errorf("signed token (RS256) not valid: %v, (token was %s)", err, tokenStr) } default: diff --git a/userauth_test.go b/userauth_test.go index 6fb5b2f..cc4c01d 100644 --- a/userauth_test.go +++ b/userauth_test.go @@ -104,18 +104,7 @@ func TestUserTokenAuthenticator_ValidateSignature_RSA(t *testing.T) { r.Header.Set("X-Amz-Security-Token", expiredToken) r.URL.Path = "/dummy/" _, err = a.Authenticate(r) - assert.Nil(t, err) - - // Create and test expired Elixir token with wrong username - expiredAndWrongUserToken, err := helper.CreateRSAToken(prKeyParsed, "RS256", "JWT", helper.ExpiredAndWrongUserClaims) - assert.NoError(t, err) - - r, _ = http.NewRequest("", "/", nil) - r.Host = "localhost" - r.Header.Set("X-Amz-Security-Token", expiredAndWrongUserToken) - r.URL.Path = "/username/" - _, expiredAndWrongUser := a.Authenticate(r) - assert.Equal(t, "token supplied username c5773f41d17d27bd53b1e6794aedc32d7906e779@elixir-europe.org but URL had username", expiredAndWrongUser.Error()) + assert.Error(t, err) // Elixir token is not valid (e.g. issued in a future time) nonValidToken, err := helper.CreateRSAToken(prKeyParsed, "RS256", "JWT", helper.NonValidClaims) @@ -211,18 +200,7 @@ func TestUserTokenAuthenticator_ValidateSignature_EC(t *testing.T) { r.Header.Set("X-Amz-Security-Token", expiredToken) r.URL.Path = "/dummy/" _, err = a.Authenticate(r) - assert.Nil(t, err) - - // Create and test expired Elixir token with wrong username - expiredAndWrongUserToken, err := helper.CreateECToken(prKeyParsed, "ES256", "JWT", helper.ExpiredAndWrongUserClaims) - assert.NoError(t, err) - - r, _ = http.NewRequest("", "/", nil) - r.Host = "localhost" - r.Header.Set("X-Amz-Security-Token", expiredAndWrongUserToken) - r.URL.Path = "/username/" - _, expiredAndWrongUser := a.Authenticate(r) - assert.Equal(t, "token supplied username c5773f41d17d27bd53b1e6794aedc32d7906e779@elixir-europe.org but URL had username", expiredAndWrongUser.Error()) + assert.Error(t, err) // Elixir token is not valid nonValidToken, err := helper.CreateECToken(prKeyParsed, "ES256", "JWT", helper.NonValidClaims)