Commit eefae489 authored by Jorge Moratinos's avatar Jorge Moratinos
Browse files

Merge branch 'OCF111-security-pki-certificates-authentication-and-authorization' into 'staging'

Resolve "Security PKI (Certificates) Authentication and Authorization"

Closes #111

See merge request !116
parents 1a835c3a db93ee11
Loading
Loading
Loading
Loading
Loading
+76 −6
Original line number Original line Diff line number Diff line
@@ -105,13 +105,67 @@ class SecurityOperations(Resource):
                    current_app.logger.error("Not found security context")
                    current_app.logger.error("Not found security context")
                    return not_found_error(detail=security_context_not_found_detail, cause=api_invoker_no_context_cause)
                    return not_found_error(detail=security_context_not_found_detail, cause=api_invoker_no_context_cause)


                if not authentication_info:
                for security_info_obj in services_security_object['security_info']:
                for security_info_obj in services_security_object['security_info']:
                    if security_info_obj.get('sel_security_method') == "PKI":
                        current_app.logger.debug("PKI security method selected")
                        if authentication_info:
                            # Read the CA certificate from the file
                            with open("/usr/src/app/capif_security/ca.crt", "rb") as key_file:
                                key_data = key_file.read()
                            # Decode the certificate to a string
                            key_data = key_data.decode('utf-8')
                            # Add the CA certificate to the authentication_info
                            security_info_obj['authentication_info'] = key_data
                        else:
                            # If authentication_info is not needed, remove the key_data
                            del security_info_obj['authentication_info']
                            del security_info_obj['authentication_info']
                if not authorization_info:

                    for security_info_obj in services_security_object['security_info']:
                        if authorization_info:
                            security_info_obj['authorization_info'] = security_info_obj.get('authorization_info', "")
                        else:
                            # If authorization_info is not needed, remove the key_data
                            del security_info_obj['authorization_info']
                            del security_info_obj['authorization_info']


                    elif security_info_obj.get('sel_security_method') == "PSK":
                        current_app.logger.debug("PSK security method selected")
                        if authentication_info:
                            # Read the PSK from the file -> TODO
                            with open("/usr/src/app/capif_security/ca.crt", "rb") as key_file:
                                key_data = key_file.read()
                            # Decode the PSK to a string
                            key_data = key_data.decode('utf-8')
                            # Add the PSK to the authentication_info
                            security_info_obj['authentication_info'] = key_data
                        else:
                            # If authentication_info is not needed, remove the key_data
                            del security_info_obj['authentication_info']

                        if authorization_info:
                            security_info_obj['authorization_info'] = security_info_obj.get('authorization_info', "UNDER DEVELOPMENT")
                        else:
                            # If authorization_info is not needed, remove the key_data
                            del security_info_obj['authorization_info']

                    elif security_info_obj.get('sel_security_method') == "OAUTH":
                        current_app.logger.debug("OAUTH security method selected, this request is not needed")

                        if authentication_info:
                            security_info_obj['authentication_info'] = security_info_obj.get('authentication_info', "")
                        else:
                            # If authentication_info is not needed, remove the key_data
                            del security_info_obj['authentication_info']

                        if authorization_info:
                            security_info_obj['authorization_info'] = security_info_obj.get('authorization_info', "")
                        else:
                            # If authorization_info is not needed, remove the key_data
                            del security_info_obj['authorization_info']

                    else:
                        current_app.logger.error("Bad format security method")
                        return bad_request_error(detail="Bad format security method", cause="Bad format security method", invalid_params=[{"param": "securityMethod", "reason": "Bad format security method"}])


                properyly_json = json.dumps(
                properyly_json = json.dumps(
                    services_security_object, default=json_util.default)
                    services_security_object, default=json_util.default)
                my_service_security = dict_to_camel_case(
                my_service_security = dict_to_camel_case(
@@ -176,6 +230,22 @@ class SecurityOperations(Resource):
                        return not_found_error(detail=f"Service with interfaceDescription {json.dumps(clean_empty(service_instance.interface_details.to_dict()))} not found", cause="Not found Service")
                        return not_found_error(detail=f"Service with interfaceDescription {json.dumps(clean_empty(service_instance.interface_details.to_dict()))} not found", cause="Not found Service")


                    # We obtain the interface security methods
                    # We obtain the interface security methods
                    # We need to go deeper here, because the interface description is an array
                    # and we need to find the correct one according to preferred security method by invoker,
                    # maybe Published API contains more than one interface description, and each one is related
                    # with a different security method, then we need to get a complete list (interface and related security methods)
                    # amd then we need to check if the preferred security method is compatible with the interface description
                    # also the security methods inside interface description is not mandatory, in that case we use aefProfile.securityMethods
                    # an also that aefProfile.securityMethods is not mandatory, only in cases described on TS 29222 - 8.2.4.2.4	Type: AefProfile - 
                    # 
                    # NOTE4: 
                    # For AEFs defined by 3GPP interacting with API invokers via CAPIF-2e, at least one of the "securityMethods" attribute 
                    # within this data type or the "securityMethods" attribute within the "interfaceDescriptions" attribute shall be present. 
                    # For AEFs defined by 3GPP interacting with API invokers via CAPIF-2, the "securityMethods" attribute is optional. 
                    # For AEFs not defined by 3GPP, the "securityMethods" attribute is optional.
                    # 
                    # To achieve this, we need to setup at config which domains or IPs are CAPIF-2e or CAPIF-2, and then we need to check if the domain or IP of the service is in the list.

                    security_methods = aef_profile["aef_profiles"][0]["interface_descriptions"][0]["security_methods"]
                    security_methods = aef_profile["aef_profiles"][0]["interface_descriptions"][0]["security_methods"]


                    current_app.logger.debug("Interface security methods: " + str(security_methods))
                    current_app.logger.debug("Interface security methods: " + str(security_methods))
+47 −6
Original line number Original line Diff line number Diff line
@@ -3,6 +3,9 @@
VAULT_ADDR="http://$VAULT_HOSTNAME:$VAULT_PORT"
VAULT_ADDR="http://$VAULT_HOSTNAME:$VAULT_PORT"
VAULT_TOKEN=$VAULT_ACCESS_TOKEN
VAULT_TOKEN=$VAULT_ACCESS_TOKEN


CERTS_FOLDER="/usr/src/app/capif_security"
# cd $CERTS_FOLDER

# Maximum number of retry attempts
# Maximum number of retry attempts
MAX_RETRIES=30
MAX_RETRIES=30
# Delay between retries (in seconds)
# Delay between retries (in seconds)
@@ -10,6 +13,40 @@ RETRY_DELAY=10
# Attempt counter
# Attempt counter
ATTEMPT=0
ATTEMPT=0


while [ $ATTEMPT -lt $MAX_RETRIES ]; do
    # Increment ATTEMPT using eval
    eval "ATTEMPT=\$((ATTEMPT + 1))"
    echo "Attempt $ATTEMPT of $MAX_RETRIES"

    # Make the request to Vault and store the response in a variable
    RESPONSE=$(curl -s -k --connect-timeout 5 --max-time 10 \
        --header "X-Vault-Token: $VAULT_TOKEN" \
        --request GET "$VAULT_ADDR/v1/secret/data/ca" | jq -r '.data.data.ca')

    echo "$RESPONSE"

    # Check if the response is "null" or empty
    if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then
        echo "$RESPONSE" > $CERTS_FOLDER/ca.crt
        openssl verify -CAfile $CERTS_FOLDER/ca.crt $CERTS_FOLDER/ca.crt
        echo "CA Root successfully saved."
        SUCCES_OPERATION=true
        break
    else
        echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..."
        sleep $RETRY_DELAY
    fi
done

if [ "$SUCCES_OPERATION" = false ]; then
    echo "Error: Failed to retrieve ca root a valid response after $MAX_RETRIES attempts."
    exit 1  # Exit with failure
fi

# Setup inital value to ATTEMPT and SUCCESS_OPERATION
ATTEMPT=0
SUCCES_OPERATION=false

while [ $ATTEMPT -lt $MAX_RETRIES ]; do
while [ $ATTEMPT -lt $MAX_RETRIES ]; do
    # Increment ATTEMPT using eval
    # Increment ATTEMPT using eval
    eval "ATTEMPT=\$((ATTEMPT + 1))"
    eval "ATTEMPT=\$((ATTEMPT + 1))"
@@ -24,16 +61,20 @@ while [ $ATTEMPT -lt $MAX_RETRIES ]; do


    # Check if the response is "null" or empty
    # Check if the response is "null" or empty
    if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then
    if [ -n "$RESPONSE" ] && [ "$RESPONSE" != "null" ]; then
        echo "$RESPONSE" > /usr/src/app/capif_security/server.key
        echo "$RESPONSE" > $CERTS_FOLDER/server.key
        echo "Public key successfully saved."
        echo "Public key successfully saved."
        gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \
        SUCCES_OPERATION=true
         --chdir /usr/src/app/capif_security wsgi:app
        break
        exit 0  # Exit successfully
    else
    else
        echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..."
        echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..."
        sleep $RETRY_DELAY
        sleep $RETRY_DELAY
    fi
    fi
done
done


echo "Error: Failed to retrieve a valid response after $MAX_RETRIES attempts."
if [ "$SUCCES_OPERATION" = false ]; then
    echo "Error: Failed to retrieve server key valid response after $MAX_RETRIES attempts."
    exit 1  # Exit with failure
    exit 1  # Exit with failure
fi

gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \
         --chdir $CERTS_FOLDER wsgi:app
+3 −3
Original line number Original line Diff line number Diff line
@@ -4,7 +4,7 @@ source $(dirname "$(readlink -f "$0")")/variables.sh
help() {
help() {
  echo "Usage: $1 <options>"
  echo "Usage: $1 <options>"
  echo "       -c : Setup different hostname for capif"
  echo "       -c : Setup different hostname for capif"
  echo "       -s : Run Mock server"
  echo "       -s : Run Mock server. Default true"
  echo "       -m : Run monitoring service"
  echo "       -m : Run monitoring service"
  echo "       -l : Set Log Level (default DEBUG). Select one of: [CRITICAL, FATAL, ERROR, WARNING, WARN, INFO, DEBUG, NOTSET]"
  echo "       -l : Set Log Level (default DEBUG). Select one of: [CRITICAL, FATAL, ERROR, WARNING, WARN, INFO, DEBUG, NOTSET]"
  echo "       -r : Remove cached information on build"
  echo "       -r : Remove cached information on build"
@@ -35,7 +35,7 @@ then
fi
fi


# Read params
# Read params
while getopts ":c:l:mshrv:f:g:b:" opt; do
while getopts ":c:l:ms:hrv:f:g:b:" opt; do
  case $opt in
  case $opt in
    c)
    c)
      CAPIF_HOSTNAME="$OPTARG"
      CAPIF_HOSTNAME="$OPTARG"
@@ -44,7 +44,7 @@ while getopts ":c:l:mshrv:f:g:b:" opt; do
      MONITORING_STATE=true
      MONITORING_STATE=true
      ;;
      ;;
    s)
    s)
      ROBOT_MOCK_SERVER=true
      ROBOT_MOCK_SERVER="$OPTARG"
      ;;
      ;;
    v)
    v)
      OCF_VERSION="$OPTARG"
      OCF_VERSION="$OPTARG"
