Resolve "Security PKI (Certificates) Authentication and Authorization"

Proposers

  • Jorge Moratinos (TID)

Description

This MR will include on OpenCAPIF some steps according to PKI Security Method.

Demo or definition of done

We need to check creation of Security context with security method PKI and after that execute/simulate the flow from AEF to get security information and check authenticationInfo field.

References

  1. Wiki PKI Discussion

Acknowledgements

This work is funded by the European Commission through the HORIZON-JU-SNS-2022 call of FIDAL project with Grant Agreement number 101096146.

Closes #111 (closed)

Edited by Jorge Moratinos

Merge request reports

Loading
+73 −3
Changes for services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py: 73 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -105,13 +105,67 @@ class SecurityOperations(Resource):
                    current_app.logger.error("Not found security context")
                    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']:
                    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']
                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']

                    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(
                    services_security_object, default=json_util.default)
                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")

                    # 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"]

                    current_app.logger.debug("Interface security methods: " + str(security_methods))
+46 −5
Changes for services/TS29222_CAPIF_Security_API/prepare_security.sh: 46 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -3,6 +3,9 @@
VAULT_ADDR="http://$VAULT_HOSTNAME:$VAULT_PORT"
VAULT_TOKEN=$VAULT_ACCESS_TOKEN

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

# Maximum number of retry attempts
MAX_RETRIES=30
# Delay between retries (in seconds)
@@ -10,6 +13,40 @@ RETRY_DELAY=10
# Attempt counter
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
    # Increment ATTEMPT using eval
    eval "ATTEMPT=\$((ATTEMPT + 1))"
@@ -24,16 +61,20 @@ while [ $ATTEMPT -lt $MAX_RETRIES ]; do

    # Check if the response is "null" or empty
    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."
        gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \
         --chdir /usr/src/app/capif_security wsgi:app
        exit 0  # Exit successfully
        SUCCES_OPERATION=true
        break
    else
        echo "Invalid response ('null' or empty), retrying in $RETRY_DELAY seconds..."
        sleep $RETRY_DELAY
    fi
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
fi

gunicorn -k uvicorn.workers.UvicornH11Worker --bind 0.0.0.0:8080 \
         --chdir $CERTS_FOLDER wsgi:app
+3 −3
Changes for services/run.sh: 3 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@ source $(dirname "$(readlink -f "$0")")/variables.sh
help() {
  echo "Usage: $1 <options>"
  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 "       -l : Set Log Level (default DEBUG). Select one of: [CRITICAL, FATAL, ERROR, WARNING, WARN, INFO, DEBUG, NOTSET]"
  echo "       -r : Remove cached information on build"
@@ -35,7 +35,7 @@ then
fi

# 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
    c)
      CAPIF_HOSTNAME="$OPTARG"
@@ -44,7 +44,7 @@ while getopts ":c:l:mshrv:f:g:b:" opt; do
      MONITORING_STATE=true
      ;;
    s)
      ROBOT_MOCK_SERVER=true
      ROBOT_MOCK_SERVER="$OPTARG"
      ;;
    v)
      OCF_VERSION="$OPTARG"
+1 −0
Changes for services/variables.sh: 1 added line, 0 removed lines.
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ export LOG_LEVEL=DEBUG
export CACHED_INFO=""
export BUILD_DOCKER_IMAGES=true
export REMOVE_IMAGES=false
export ROBOT_MOCK_SERVER=true

# Needed to avoid write permissions on bind volumes with prometheus and grafana
export DUID=$(id -u)
+65 −0
Changes for tests/features/CAPIF Security Api/capif_security_api.robot: 65 added lines, 0 removed lines.
Original line number 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
    ...    error=invalid_scope
    ...    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
Loading