Ocf112 security psk method authentication and authorization

Proposers

  • Pelayo Torres (TID)

Description

Developed PSK security method using a new nginx image to obtain all the necessary data.

Demo or definition of done

CAPIF creates and store the PSK when an Invoker creates a security context. CAPIF returns the PSK to the AEF when the AEF request the Invoker security context. Created a script to patch and upload the new Nginx image whit the necessary module to obtain the data to create the PSK.

Acknowledgements

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

Merge request reports

Loading
+106 −2
Changes for services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py: 106 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -5,9 +5,13 @@ from datetime import datetime, timedelta

import rfc3987
from bson import json_util
from flask import current_app
from flask import current_app, request
from flask_jwt_extended import create_access_token
from pymongo import ReturnDocument
import hmac 
import hashlib 
import unicodedata


from ..core.publisher import Publisher
from ..models.access_token_claims import AccessTokenClaims
@@ -94,6 +98,47 @@ class SecurityOperations(Resource):
            token_error = AccessTokenErr(error="invalid_scope", error_description="malformed scope")
            return make_response(object=clean_empty(token_error.to_dict()), status=400)
    

    def __derive_psk(self, master_key:str, session_id:str, interface:dict): 
        ## Derive the PSK using the provided master key, session ID, and interface information

        # Interface information
        host = None
        if 'fqdn' in interface:
            host = interface['fqdn']
        elif 'ipv4Addr' in interface:
            host = interface['ipv4Addr']
        elif 'ipv6Addr' in interface:
            host = interface['ipv6Addr']
        port = interface.get('port', None)

        api_prefix = interface.get('apiPrefix', '')
        scheme = "https" if port in (None, 443) else "http"

        interface_info = f"{scheme}://{host}"
        if port and port != 443:
            interface_info += f":{port}"
        interface_info += api_prefix
        
        # Normalize the strings to NFKC form
        p0_string = unicodedata.normalize("NFKC", interface_info).encode("utf-8") 
        p1_string = unicodedata.normalize("NFKC", session_id).encode("utf-8") 
        
        # Convert to octet format (0xFF) 
        p0_octet_string = ' '.join(f'0x{byte:02X}' for byte in p0_string) 
        p1_octet_string = ' '.join(f'0x{byte:02X}' for byte in p1_string) 

        # Convert number of bytes to 16-bit big-endian 
        l0 = ' '.join(f'0x{byte:02X}' for byte in len(p0_octet_string).to_bytes(2, 'big')) 
        l1 = ' '.join(f'0x{byte:02X}' for byte in len(p1_octet_string).to_bytes(2, 'big')) 
        
        # Create S string using FC (0x7A) and the octet strings with their lengths 
        S = "0x7A" + ' ' + p0_octet_string + ' ' + l0 + ' ' + p1_octet_string + ' ' + l1 
        psk = hmac.new(master_key.encode("utf-8"), S.encode("utf-8"), hashlib.sha256).digest() 
        
        return psk


    def __init__(self):
        Resource.__init__(self)
        self.filter_aef_id = "aef_profiles.aef_id"
@@ -274,7 +319,7 @@ class SecurityOperations(Resource):
                        self.db.capif_service_col)
                    services_security_object = capif_service_col.find_one(
                        {"api_id": service_instance.api_id, self.filter_aef_id: service_instance.aef_id}, {"aef_profiles.security_methods.$": 1})

                    current_app.logger.debug("Aef profile: " + str(services_security_object))
                    if services_security_object is None:
                        current_app.logger.error(
                            "Not found service with this aef id: " + service_instance.aef_id)