+1 −0
Original line number Original line Diff line number Diff line
@@ -32,6 +32,7 @@ export LOG_LEVEL=DEBUG
export CACHED_INFO=""
export CACHED_INFO=""
export BUILD_DOCKER_IMAGES=true
export BUILD_DOCKER_IMAGES=true
export REMOVE_IMAGES=false
export REMOVE_IMAGES=false
export ROBOT_MOCK_SERVER=true


# Needed to avoid write permissions on bind volumes with prometheus and grafana
# Needed to avoid write permissions on bind volumes with prometheus and grafana
export DUID=$(id -u)
export DUID=$(id -u)
+65 −0
Original line number Original line Diff line number Diff line
@@ -1252,3 +1252,68 @@ Retrieve access token with invalid apiName at scope
    Check Response Variable Type And Values    ${resp}    400    AccessTokenErr
    Check Response Variable Type And Values    ${resp}    400    AccessTokenErr
    ...    error=invalid_scope
    ...    error=invalid_scope
    ...    error_description=One of the api names does not exist or is not associated with the aef id provided
    ...    error_description=One of the api names does not exist or is not associated with the aef id provided


Retrieve the Security Context of an API Invoker for PKI security method
    [Tags]    capif_security_api-28    smoke
    # Default Invoker Registration and Onboarding
    ${register_user_info_invoker}    ${url}    ${request_body}=    Invoker Default Onboarding

    # Register Provider
    ${register_user_info_provider}=    Provider Default Registration

    # Publish Service API
    # Create list with security methods
    ${security_methods}=    Create List     PKI
    ${service_api_description_published_1}    ${resource_url}    ${request_body}=    Publish Service Api
    ...    ${register_user_info_provider}
    ...    service_1
    ...    security_methods=${security_methods}

    # Store apiId1
    ${service_api_id_1}=    Set Variable    ${service_api_description_published_1['apiId']}

    # Create Security Context
    ${request_body}=    Create Service Security Default Body
    ...    ${NOTIFICATION_DESTINATION_URL}
    ...    aef_id=${register_user_info_provider['aef_id']}
    ...    api_id=${service_api_id_1}
    ${resp}=    Put Request Capif
    ...    /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']}
    ...    json=${request_body}
    ...    server=${CAPIF_HTTPS_URL}
    ...    verify=ca.crt
    ...    username=${INVOKER_USERNAME}

    Check Response Variable Type And Values    ${resp}    201    ServiceSecurity
    ${resource_url}=    Check Location Header    ${resp}    ${LOCATION_SECURITY_RESOURCE_REGEX}

    ${service_security_context}=    Set Variable    ${resp.json()}

    # Retrieve Security context can setup by parameters if authenticationInfo and authorizationInfo are needed at response.
    ${resp}=    Get Request Capif
    ...    /capif-security/v1/trustedInvokers/${register_user_info_invoker['api_invoker_id']}?authenticationInfo=true&authorizationInfo=true
    ...    server=${CAPIF_HTTPS_URL}
    ...    verify=ca.crt
    ...    username=${AEF_PROVIDER_USERNAME}

    # Check Results
    Check Response Variable Type And Values    ${resp}    200    ServiceSecurity

    # Response must accomplish:
    # aefProfile must contain authenticationInfo with CA root certificate
    # aefProfile must NOT CONTAIN authorizationInfo

    # Create security_info to compare with response
    ## Read CA root certificate
    ${ca_root}=   Read File Utf8     ca.crt
    ## Create a securityInfo with authenticationInfo with CA root certificate
    ${security_info_expected}=   Add Key To Object    ${service_security_context['securityInfo'][0]}    authenticationInfo    ${ca_root}
    ## Create List of securityInfo
    ${security_info_list}=    Create List     ${security_info_expected}
    ## Set expected securityInfo list in service_security_context
    ${service_security_context_filtered}=   Add Key To Object    ${service_security_context}    securityInfo    ${security_info_list}
    Log Dictionary    ${service_security_context_filtered}

    # Check Results
    Dictionaries Should Be Equal    ${resp.json()}    ${service_security_context_filtered}
Loading