diff --git a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py index df4324bad2e0148a35e958b9f8019b7300b0fe40..e6d367abe2a42c6f5a6543db21e1159d94167ded 100644 --- a/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py +++ b/services/TS29222_CAPIF_Security_API/capif_security/core/servicesecurity.py @@ -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 @@ -93,6 +97,47 @@ class SecurityOperations(Resource): current_app.logger.error("Bad format Scope: " + e) 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) @@ -196,7 +241,7 @@ class SecurityOperations(Resource): return internal_server_error(detail=exception, cause=str(e)) def create_servicesecurity(self, api_invoker_id, service_security): - + mycol = self.db.get_col_by_name(self.db.security_info) try: @@ -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) diff --git a/services/docker-compose-capif.yml b/services/docker-compose-capif.yml index b6e2bfecd91d06df500a77384cb193a9d2cd85cf..e021aa52c815819c8508dab50c5de0fc5f76a2a3 100644 --- a/services/docker-compose-capif.yml +++ b/services/docker-compose-capif.yml @@ -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 diff --git a/services/nginx/Dockerfile b/services/nginx/Dockerfile index c87732c5ba562af7dac31974b62bc560a47faef1..b163386ec2e8883c87d09c768fe9edd1b6f35704 100644 --- a/services/nginx/Dockerfile +++ b/services/nginx/Dockerfile @@ -1,4 +1,4 @@ -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 diff --git a/services/nginx/nginx.conf b/services/nginx/nginx.conf index f51e177fd0fce53f6d151e3483fcd4802a5a1d3c..ecde2dc13855140e0b2cb70437149c297cf9bb46 100644 --- a/services/nginx/nginx.conf +++ b/services/nginx/nginx.conf @@ -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; } diff --git a/tools/base_images_scripts/create_nginx_patched_images.sh b/tools/base_images_scripts/create_nginx_patched_images.sh new file mode 100755 index 0000000000000000000000000000000000000000..07dd769d81f6bcbf37a1db38b2425d4f2692e551 --- /dev/null +++ b/tools/base_images_scripts/create_nginx_patched_images.sh @@ -0,0 +1,120 @@ +#!/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."