@@ -306,6 +351,36 @@ class SecurityOperations(Resource):
                # Select the highest-priority security method
                service_instance.sel_security_method = sorted_methods[0]

                if service_instance.sel_security_method == "PSK":
                    request.headers.get('X-TLS-Protocol', 'N/A')
                    sesionId = request.headers.get('X-TLS-Session-ID', 'N/A')  
                    Mkey = request.headers.get('X-TLS-MKey', 'N/A') 
                    current_app.logger.info(f"TLS Protocol: {request.headers.get('X-TLS-Protocol', 'N/A')}, Session id: {sesionId}, Master Key: {Mkey}") 

                    interface = None
                    if service_instance.interface_details:
                        current_app.logger.debug("Interface details found")
                        interface = service_instance.interface_details.to_dict()
                    
                    else:
                        current_app.logger.error("Interface details not found")
                        services_security_object = capif_service_col.find_one(
                        {"api_id": service_instance.api_id}, {"aef_profiles": {"$elemMatch": {"aef_id": service_instance.aef_id}}, "_id": 0})
                        current_app.logger.debug("Aef profile: " + str(services_security_object["aef_profiles"][0]))
                        if "interface_descriptions" in services_security_object["aef_profiles"][0]:
                            current_app.logger.debug("Aef profile: " + str(services_security_object["aef_profiles"][0]["interface_descriptions"]))
                            interface = services_security_object["aef_profiles"][0]["interface_descriptions"][0]
                        elif "domain_name" in services_security_object["aef_profiles"][0]:
                            current_app.logger.debug("Aef profile: " + str(services_security_object["aef_profiles"][0]["domain_name"]))
                            interface = services_security_object["aef_profiles"][0]["domain_name"]
                    
                    if interface:
                        current_app.logger.debug("Deriving PSK")
                        psk = self.__derive_psk(Mkey, sesionId, interface)
                        current_app.logger.debug("PSK derived : " + str(psk))

                        service_instance.authorization_info = str(psk)

                # Send service instance to ACL
                current_app.logger.debug("Sending message to create ACL")
                publish_ops.publish_message("acls-messages", "create-acl:"+str(
@@ -501,6 +576,35 @@ class SecurityOperations(Resource):
                        valid_security_method)[0]
                update_acls.append({"api_id": service_instance.api_id, "aef_id": service_instance.aef_id})

                if service_instance.sel_security_method == "PSK":
                    request.headers.get('X-TLS-Protocol', 'N/A')
                    sesionId = request.headers.get('X-TLS-Session-ID', 'N/A')  
                    Mkey = request.headers.get('X-TLS-MKey', 'N/A') 
                    current_app.logger.info(f"TLS Protocol: {request.headers.get('X-TLS-Protocol', 'N/A')}, Session id: {sesionId}, Master Key: {Mkey}") 

                    interface = None
                    if service_instance.interface_details:
                        current_app.logger.debug("Interface details found")
                        interface = service_instance.interface_details.to_dict()
                    
                    else:
                        current_app.logger.error("Interface details not found")
                        services_security_object = capif_service_col.find_one(
                        {"api_id": service_instance.api_id}, {"aef_profiles": {"$elemMatch": {"aef_id": service_instance.aef_id}}, "_id": 0})
                        current_app.logger.debug("Aef profile: " + str(services_security_object["aef_profiles"][0]))
                        if "interface_descriptions" in services_security_object["aef_profiles"][0]:
                            current_app.logger.debug("Aef profile: " + str(services_security_object["aef_profiles"][0]["interface_descriptions"]))
                            interface = services_security_object["aef_profiles"][0]["interface_descriptions"][0]
                        elif "domain_name" in services_security_object["aef_profiles"][0]:
                            current_app.logger.debug("Aef profile: " + str(services_security_object["aef_profiles"][0]["domain_name"]))
                            interface = services_security_object["aef_profiles"][0]["domain_name"]
                    
                    if interface:
                        current_app.logger.debug("Deriving PSK")
                        psk = self.__derive_psk(Mkey, sesionId, interface)
                        current_app.logger.debug("PSK derived : " + str(psk))

                        service_instance.authorization_info = str(psk)
            service_security = service_security.to_dict()
            service_security = clean_empty(service_security)

+1 −1
Changes for services/nginx/Dockerfile: 1 added line, 1 removed line.
Original line number Diff line number Diff line
FROM labs.etsi.org:5050/ocf/capif/nginx:1.27.1
FROM labs.etsi.org:5050/ocf/capif/nginx-ocf-patched:1.27.1
RUN apt-get update && apt-get install -y jq && apt-get clean
RUN apt-get install -y openssl
RUN apt-get install -y curl
+4 −0
Changes for services/nginx/nginx.conf: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -142,6 +142,10 @@ http {
                  return 401 $security_error_message;
                }

                proxy_set_header X-TLS-Protocol $ssl_protocol;
                proxy_set_header X-TLS-Session-ID $ssl_session_id;
                proxy_set_header X-TLS-MKey $sslkeylog_mk;

                proxy_set_header X-SSL-Client-Cert $ssl_client_cert;
                proxy_pass http://capif-security:8080;
              }
+1 −1
Changes for services/docker-compose-capif.yml: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -308,7 +308,7 @@ services:
    ports:
      - "8080:8080"
      - "443:443"
    image: ${REGISTRY_BASE_URL}/nginx:${OCF_VERSION}
    image: labs.etsi.org:5050/ocf/capif/nginx-ocf-patched:1.27.1
    environment:
      - CAPIF_HOSTNAME=${CAPIF_HOSTNAME}
      - VAULT_HOSTNAME=vault
+120 −0
Changes for tools/base_images_scripts/create_nginx_patched_images.sh: 120 added lines, 0 removed lines.
Original line number Diff line number Diff line
#!/bin/bash

set -euo pipefail

NGINX_VERSION=1.27.1
PLATFORMS=("linux/arm64"
"linux/amd64")
PATCH_REPO="https://labs.etsi.org/rep/ocf/tools/nginx-sslkeylog.git"
REGISTRY="labs.etsi.org:5050/ocf/capif"

MANIFEST_AMEND=""
for platform in "${PLATFORMS[@]}";do
  image_name="nginx-ocf-patched:$NGINX_VERSION"
  echo "$image_name pulled for platform $platform"

  container_id=$(docker run -d --platform=$platform --name build-nginx debian:bullseye sleep infinity)

  docker exec $container_id bash -c "
    set -e

    echo 'Installing build dependencies...'
    apt-get update && apt-get install -y \
      build-essential \
      libpcre3-dev \
      libssl-dev \
      zlib1g-dev \
      curl \
      patch \
      ca-certificates \
      openssl \
      git \
      jq \
      gettext

    echo 'Creating nginx user...'
    useradd -r -d /etc/nginx -s /sbin/nologin nginx

    echo 'Downloading NGINX $NGINX_VERSION...'
    curl -LO https://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz
    tar -xzf nginx-${NGINX_VERSION}.tar.gz
    cd nginx-${NGINX_VERSION}

    echo 'Cloning and applying patch...'
    git clone ${PATCH_REPO} ../nginx-sslkeylog
    patch -p1 < ../nginx-sslkeylog/nginx-patches/${NGINX_VERSION}.patch

    echo 'Configuring NGINX...'
    ./configure \
      --prefix=/etc/nginx \
      --sbin-path=/usr/sbin/nginx \
      --modules-path=/usr/lib/nginx/modules \
      --conf-path=/etc/nginx/nginx.conf \
      --error-log-path=/var/log/nginx/error.log \
      --http-log-path=/var/log/nginx/access.log \
      --pid-path=/var/run/nginx.pid \
      --lock-path=/var/run/nginx.lock \
      --http-client-body-temp-path=/var/cache/nginx/client_temp \
      --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
      --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
      --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
      --http-scgi-temp-path=/var/cache/nginx/scgi_temp \
      --user=nginx \
      --group=nginx \
      --with-compat \
      --with-file-aio \
      --with-threads \
      --with-http_addition_module \
      --with-http_auth_request_module \
      --with-http_dav_module \
      --with-http_flv_module \
      --with-http_gunzip_module \
      --with-http_gzip_static_module \
      --with-http_mp4_module \
      --with-http_random_index_module \
      --with-http_realip_module \
      --with-http_secure_link_module \
      --with-http_slice_module \
      --with-http_ssl_module \
      --with-http_stub_status_module \
      --with-http_sub_module \
      --with-http_v2_module \
      --with-http_v3_module \
      --with-mail \
      --with-mail_ssl_module \
      --with-stream \
      --with-stream_realip_module \
      --with-stream_ssl_module \
      --with-stream_ssl_preread_module \
      --with-cc-opt='-g -O2 -ffile-prefix-map=/data/builder/debuild/nginx-${NGINX_VERSION}/debian/debuild-base/nginx-${NGINX_VERSION}=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' \
      --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' \
      --add-module=../nginx-sslkeylog

    echo 'Building NGINX...'
    make -j\$(nproc) && make install

    echo 'Creating required temporary directories...'
    mkdir -p /var/cache/nginx/{client_temp,proxy_temp,fastcgi_temp,uwsgi_temp,scgi_temp}

    echo '✅ NGINX build completed.'
  "

  tag=$(echo $platform | awk -F'/' '{print $NF}')

  docker commit $container_id $image_name
  docker tag $image_name $REGISTRY/$image_name-$tag
  echo "$REGISTRY/$image_name-$tag tagged"
  docker push $REGISTRY/$image_name-$tag
  echo "$REGISTRY/$image_name-$tag pushed"
  MANIFEST_AMEND="$MANIFEST_AMEND --amend $REGISTRY/$image_name-$tag"
  docker stop $container_id
  docker rm $container_id

done

docker manifest create $REGISTRY/$image_name $MANIFEST_AMEND
echo "$REGISTRY/$image_name Manifest created with amend $MANIFEST_AMEND"
docker manifest push $REGISTRY/$image_name
echo "$REGISTRY/$image_name Manifest pushed"

echo "🎉 All builds completed successfully."
Loading