lists.osmocom.org
Sign In Sign Up
Manage this list Sign In Sign Up

Keyboard Shortcuts

Thread View

  • j: Next unread message
  • k: Previous unread message
  • j a: Jump to all threads
  • j l: Jump to MailingList overview

gerrit-log

Thread Start a new thread
Download
Threads by month
  • ----- 2026 -----
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2025 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2024 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2023 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
  • ----- 2022 -----
  • December
  • November
  • October
  • September
  • August
  • July
  • June
  • May
  • April
  • March
  • February
  • January
gerrit-log@lists.osmocom.org

August 2026

  • 1 participants
  • 2269 discussions
[S] Change in onomondo-eim[master]: WIP: invert asn1ct tag workaround
by dexter 07 Aug '26

07 Aug '26
dexter has uploaded this change for review. ( https://gerrit.osmocom.org/c/onomondo-eim/+/43176?usp=email ) Change subject: WIP: invert asn1ct tag workaround ...................................................................... WIP: invert asn1ct tag workaround The erlang asn1ct compiler runs into problems when a context specific ASN.1 tag is applied twice, which is the case in the original SGP.32 SGP32Definitions spec. Currently we remove the excess tag on the ASN.1 struct definition. This works fine as long as the structs are only used internally, which is the case when ASN.1 ESipa bindings are used. When the JSON ESipa bindings are used, many of the structs are used directly. A missing context specific tag on the struct definition will then obviously cause interoperability problems. With this patch we invert the workaround, we now no longer remove the context specific tag at the struct definition, instead we remove it at the struct member in each parent struct. Related: SYS#8100 Change-Id: I43625be897f7a1cdbf0fa6b71ee651df993f8584 --- M asn1/SGP32Definitions.asn1 1 file changed, 8 insertions(+), 7 deletions(-) git pull ssh://gerrit.osmocom.org:29418/onomondo-eim refs/changes/76/43176/1 diff --git a/asn1/SGP32Definitions.asn1 b/asn1/SGP32Definitions.asn1 index cc052b4..d5500bf 100644 --- a/asn1/SGP32Definitions.asn1 +++ b/asn1/SGP32Definitions.asn1 @@ -383,10 +383,11 @@ ecallIndication [123] BOOLEAN OPTIONAL, -- Tag '9F7B' fallbackAllowed [103] BOOLEAN OPTIONAL -- Tag '9F67' } +-- workaround for erlang asn1ct: remove already specified context-specific tag [56] on member authenticateServerResponse -- workaround: add prefix ("SGP32-") to avoid clash with SGP.22 SGP32-AuthenticateClientRequest ::= [59] SEQUENCE { -- Tag 'BF3B' transactionId [0] TransactionId, - authenticateServerResponse [56] SGP32-AuthenticateServerResponse -- This is the response from ES10b.AuthenticateServer + authenticateServerResponse SGP32-AuthenticateServerResponse -- This is the response from ES10b.AuthenticateServer } -- workaround: add prefix ("SGP32-") to avoid clash with SGP.22 SGP32-EUICCInfo2 ::= [34] SEQUENCE { -- Tag 'BF22' @@ -613,9 +614,8 @@ SGP32-SetDefaultDpAddressResponse ::= [101] SEQUENCE { -- Tag 'BF65' setDefaultDpAddressResult INTEGER { ok (0), undefinedError (127)} } --- workaround for erlang asn1ct: remove already specified context-specific tag [33] -- workaround: add prefix ("SGP32-") to avoid clash with SGP.22 -SGP32-PrepareDownloadResponse ::= CHOICE { -- Tag 'BF21' +SGP32-PrepareDownloadResponse ::= [33] CHOICE { -- Tag 'BF21' downloadResponseOk PrepareDownloadResponseOk, downloadResponseError PrepareDownloadResponseError, compactDownloadResponseOk CompactPrepareDownloadResponseOk @@ -643,9 +643,8 @@ euiccCertificate Certificate, -- eUICC Certificate (CERT.EUICC.ECDSA) signed by the EUM eumCertificate Certificate -- EUM Certificate (CERT.EUM.ECDSA) signed by the requested CI } --- workaround for erlang asn1ct: remove already specified context-specific tag [56] -- workaround: add prefix ("SGP32-") to avoid clash with SGP.22 -SGP32-AuthenticateServerResponse ::= CHOICE { -- Tag 'BF38' +SGP32-AuthenticateServerResponse ::= [56] CHOICE { -- Tag 'BF38' authenticateResponseOk SGP32-AuthenticateResponseOk, authenticateResponseError AuthenticateResponseError, compactAuthenticateResponseOk CompactAuthenticateResponseOk @@ -758,9 +757,10 @@ matchingId UTF8String OPTIONAL, ctxParams1 [2] CtxParams1 OPTIONAL } +-- workaround for erlang asn1ct: remove already specified context-specific tag [56] on member authenticateServerResponse AuthenticateClientRequestEsipa ::= [59] SEQUENCE { -- Tag 'BF3B' transactionId [0] TransactionId, -- The TransactionID generated by the SM-DP+/SM-DS - authenticateServerResponse [56] SGP32-AuthenticateServerResponse -- This is the response from ES10b.AuthenticateServer, possibly in compact format + authenticateServerResponse SGP32-AuthenticateServerResponse -- This is the response from ES10b.AuthenticateServer, possibly in compact format } AuthenticateClientResponseEsipa ::= [59] CHOICE { -- Tag 'BF3B' authenticateClientOkDPEsipa AuthenticateClientOkDPEsipa, @@ -794,9 +794,10 @@ transactionId [0] TransactionId, -- The TransactionID generated by the SM-DS profileDownloadTrigger [84] ProfileDownloadTriggerRequest OPTIONAL -- Tag 'BF54' } +-- workaround for erlang asn1ct: remove already specified context-specific tag [33] on member prepareDownloadResponse GetBoundProfilePackageRequestEsipa ::= [58] SEQUENCE { -- Tag 'BF3A' transactionId [0] TransactionId, -- The TransactionID generated by the SM-DP+ - prepareDownloadResponse [33] SGP32-PrepareDownloadResponse -- This is the response from ES10b.PrepareDownload, possibly in compact format + prepareDownloadResponse SGP32-PrepareDownloadResponse -- This is the response from ES10b.PrepareDownload, possibly in compact format } GetBoundProfilePackageResponseEsipa ::= [58] CHOICE { -- Tag 'BF3A' getBoundProfilePackageOkEsipa GetBoundProfilePackageOkEsipa, -- To view, visit https://gerrit.osmocom.org/c/onomondo-eim/+/43176?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: newchange Gerrit-Project: onomondo-eim Gerrit-Branch: master Gerrit-Change-Id: I43625be897f7a1cdbf0fa6b71ee651df993f8584 Gerrit-Change-Number: 43176 Gerrit-PatchSet: 1 Gerrit-Owner: dexter <pmaier(a)sysmocom.de>
1 0
0 0
[L] Change in onomondo-eim[master]: esipa_json_handler: Add ESipa JSON bindings
by dexter 07 Aug '26

07 Aug '26
dexter has uploaded this change for review. ( https://gerrit.osmocom.org/c/onomondo-eim/+/43175?usp=email ) Change subject: esipa_json_handler: Add ESipa JSON bindings ...................................................................... esipa_json_handler: Add ESipa JSON bindings Since SGP.32, version 1.2, the JSON ESipa bindings are mandatory for the eIM (for the IPAd they are still optional). With this patch we add support for the JSON ESipa bindings. Change-Id: Id73ae78a976608fe6f367f8c3b92d8b7b4e195d4 Related: SYS#8100 --- M src/esipa_asn1_http_handler.erl M src/esipa_json_handler.erl A src/esipa_json_http_handler.erl M src/onomondo_eim_app.erl 4 files changed, 479 insertions(+), 41 deletions(-) git pull ssh://gerrit.osmocom.org:29418/onomondo-eim refs/changes/75/43175/1 diff --git a/src/esipa_asn1_http_handler.erl b/src/esipa_asn1_http_handler.erl index 5f6ff9d..3b03826 100644 --- a/src/esipa_asn1_http_handler.erl +++ b/src/esipa_asn1_http_handler.erl @@ -25,7 +25,7 @@ {ok, IpaToEim} = esipa_asn1_codec:decode_ipa_to_eim(Data), {EsipaMsgType, _} = IpaToEim, logger:info( - "Handling incoming IPAd request: ~p,~nPeer=~p, Pid=~p~n", + "Handling incoming IPAd request (ASN.1): ~p,~nPeer=~p, Pid=~p~n", [EsipaMsgType, maps:get(peer, Req0), maps:get(pid, Req0)] ), logger:debug( @@ -64,7 +64,7 @@ maps:get(pid, Req0), [{[{procedureError, abortedOrder}]}], Reason ), logger:info( - "Handling of IPAd request terminated unexpectedly, Reason=~p Pid=~p~n", + "Handling of IPAd request (ASN.1) terminated unexpectedly, Reason=~p Pid=~p~n", [Reason, maps:get(pid, Req0)] ), cowboy_req:reply(500, ?RESPONSE_HEADERS, <<"Internal Server Error">>, Req0) diff --git a/src/esipa_json_handler.erl b/src/esipa_json_handler.erl index 62b4396..c5d8368 100644 --- a/src/esipa_json_handler.erl +++ b/src/esipa_json_handler.erl @@ -2,47 +2,419 @@ % % SPDX-License-Identifier: AGPL-3.0-only % +% Author: Philipp Maier <pmaier(a)sysmocom.de> / sysmocom - s.f.m.c. GmbH + -module(esipa_json_handler). --behavior(cowboy_handler). --export([init/2]). +-export([handle_json/2]). -init(Req0, State) -> - Path = cowboy_req:path(Req0), - Req = - case is_esipa_endpoint(Path) of +% Decode hex-string to binary +to_binary(absent) -> + absent; +to_binary(HexStr) -> + utils:hex_to_binary(HexStr). + +% Encode binary to hex-string +to_hex(absent) -> + absent; +to_hex(Binary) -> + utils:binary_to_hex(Binary). + +% Decode binary from base64 +from_base64(absent) -> + absent; +from_base64(ValueBase64) -> + base64:decode(ValueBase64). + +% Encode binary to base64 +to_base64(absent) -> + absent; +to_base64(ValueBinary) -> + base64:encode(ValueBinary). + +% Decode base64 encoded string to map (ASN.1) +from_base64asn1(_, _, absent) -> + absent; +from_base64asn1(Asn1SpecName, Asn1TypeName, Asn1ValueBase64) -> + Asn1ValueBin = base64:decode(Asn1ValueBase64), + {ok, Asn1ValueMap} = Asn1SpecName:decode(Asn1TypeName, Asn1ValueBin), + Asn1ValueMap. + +% Encode map (ASN.1) to base64 encoded string +to_base64asn1(_, _, absent) -> + absent; +to_base64asn1(Asn1SpecName, Asn1TypeName, Asn1ValueMap) -> + {ok, Asn1ValueBin} = Asn1SpecName:encode(Asn1TypeName, Asn1ValueMap), + base64:encode(Asn1ValueBin). + +% Join a list of tuples into a map +join_loop(Map, []) -> + Map; +join_loop(Map, [TuplesHead | TuplesTail]) -> + {Key, Value} = TuplesHead, + case Value of + absent -> + join_loop(Map, TuplesTail); + _ -> + join_loop(maps:merge(Map, #{Key => Value}), TuplesTail) + end. +join(Tuples) -> + join_loop(#{}, Tuples). + +% Chose the first (there should only be exactly one) non-absent member from the given tuple list +choose([]) -> + throw("missing required JSON element (choice, oneOf)"); +choose([TuplesHead | TuplesTail]) -> + {_, Value} = TuplesHead, + case Value of + absent -> + choose(TuplesTail); + _ -> + TuplesHead + end. + +%GSMA SGP.32, section 6.4.1.1 +handle_json(Pid, {<<"/gsma/rsp2/esipa/initiateAuthentication">>, EsipaReq}) -> + Req = join([ + {euiccChallenge, from_base64(maps:get(<<"euiccChallenge">>, EsipaReq, absent))}, + {euiccInfo1, + from_base64asn1( + 'RSPDefinitions', 'EUICCInfo1', maps:get(<<"euiccInfo1">>, EsipaReq, absent) + )}, + {smdpAddress, maps:get(<<"smdpAddress">>, EsipaReq, absent)}, + {eimTransactionId, to_binary(maps:get(<<"eimTransactionId">>, EsipaReq, absent))} + ]), + EsipaReqAsn = {initiateAuthenticationRequestEsipa, Req}, + + EimToIpaAsn = esipa_asn1_handler:handle_asn1(Pid, EsipaReqAsn), + + case EimToIpaAsn of + {initiateAuthenticationResponseEsipa, + {initiateAuthenticationOkEsipa, InitiateAuthenticationOkEsipa}} -> + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }}, + {transactionId, + to_hex(maps:get(transactionId, InitiateAuthenticationOkEsipa, absent))}, + {serverSigned1, + to_base64asn1( + 'RSPDefinitions', + 'ServerSigned1', + maps:get(serverSigned1, InitiateAuthenticationOkEsipa) + )}, + {serverSignature1, + to_base64(maps:get(serverSignature1, InitiateAuthenticationOkEsipa))}, + {euiccCiPKIdentifierToBeUsed, + to_base64( + maps:get(euiccCiPKIdentifierToBeUsed, InitiateAuthenticationOkEsipa) + )}, + {serverCertificate, + to_base64asn1( + 'PKIX1Explicit88', + 'Certificate', + maps:get(serverCertificate, InitiateAuthenticationOkEsipa) + )}, + {matchingId, maps:get(matchingId, InitiateAuthenticationOkEsipa, absent)}, + {ctxParams1, + to_base64asn1( + 'RSPDefinitions', + 'CtxParams1', + maps:get(ctxParams1, InitiateAuthenticationOkEsipa, absent) + )} + ]); + _ -> + join([{header, #{<<"functionExecutionStatus">> => #{<<"status">> => <<"Failed">>}}}]) + end; +%GSMA SGP.32, section 6.4.1.2 +handle_json(Pid, {<<"/gsma/rsp2/esipa/authenticateClient">>, EsipaReq}) -> + Req = join([ + {transactionId, to_binary(maps:get(<<"transactionId">>, EsipaReq))}, + {authenticateServerResponse, + from_base64asn1( + 'SGP32Definitions', + 'SGP32-AuthenticateServerResponse', + maps:get(<<"authenticateServerResponse">>, EsipaReq) + )} + ]), + EsipaReqAsn = {authenticateClientRequestEsipa, Req}, + + EimToIpaAsn = esipa_asn1_handler:handle_asn1(Pid, EsipaReqAsn), + + case EimToIpaAsn of + {authenticateClientResponseEsipa, + {authenticateClientOkDPEsipa, AuthenticateClientOkDPEsipa}} -> + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }}, + {transactionId, + to_hex(maps:get(transactionId, AuthenticateClientOkDPEsipa, absent))}, + % (The profileMetadata field has different spellings, JSON:"profileMetadata", ASN.1:"profileMetaData") + {profileMetadata, + to_base64asn1( + 'SGP32Definitions', + 'SGP32-StoreMetadataRequest', + maps:get(profileMetaData, AuthenticateClientOkDPEsipa, absent) + )}, + {smdpSigned2, + to_base64asn1( + 'RSPDefinitions', + 'SmdpSigned2', + maps:get(smdpSigned2, AuthenticateClientOkDPEsipa) + )}, + {smdpSignature2, + to_base64( + maps:get(smdpSignature2, AuthenticateClientOkDPEsipa) + )}, + {smdpCertificate, + to_base64asn1( + 'PKIX1Explicit88', + 'Certificate', + maps:get(smdpCertificate, AuthenticateClientOkDPEsipa) + )}, + {hashCc, + to_base64( + maps:get(hashCc, AuthenticateClientOkDPEsipa, absent) + )} + ]); + _ -> + join([{header, #{<<"functionExecutionStatus">> => #{<<"status">> => <<"Failed">>}}}]) + end; +%GSMA SGP.32, section 6.4.1.3 +handle_json(Pid, {<<"/gsma/rsp2/esipa/getBoundProfilePackage">>, EsipaReq}) -> + Req = join([ + {transactionId, to_binary(maps:get(<<"transactionId">>, EsipaReq))}, + {prepareDownloadResponse, + from_base64asn1( + 'SGP32Definitions', + 'SGP32-PrepareDownloadResponse', + maps:get(<<"prepareDownloadResponse">>, EsipaReq) + )} + ]), + EsipaReqAsn = {getBoundProfilePackageRequestEsipa, Req}, + + EimToIpaAsn = esipa_asn1_handler:handle_asn1(Pid, EsipaReqAsn), + + case EimToIpaAsn of + {getBoundProfilePackageResponseEsipa, + {getBoundProfilePackageOkEsipa, GetBoundProfilePackageOkEsipa}} -> + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }}, + {transactionId, + to_hex(maps:get(transactionId, GetBoundProfilePackageOkEsipa, absent))}, + {boundProfilePackage, + to_base64asn1( + 'RSPDefinitions', + 'BoundProfilePackage', + maps:get(boundProfilePackage, GetBoundProfilePackageOkEsipa) + )} + ]); + {getBoundProfilePackageResponseEsipa, + {getBoundProfilePackageErrorEsipa, _GetBoundProfilePackageErrorEsipa}} -> + join([{header, #{<<"functionExecutionStatus">> => #{<<"status">> => <<"Failed">>}}}]); + _ -> + join([{header, #{<<"functionExecutionStatus">> => #{<<"status">> => <<"Failed">>}}}]) + end; +%GSMA SGP.32, section 6.4.1.5 +handle_json(Pid, {<<"/gsma/rsp2/esipa/getEimPackage">>, EsipaReq}) -> + NotifyStateChange = + case maps:get(<<"notifyStateChange">>, EsipaReq, false) of true -> - cowboy_req:reply( - 501, - #{ - <<"content-type">> => <<"application/json">> - }, - <<"{}">>, - Req0 - ); - false -> - cowboy_req:reply( - 404, - #{ - <<"content-type">> => <<"text/plain">> - }, - <<"Not Found">>, - Req0 - ) + {notifyStateChange, 'NULL'}; + _ -> + {notifyStateChange, absent} end, - {ok, Req, State}. + StateChangeCause = + case maps:get(<<"stateChangeCause">>, EsipaReq, absent) of + absent -> + {stateChangeCause, absent}; + 0 -> + {stateChangeCause, otherEim}; + 1 -> + {stateChangeCause, fallback}; + 2 -> + {stateChangeCause, emergencyProfile}; + 3 -> + {stateChangeCause, local}; + 4 -> + {stateChangeCause, reset}; + 5 -> + {stateChangeCause, immediateEnableProfile}; + 6 -> + {stateChangeCause, deviceChange}; + 7 -> + {stateChangeCause, undefined}; + StateChangeCauseInt -> + % TODO: print warning + {stateChangeCause, StateChangeCauseInt} + end, + Req = join([ + {eidValue, to_binary(maps:get(<<"eidValue">>, EsipaReq))}, + NotifyStateChange, + StateChangeCause, + {rPLMN, to_binary(maps:get(<<"rPlmn">>, EsipaReq, absent))} + ]), + EsipaReqAsn = {getEimPackageRequest, Req}, -is_esipa_endpoint(Path) -> - lists:member( - Path, - [ - <<"/gsma/rsp2/esipa/initiateAuthentication">>, - <<"/gsma/rsp2/esipa/authenticateClient">>, - <<"/gsma/rsp2/esipa/getBoundProfilePackage">>, - <<"/gsma/rsp2/esipa/transferEimPackage">>, - <<"/gsma/rsp2/esipa/getEimPackage">>, - <<"/gsma/rsp2/esipa/provideEimPackageResult">>, - <<"/gsma/rsp2/esipa/handleNotification">>, - <<"/gsma/rsp2/esipa/cancelSession">> - ] - ). + EimToIpaAsn = esipa_asn1_handler:handle_asn1(Pid, EsipaReqAsn), + + case EimToIpaAsn of + {getEimPackageResponse, {euiccPackageRequest, EuiccPackageRequest}} -> + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }}, + {euiccPackageRequest, + to_base64asn1( + 'SGP32Definitions', 'EuiccPackageRequest', EuiccPackageRequest + )} + ]); + {getEimPackageResponse, {ipaEuiccDataRequest, IpaEuiccDataRequest}} -> + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }}, + {ipaEuiccDataRequest, + to_base64asn1( + 'SGP32Definitions', 'IpaEuiccDataRequest', IpaEuiccDataRequest + )} + ]); + {getEimPackageResponse, {profileDownloadTriggerRequest, ProfileDownloadTriggerRequest}} -> + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }}, + {profileDownloadTriggerRequest, + to_base64asn1( + 'SGP32Definitions', + 'ProfileDownloadTriggerRequest', + ProfileDownloadTriggerRequest + )} + ]); + {getEimPackageResponse, {eimPackageError, EimPackageError}} -> + EimPackageErrorInt = + case EimPackageError of + noEimPackageAvailable -> + 1; + eidNotFound -> + 2; + invalidEid -> + 3; + missingEid -> + 4; + _ -> + 127 + end, + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }}, + {eimPackageError, EimPackageErrorInt} + ]); + _ -> + join([{header, #{<<"functionExecutionStatus">> => #{<<"status">> => <<"Failed">>}}}]) + end; +%GSMA SGP.32, section 6.4.1.6 +handle_json(Pid, {<<"/gsma/rsp2/esipa/provideEimPackageResult">>, EsipaReq}) -> + Req = join([ + {eidValue, to_binary(maps:get(<<"eidValue">>, EsipaReq, absent))}, + {eimPackageResult, + from_base64asn1( + 'SGP32Definitions', 'EimPackageResult', maps:get(<<"eimPackageResult">>, EsipaReq) + )} + ]), + EsipaReqAsn = {provideEimPackageResult, Req}, + + EimToIpaAsn = esipa_asn1_handler:handle_asn1(Pid, EsipaReqAsn), + + case EimToIpaAsn of + {provideEimPackageResultResponse, {eimAcknowledgements, EimAcknowledgements}} -> + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }}, + {eimAcknowledgements, + to_base64asn1( + 'SGP32Definitions', 'EimAcknowledgements', EimAcknowledgements + )} + ]); + {provideEimPackageResultResponse, {emptyResponse, #{}}} -> + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }} + ]); + {provideEimPackageResultError, + {provideEimPackageResultError, _ProvideEimPackageResultError}} -> + join([{header, #{<<"functionExecutionStatus">> => #{<<"status">> => <<"Failed">>}}}]); + _ -> + join([{header, #{<<"functionExecutionStatus">> => #{<<"status">> => <<"Failed">>}}}]) + end; +%GSMA SGP.32, section 6.4.1.7 +handle_json(Pid, {<<"/gsma/rsp2/esipa/handleNotification">>, EsipaReq}) -> + Req = choose([ + {pendingNotification, + from_base64asn1( + 'SGP32Definitions', + 'SGP32-PendingNotification', + maps:get(<<"pendingNotification">>, EsipaReq, absent) + )}, + {provideEimPackageResult, + from_base64asn1( + 'SGP32Definitions', + 'ProvideEimPackageResult', + maps:get(<<"provideEimPackageResult">>, EsipaReq, absent) + )} + ]), + EsipaReqAsn = {handleNotificationEsipa, Req}, + + EimToIpaAsn = esipa_asn1_handler:handle_asn1(Pid, EsipaReqAsn), + + case EimToIpaAsn of + emptyResponse -> + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }} + ]); + _ -> + join([{header, #{<<"functionExecutionStatus">> => #{<<"status">> => <<"Failed">>}}}]) + end; +%GSMA SGP.32, section 6.4.1.8 +handle_json(Pid, {<<"/gsma/rsp2/esipa/cancelSession">>, EsipaReq}) -> + Req = join([ + {transactionId, to_binary(maps:get(<<"transactionId">>, EsipaReq))}, + {cancelSessionResponse, + from_base64asn1( + 'SGP32Definitions', + 'SGP32-CancelSessionResponse', + maps:get(<<"cancelSessionResponse">>, EsipaReq) + )} + ]), + EsipaReqAsn = {cancelSessionRequestEsipa, Req}, + + EimToIpaAsn = esipa_asn1_handler:handle_asn1(Pid, EsipaReqAsn), + + case EimToIpaAsn of + {cancelSessionResponseEsipa, {cancelSessionOk, _CancelSessionOk}} -> + join([ + {header, #{ + <<"functionExecutionStatus">> => #{<<"status">> => <<"Executed-Success">>} + }} + ]); + {cancelSessionResponseEsipa, {cancelSessionError, _CancelSessionError}} -> + join([{header, #{<<"functionExecutionStatus">> => #{<<"status">> => <<"Failed">>}}}]); + _ -> + join([{header, #{<<"functionExecutionStatus">> => #{<<"status">> => <<"Failed">>}}}]) + end; +%Unsupported request +handle_json(Pid, Request) -> + mnesia_db_work:finish(Pid, [{[{procedureError, abortedOrder}]}], unsupported), + logger:info( + "Handling of IPAd request failed, the request type is unsupported,~nRequest=~p,~nPid=~p~n", + [Request, Pid] + ), + {error, unsupported_request}. diff --git a/src/esipa_json_http_handler.erl b/src/esipa_json_http_handler.erl new file mode 100644 index 0000000..7d2de97 --- /dev/null +++ b/src/esipa_json_http_handler.erl @@ -0,0 +1,66 @@ +% Copyright (c) 2025 Onomondo ApS & sysmocom - s.f.m.c. GmbH. All rights reserved. +% +% SPDX-License-Identifier: AGPL-3.0-only +% +-module(esipa_json_http_handler). +-behavior(cowboy_handler). + +-define(RESPONSE_HEADERS, #{ + <<"content-type">> => <<"application/json;charset=UTF-8">>, + <<"x-admin-protocol">> => <<"gsma/rsp/v2.1.0">> +}). + +-export([init/2, terminate/3]). + +init(Req0, State) -> + Req = + case cowboy_req:header(<<"content-type">>, Req0) of + <<"application/json;charset=UTF-8">> -> + {ok, Data, _Req1} = cowboy_req:read_body(Req0), + IpaToEim = jiffy:decode(Data, [return_maps]), + Path = cowboy_req:path(Req0), + logger:info( + "Handling incoming IPAd request (JSON): ~p,~nPeer=~p, Pid=~p~n", + [Path, maps:get(peer, Req0), maps:get(pid, Req0)] + ), + logger:debug( + "Rx ESipa JSON,~nPeer=~p, Pid=~p,~nIpaToEim=~p~n", + [maps:get(peer, Req0), maps:get(pid, Req0), IpaToEim] + ), + EimToIpa = esipa_json_handler:handle_json(maps:get(pid, Req0), {Path, IpaToEim}), + logger:debug( + "Tx ESipa JSON,~nPeer=~p,Pid=~p,~nEimToIpa=~p~n", + [maps:get(peer, Req0), maps:get(pid, Req0), EimToIpa] + ), + case EimToIpa of + {error, unsupported_request} -> + cowboy_req:reply( + 400, + ?RESPONSE_HEADERS, + <<"Unsupported Request">>, + Req0 + ); + _ -> + EncodedRespBody = jiffy:encode(EimToIpa, [force_utf8]), + cowboy_req:reply(200, ?RESPONSE_HEADERS, EncodedRespBody, Req0) + end; + _ -> + cowboy_req:reply(415, ?RESPONSE_HEADERS, <<"Unsupported content-type">>, Req0) + end, + {ok, Req, State}. + +% Handle termination of HTTP requests +terminate(Reason, Req0, _State) -> + case Reason of + normal -> + ok; + _ -> + mnesia_db_work:finish( + maps:get(pid, Req0), [{[{procedureError, abortedOrder}]}], Reason + ), + logger:info( + "Handling of IPAd request (JSON) terminated unexpectedly, Reason=~p Pid=~p~n", + [Reason, maps:get(pid, Req0)] + ), + cowboy_req:reply(500, ?RESPONSE_HEADERS, <<"Internal Server Error">>, Req0) + end. diff --git a/src/onomondo_eim_app.erl b/src/onomondo_eim_app.erl index 89ffd4a..e796c96 100644 --- a/src/onomondo_eim_app.erl +++ b/src/onomondo_eim_app.erl @@ -51,7 +51,7 @@ cowboy_router:compile([ {'_', [ % SGP.32 Section 6.4.1 - {"/gsma/rsp2/esipa/[...]", esipa_json_handler, []}, + {"/gsma/rsp2/esipa/[...]", esipa_json_http_handler, []}, % SGP.32 Section 6.1.1: Any function execution request using ASN.1 binding SHALL be sent to the generic % HTTP path 'gsma/rsp2/asn1' {"/gsma/rsp2/asn1", esipa_asn1_http_handler, []}, -- To view, visit https://gerrit.osmocom.org/c/onomondo-eim/+/43175?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: newchange Gerrit-Project: onomondo-eim Gerrit-Branch: master Gerrit-Change-Id: Id73ae78a976608fe6f367f8c3b92d8b7b4e195d4 Gerrit-Change-Number: 43175 Gerrit-PatchSet: 1 Gerrit-Owner: dexter <pmaier(a)sysmocom.de>
1 0
0 0
[XS] Change in onomondo-eim[master]: WIP: SGP32Definitions: apply (questionable) workaround
by dexter 07 Aug '26

07 Aug '26
dexter has uploaded this change for review. ( https://gerrit.osmocom.org/c/onomondo-eim/+/43177?usp=email ) Change subject: WIP: SGP32Definitions: apply (questionable) workaround ...................................................................... WIP: SGP32Definitions: apply (questionable) workaround Depends: osmo-ttcn3-hacks.git Change-Id I4e9834a8a5ae29397542c696c0bc81692d4d564b Change-Id: I566571778d2433bd87a46276e81d9d526ae97325 Related: SYS#8100 --- M asn1/SGP32Definitions.asn1 1 file changed, 3 insertions(+), 3 deletions(-) git pull ssh://gerrit.osmocom.org:29418/onomondo-eim refs/changes/77/43177/1 diff --git a/asn1/SGP32Definitions.asn1 b/asn1/SGP32Definitions.asn1 index d5500bf..76cae00 100644 --- a/asn1/SGP32Definitions.asn1 +++ b/asn1/SGP32Definitions.asn1 @@ -696,9 +696,8 @@ tbsOtherNotification NotificationMetadata, euiccNotificationSignature [APPLICATION 55] OCTET STRING -- eUICC signature of tbsOtherNotification, Tag '5F37' } --- workaround for erlang asn1ct: remove already specified context-specific tag [65] -- workaround: add prefix ("SGP32-") to avoid clash with SGP.22 -SGP32-CancelSessionResponse ::= CHOICE { -- Tag 'BF41' +SGP32-CancelSessionResponse ::= [65] CHOICE { -- Tag 'BF41' cancelSessionResponseOk CancelSessionResponseOk, cancelSessionResponseError INTEGER {invalidTransactionId(5), undefinedError(127)}, compactCancelSessionResponseOk CompactCancelSessionResponseOk @@ -822,8 +821,9 @@ pendingNotification [0] SGP32-PendingNotification, -- A Notification to be delivered to a Notification Receiver, possibly in compact format provideEimPackageResult [80] ProvideEimPackageResult -- Tag 'BF50' } +-- workaround: explicitly assign tag [0] to member transactionId, exactly like it is done with other esipa-types CancelSessionRequestEsipa ::= [65] SEQUENCE { -- Tag 'BF41' - transactionId TransactionId, -- The TransactionID generated by the SM-DP+ + transactionId [0] TransactionId, -- The TransactionID generated by the SM-DP+ cancelSessionResponse SGP32-CancelSessionResponse -- This is the response from ES10b. CancelSession function, possibly in compact format } CancelSessionResponseEsipa ::= [65] CHOICE { -- Tag 'BF41' -- To view, visit https://gerrit.osmocom.org/c/onomondo-eim/+/43177?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: newchange Gerrit-Project: onomondo-eim Gerrit-Branch: master Gerrit-Change-Id: I566571778d2433bd87a46276e81d9d526ae97325 Gerrit-Change-Number: 43177 Gerrit-PatchSet: 1 Gerrit-Owner: dexter <pmaier(a)sysmocom.de>
1 0
0 0
[XS] Change in onomondo-eim[master]: SGP32Definitions: remove wrong comment about workaround
by dexter 07 Aug '26

07 Aug '26
dexter has uploaded this change for review. ( https://gerrit.osmocom.org/c/onomondo-eim/+/43174?usp=email ) Change subject: SGP32Definitions: remove wrong comment about workaround ...................................................................... SGP32Definitions: remove wrong comment about workaround This patch removes a misplaced workaround comment. The ASN.1 structures in questions are equal to the original spec and were not modified. Related: SYS#8100 Change-Id: Ibba9f380f32ddb947210cedf028460deb22e22ea --- M asn1/SGP32Definitions.asn1 1 file changed, 0 insertions(+), 2 deletions(-) git pull ssh://gerrit.osmocom.org:29418/onomondo-eim refs/changes/74/43174/1 diff --git a/asn1/SGP32Definitions.asn1 b/asn1/SGP32Definitions.asn1 index dffa6c8..cc052b4 100644 --- a/asn1/SGP32Definitions.asn1 +++ b/asn1/SGP32Definitions.asn1 @@ -607,11 +607,9 @@ parametersNotAvailable(1), undefinedError(127) } --- workaround for erlang asn1ct: remove already specified context-specific tag SGP32-SetDefaultDpAddressRequest ::= [101] SEQUENCE { -- Tag 'BF65' defaultDpAddress UTF8String -- Default SM-DP+ address as an FQDN } --- workaround for erlang asn1ct: remove already specified context-specific tag SGP32-SetDefaultDpAddressResponse ::= [101] SEQUENCE { -- Tag 'BF65' setDefaultDpAddressResult INTEGER { ok (0), undefinedError (127)} } -- To view, visit https://gerrit.osmocom.org/c/onomondo-eim/+/43174?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: newchange Gerrit-Project: onomondo-eim Gerrit-Branch: master Gerrit-Change-Id: Ibba9f380f32ddb947210cedf028460deb22e22ea Gerrit-Change-Number: 43174 Gerrit-PatchSet: 1 Gerrit-Owner: dexter <pmaier(a)sysmocom.de>
1 0
0 0
[M] Change in pysim[master]: GP: LOAD/STORE DATA chunk size from SCP overhead
by laforge 07 Aug '26

07 Aug '26
Attention is currently required from: Hoernchen. laforge has posted comments on this change by Hoernchen. ( https://gerrit.osmocom.org/c/pysim/+/43172?usp=email ) Change subject: GP: LOAD/STORE DATA chunk size from SCP overhead ...................................................................... Patch Set 1: Code-Review+1 -- To view, visit https://gerrit.osmocom.org/c/pysim/+/43172?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: comment Gerrit-Project: pysim Gerrit-Branch: master Gerrit-Change-Id: Ic208f3959a38896f64fb6ccefb24cc360a3ac3a2 Gerrit-Change-Number: 43172 Gerrit-PatchSet: 1 Gerrit-Owner: Hoernchen <ewild(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-Attention: Hoernchen <ewild(a)sysmocom.de> Gerrit-Comment-Date: Fri, 07 Aug 2026 07:50:24 +0000 Gerrit-HasComments: No Gerrit-Has-Labels: Yes
1 0
0 0
[M] Change in pysim[master]: GP: mixed PSK TLS PUT KEY (Amendment B Table 3-13)
by laforge 07 Aug '26

07 Aug '26
Attention is currently required from: Hoernchen, dexter, lynxis lazus. laforge has posted comments on this change by Hoernchen. ( https://gerrit.osmocom.org/c/pysim/+/43136?usp=email ) Change subject: GP: mixed PSK TLS PUT KEY (Amendment B Table 3-13) ...................................................................... Patch Set 1: (2 comments) File pySim/global_platform/__init__.py: https://gerrit.osmocom.org/c/pysim/+/43136/comment/f9c966a0_e5a90a56?usp=em… : PS1, Line 655: (b'\x85' + bertlv_encode_len(len(block)) + block + : bytes([len(kcv)]) + kcv) I would have preferred the use of our object-oriented BER_TLV_IE encoder for the 0x85 as well as some new Transcodable-derived class for the LV KCV section. That would feel more "pysim-onic" to me ,than the low-level byte-concatenation here in a rather high-level method. Or alternatively some kind of costruct. https://gerrit.osmocom.org/c/pysim/+/43136/comment/fb5dd59c_19f11f10?usp=em… : PS1, Line 1113: wouldn't this return more than the three bytes that the comment above indicates? But then, I see, the existing functions above do the same and the truncation happens in the shared compute_kcv() below. -- To view, visit https://gerrit.osmocom.org/c/pysim/+/43136?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: comment Gerrit-Project: pysim Gerrit-Branch: master Gerrit-Change-Id: I480a9d049a052aa5ae54fe6e2771dba44e89434d Gerrit-Change-Number: 43136 Gerrit-PatchSet: 1 Gerrit-Owner: Hoernchen <ewild(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: dexter <pmaier(a)sysmocom.de> Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-Reviewer: lynxis lazus <lynxis(a)fe80.eu> Gerrit-Attention: Hoernchen <ewild(a)sysmocom.de> Gerrit-Attention: lynxis lazus <lynxis(a)fe80.eu> Gerrit-Attention: dexter <pmaier(a)sysmocom.de> Gerrit-Comment-Date: Fri, 07 Aug 2026 07:48:29 +0000 Gerrit-HasComments: Yes Gerrit-Has-Labels: No
1 0
0 0
[M] Change in ...pyosmocom[master]: tlv: preserve the comprehension bit
by Hoernchen 06 Aug '26

06 Aug '26
Attention is currently required from: laforge. Hoernchen has posted comments on this change by Hoernchen. ( https://gerrit.osmocom.org/c/python/pyosmocom/+/43171?usp=email ) Change subject: tlv: preserve the comprehension bit ...................................................................... Patch Set 1: (1 comment) File src/osmocom/tlv.py: https://gerrit.osmocom.org/c/python/pyosmocom/+/43171/comment/97067343_d68b… : PS1, Line 638: if x.tag > 0xff: : # two byte tag: the comprehension bit is 0x8000! : if x.tag & 0x8000 == 0: : print("Fixing up COMPR_TLV_IE class %s: tag=0x%x has no comprehension bit" % (name, x.tag)) : x.tag = x.tag | 0x8000 > what exactly is the difference here? isn't this change semantically identical? No, because the original condition was AND, so if either part didnt match it would fall through to the elif and that is what clobbered the two byte tag. As the comment says two byte is 0x8000 - precisely the part in the previous AND that would fail. -- To view, visit https://gerrit.osmocom.org/c/python/pyosmocom/+/43171?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: comment Gerrit-Project: python/pyosmocom Gerrit-Branch: master Gerrit-Change-Id: Ifae37785e2a586d9130d154bc7244f35fa6f2a55 Gerrit-Change-Number: 43171 Gerrit-PatchSet: 1 Gerrit-Owner: Hoernchen <ewild(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-Attention: laforge <laforge(a)osmocom.org> Gerrit-Comment-Date: Thu, 06 Aug 2026 21:31:37 +0000 Gerrit-HasComments: Yes Gerrit-Has-Labels: No Comment-In-Reply-To: laforge <laforge(a)osmocom.org>
1 0
0 0
[M] Change in pysim[master]: GP: LOAD/STORE DATA chunk size from SCP overhead
by Hoernchen 06 Aug '26

06 Aug '26
Hoernchen has uploaded this change for review. ( https://gerrit.osmocom.org/c/pysim/+/43172?usp=email ) Change subject: GP: LOAD/STORE DATA chunk size from SCP overhead ...................................................................... GP: LOAD/STORE DATA chunk size from SCP overhead SCP.overhead was so far set at construction time (SCP02: 8, SCP03: s_mode), so the C-MAC length only. Unfortunately sec lvl >= 3 pads the data field to the cipher block size before encryption, so the real worst-case overhead is larger, scc.max_cmd_len (255 - overhead) was too big, and ADF_SD.load() used a hardcoded chunk_len=240. Real world issue with a 286 byte CAP + SCP02 + sec lvl 3: - 240-byte LOAD block is padded to 248, - encrypted - gets 8 byte C-MAC appended -> Lc = 256 That dies with a weird "ValueError: bytes must be in range(0, 256)". The only "fix" for that was to downgrade the seclevel. STORE DATA has the same overflow with large max_cmd_len (247 + padding + MAC = 256 as well). Therefore the overhead must be properly calculated from the sec level. While at it adjust the error in case I missed something to get a more useful ValueError. Change-Id: Ic208f3959a38896f64fb6ccefb24cc360a3ac3a2 --- M pySim/global_platform/__init__.py M pySim/global_platform/scp.py M tests/unittests/test_globalplatform.py 3 files changed, 220 insertions(+), 12 deletions(-) git pull ssh://gerrit.osmocom.org:29418/pysim refs/changes/72/43172/1 diff --git a/pySim/global_platform/__init__.py b/pySim/global_platform/__init__.py index 31f3907..d14c6ff 100644 --- a/pySim/global_platform/__init__.py +++ b/pySim/global_platform/__init__.py @@ -868,23 +868,32 @@ load_parser_from_grp.add_argument('--from-hex', type=is_hexstr, help='load from hex string') load_parser_from_grp.add_argument('--from-file', type=argparse.FileType('rb', 0), help='load from binary file') load_parser_from_grp.add_argument('--from-cap-file', type=argparse.FileType('rb', 0), help='load from JAVA-card CAP file') + load_parser.add_argument('--chunk-len', type=auto_uint8, default=None, + help='Block size for the LOAD command; default: as large as the current secure channel overhead permits, at most 240') @cmd2.with_argparser(load_parser) def do_load(self, opts): """Perform a GlobalPlatform LOAD command. (We currently only support loading without DAP and without ciphering.)""" if opts.from_hex is not None: - self.load(h2b(opts.from_hex)) + self.load(h2b(opts.from_hex), opts.chunk_len) elif opts.from_file is not None: - self.load(opts.from_file.read()) + self.load(opts.from_file.read(), opts.chunk_len) elif opts.from_cap_file is not None: cap = CapFile(opts.from_cap_file) - self.load(cap.get_loadfile()) + self.load(cap.get_loadfile(), opts.chunk_len) else: raise ValueError('load source not specified!') - def load(self, contents:bytes, chunk_len:int = 240): - # TODO:tune chunk_len based on the overhead of the used SCP? + def load(self, contents:bytes, chunk_len:Optional[int] = None): + # scc.max_cmd_len knows the overhead the currently active SCP + # 240 is the old default, keep it for now. + max_chunk_len = self._cmd.lchan.scc.max_cmd_len + if chunk_len is None: + chunk_len = min(240, max_chunk_len) + elif not 1 <= chunk_len <= max_chunk_len: + raise ValueError('chunk_len must be in range 1..%u (limited by the overhead of the current secure channel)' % + max_chunk_len) # build TLV according to GPC_SPE_034 section 11.6.2.3 / Table 11-58 for unencrypted case remainder = b'\xC4' + bertlv_encode_len(len(contents)) + contents # transfer this in various chunks to the card @@ -923,6 +932,8 @@ install_cap_parser_inst_prm_grp.add_argument('--install-parameters-stk', type=is_hexstr, default=None, help='Load Parameters (ETSI TS 102 226, section 8.2.1.3.2.1)') + install_cap_parser.add_argument('--chunk-len', type=auto_uint8, default=None, + help='Block size for the LOAD command; default: as large as the current secure channel overhead permits, at most 240') @cmd2.with_argparser(install_cap_parser) def do_install_cap(self, opts): @@ -961,7 +972,7 @@ self._cmd.poutput("step #1: install for load...") self.do_install_for_load("--load-file-aid %s --security-domain-aid %s" % (load_file_aid, security_domain_aid)) self._cmd.poutput("step #2: load...") - self.load(load_file) + self.load(load_file, opts.chunk_len) self._cmd.poutput("step #3: install_for_install (and make selectable)...") self.do_install_for_install("--load-file-aid %s --module-aid %s --application-aid %s --install-parameters %s --make-selectable" % (load_file_aid, module_aid, application_aid, install_parameters)) diff --git a/pySim/global_platform/scp.py b/pySim/global_platform/scp.py index a5fcf51..7573b47 100644 --- a/pySim/global_platform/scp.py +++ b/pySim/global_platform/scp.py @@ -182,6 +182,20 @@ """Should we perform R-ENC?""" return self.security_level & 0x20 + @property + def overhead(self) -> int: + """Worst-case len that wrapping a command APDU adds to its data field at the + current sec level is (255 - overhead), C-MAC + C-DECRYPTION encryption padding.""" + if not self.do_cmac: + return 0 + if not self.do_cenc: + return self.mac_len + # C-DECRYPTION pads with ('80'+['00'...] at least 1 byte) up to + # the cipher block size + C-MAC on top -> largest usable data field + # is one byte less than the largest block-size multiple within 255 - mac_len. + bs = self.sk.blocksize + return 255 - ((255 - self.mac_len) // bs * bs - 1) + def __str__(self) -> str: return "%s[%02x]" % (self.__class__.__name__, self.security_level) @@ -260,10 +274,8 @@ # Key Version Number 0x70 is a non-spec special-case of sysmoISIM-SJA2/SJA5 and possibly more sysmocom products # Key Version Number 0x01 is a non-spec special-case of sysmoUSIM-SJS1 kvn_ranges = [[0x01, 0x01], [0x20, 0x2f], [0x70, 0x70]] - - def __init__(self, *args, **kwargs): - self.overhead = 8 - super().__init__(*args, **kwargs) + # C-MAC (Single DES + final 3DES, B.1.2.2) is always one full DES block + mac_len = 8 def dek_encrypt(self, plaintext:bytes) -> bytes: # See also GPC section B.1.1.2, E.4.7, and E.4.1 @@ -338,10 +350,16 @@ # CMAC on modified APDU mlc = lc + 8 clac = cla | CLA_SM + if mlc >= 256: + raise ValueError('Modified Lc (%u) would exceed maximum when appending 8 bytes of mac' % mlc) mac = self.sk.calc_mac_1des(bytes([clac]) + apdu[1:4] + bytes([mlc]) + data) if self.do_cenc: + padded_data = pad80(data, 8) + if len(padded_data) + 8 >= 256: + raise ValueError('Modified Lc (%u) would exceed maximum when appending padding and mac' % + (len(padded_data) + 8)) k = DES3.new(self.sk.enc, DES.MODE_CBC, b'\x00'*8) - data = k.encrypt(pad80(data, 8)) + data = k.encrypt(padded_data) lc = len(data) lc += 8 @@ -477,9 +495,13 @@ def __init__(self, *args, **kwargs): self.s_mode = kwargs.pop('s_mode', 8) - self.overhead = self.s_mode super().__init__(*args, **kwargs) + @property + def mac_len(self) -> int: + # C-MAC truncated to 8 in S8 or 16 bytes in S16 mode + return self.s_mode + def dek_encrypt(self, plaintext:bytes) -> bytes: cipher = AES.new(self.card_keys.dek, AES.MODE_CBC, b'\x00'*16) return cipher.encrypt(plaintext) diff --git a/tests/unittests/test_globalplatform.py b/tests/unittests/test_globalplatform.py index d016d7d..98ff02e 100644 --- a/tests/unittests/test_globalplatform.py +++ b/tests/unittests/test_globalplatform.py @@ -424,5 +424,180 @@ load_parameters = gen_install_parameters() self.assertEqual(load_parameters, 'c900') +class SCP_Overhead_Test(unittest.TestCase): + """SCP.overhead varies according to the current security level: + C-MAC + at level >= 3 the worst-case padding! + """ + + def _scp02(self, security_level): + scp = SCP02(card_keys=ck_3des_70) + scp.sk = Scp02SessionKeys(0x0001, ck_3des_70) + scp.security_level = security_level + return scp + + def _scp03(self, security_level, s_mode=8): + scp = SCP03(card_keys=KEYSET_AES128, s_mode=s_mode) + scp.sk = Scp03SessionKeys(KEYSET_AES128, b'\x00' * s_mode, b'\x11' * s_mode) + scp.security_level = security_level + return scp + + def test_scp02(self): + self.assertEqual(self._scp02(0x00).overhead, 0) # no wrapping at all + self.assertEqual(self._scp02(0x01).overhead, 8) # C-MAC + self.assertEqual(self._scp02(0x03).overhead, 16) # C-MAC + C-DEC: pad80 to 8, largest fit 239 + + def test_scp03_s8(self): + self.assertEqual(self._scp03(0x00).overhead, 0) + self.assertEqual(self._scp03(0x01).overhead, 8) + self.assertEqual(self._scp03(0x03).overhead, 16) # pad80 to 16 within 247 -> 240, minus pad byte + self.assertEqual(self._scp03(0x33).overhead, 16) # R-MAC/R-ENC add no *command* overhead + + def test_scp03_s16(self): + self.assertEqual(self._scp03(0x01, s_mode=16).overhead, 16) + self.assertEqual(self._scp03(0x03, s_mode=16).overhead, 32) # pad80 to 16 within 239 -> 224, minus pad byte + + +class SCP_Lc_Limit_Test_Base: + """Test wrap_cmd_apdu() boundary handling: data of (255 - overhead) must produce Lc <= 255 else ValueError""" + + def _load_apdu(self, data_len): + return h2b('80E80000') + bytes([data_len]) + b'\xa5' * data_len + + def _check_boundary(self, scp): + fits = 255 - scp.overhead + wrapped = scp.wrap_cmd_apdu(self._load_apdu(fits)) + self.assertLessEqual(wrapped[4], 255) + self.assertEqual(len(wrapped), 5 + wrapped[4]) # case #3: header + Lc bytes, no Le + with self.assertRaises(ValueError) as ctx: + scp.wrap_cmd_apdu(self._load_apdu(fits + 1)) + self.assertIn('Lc', str(ctx.exception)) + + +class SCP02_Lc_Limit_Test(SCP_Lc_Limit_Test_Base, unittest.TestCase): + """Same session vectors as SCP02_Auth_Test""" + + def setUp(self): + self.scp02 = SCP02(card_keys=ck_3des_70) + self.scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8')) + self.scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3')) + self.scp02.gen_ext_auth_apdu() + + def test_cmac_only(self): + self.scp02.security_level = 0x01 + self._check_boundary(self.scp02) # 247 fits, 248 raises + + def test_cmac_cdec(self): + self.scp02.security_level = 0x03 + self._check_boundary(self.scp02) # 239 fits (-> Lc 248), 240 raises (would be 256) + + def test_cmac_cdec_wrapped_lc(self): + # my actual failing case: 240 bytes at level 3 + self.scp02.security_level = 0x03 + wrapped = self.scp02.wrap_cmd_apdu(self._load_apdu(239)) + self.assertEqual(wrapped[4], 248) # 239 -> pad80 -> 240 ciphertext + 8 mac + + +class SCP03_Lc_Limit_Test(SCP_Lc_Limit_Test_Base, unittest.TestCase): + """Session keys derived directly""" + + def _scp03(self, security_level, s_mode): + scp = SCP03(card_keys=KEYSET_AES128, s_mode=s_mode) + scp.sk = Scp03SessionKeys(KEYSET_AES128, b'\x00' * s_mode, b'\x11' * s_mode) + scp.security_level = security_level + return scp + + def test_s8_cmac_only(self): + self._check_boundary(self._scp03(0x01, 8)) # 247 fits, 248 raises + + def test_s8_cmac_cdec(self): + self._check_boundary(self._scp03(0x03, 8)) # 239 fits, 240 raises + + def test_s16_cmac_only(self): + self._check_boundary(self._scp03(0x01, 16)) # 239 fits, 240 raises + + def test_s16_cmac_cdec(self): + self._check_boundary(self._scp03(0x03, 16)) # 223 fits, 224 raises + + +class _FakeSccForLoad: + """mock lchan.scc: records LOAD APDUs, optionally wrapping them through a real SCP + instance first where the Lc overflow used to blow up""" + + def __init__(self, max_cmd_len=255, scp=None): + self.max_cmd_len = max_cmd_len + self.scp = scp + self.sent = [] + self.wrapped = [] + + def send_apdu_checksw(self, apdu, sw='9000'): + self.sent.append(apdu.lower()) + if self.scp: + self.wrapped.append(self.scp.wrap_cmd_apdu(h2b(apdu))) + return ('', '9000') + + +class Load_ChunkLen_Test(unittest.TestCase): + """ADF_SD.load() chunking: block size must use scc.max_cmd_len""" + + payload = b'\xaa' * 500 # actual real world case LOAD TLV: C4 + 8201f4 + 500 = 504 total + + def _sd(self, scc): + cmd = type('_Cmd', (), {'lchan': type('_Lchan', (), {'scc': scc})(), + 'poutput': lambda self, *args: None})() + # cmd2 CommandSet has a r/o _cmd property -> shadow it + _SD = type('_SD', (ADF_SD.AddlShellCommands,), {'_cmd': cmd}) + return _SD.__new__(_SD) + + def _blocks(self, scc): + """Get (p1, p2, lc) from LOAD APDU""" + for apdu in scc.sent: + self.assertEqual(apdu[0:4], '80e8') + yield int(apdu[4:6], 16), int(apdu[6:8], 16), int(apdu[8:10], 16) + + def test_default_no_scp(self): + """Without SCP the old 240 byte block size is kept, no idea what else might rely on this number""" + scc = _FakeSccForLoad(max_cmd_len=255) + self._sd(scc).load(self.payload) + blocks = list(self._blocks(scc)) + self.assertEqual([b[2] for b in blocks], [240, 240, 24]) + self.assertEqual([b[0] for b in blocks], [0x00, 0x00, 0x80]) # P1: last block flagged + self.assertEqual([b[1] for b in blocks], [0, 1, 2]) # P2: block num + + def test_default_scp02_level3(self): + """max_cmd_len 239 (SCP02 lvl 3) squeezes the blocks""" + scc = _FakeSccForLoad(max_cmd_len=239) + self._sd(scc).load(self.payload) + self.assertEqual([b[2] for b in list(self._blocks(scc))], [239, 239, 26]) + + def test_explicit_chunk_len(self): + scc = _FakeSccForLoad(max_cmd_len=255) + self._sd(scc).load(self.payload, chunk_len=100) + self.assertEqual([b[2] for b in list(self._blocks(scc))], [100] * 5 + [4]) + + def test_explicit_chunk_len_too_large(self): + scc = _FakeSccForLoad(max_cmd_len=239) + with self.assertRaises(ValueError): + self._sd(scc).load(self.payload, chunk_len=240) + self.assertEqual(scc.sent, []) # nothing sent! + + def test_explicit_chunk_len_zero(self): + scc = _FakeSccForLoad(max_cmd_len=255) + with self.assertRaises(ValueError): + self._sd(scc).load(self.payload, chunk_len=0) + + def test_end_to_end_scp02_level3(self): + """original failure: 286 byte CAP + SCP02 lvl 3""" + scp02 = SCP02(card_keys=ck_3des_70) + scp02.gen_init_update_apdu(host_challenge=h2b('40A62C37FA6304F8')) + scp02.parse_init_update_resp(h2b('00000000000000000000700200016B4524ABEE7CF32EA3838BC148F3')) + scp02.gen_ext_auth_apdu() + scp02.security_level = 0x03 + scc = _FakeSccForLoad(max_cmd_len=255 - scp02.overhead, scp=scp02) + self._sd(scc).load(b'\x5a' * 286) + self.assertEqual(len(scc.sent), 2) # 289 byte TLV in blocks of 239 + for wrapped in scc.wrapped: + self.assertLessEqual(wrapped[4], 255) + + if __name__ == "__main__": unittest.main() -- To view, visit https://gerrit.osmocom.org/c/pysim/+/43172?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: newchange Gerrit-Project: pysim Gerrit-Branch: master Gerrit-Change-Id: Ic208f3959a38896f64fb6ccefb24cc360a3ac3a2 Gerrit-Change-Number: 43172 Gerrit-PatchSet: 1 Gerrit-Owner: Hoernchen <ewild(a)sysmocom.de>
1 0
0 0
[M] Change in ...pyosmocom[master]: tlv: preserve the comprehension bit
by laforge 06 Aug '26

06 Aug '26
Attention is currently required from: Hoernchen. laforge has posted comments on this change by Hoernchen. ( https://gerrit.osmocom.org/c/python/pyosmocom/+/43171?usp=email ) Change subject: tlv: preserve the comprehension bit ...................................................................... Patch Set 1: Code-Review+1 (1 comment) File src/osmocom/tlv.py: https://gerrit.osmocom.org/c/python/pyosmocom/+/43171/comment/1567ed43_a4ce… : PS1, Line 638: if x.tag > 0xff: : # two byte tag: the comprehension bit is 0x8000! : if x.tag & 0x8000 == 0: : print("Fixing up COMPR_TLV_IE class %s: tag=0x%x has no comprehension bit" % (name, x.tag)) : x.tag = x.tag | 0x8000 what exactly is the difference here? isn't this change semantically identical? -- To view, visit https://gerrit.osmocom.org/c/python/pyosmocom/+/43171?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: comment Gerrit-Project: python/pyosmocom Gerrit-Branch: master Gerrit-Change-Id: Ifae37785e2a586d9130d154bc7244f35fa6f2a55 Gerrit-Change-Number: 43171 Gerrit-PatchSet: 1 Gerrit-Owner: Hoernchen <ewild(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-Attention: Hoernchen <ewild(a)sysmocom.de> Gerrit-Comment-Date: Thu, 06 Aug 2026 16:33:59 +0000 Gerrit-HasComments: Yes Gerrit-Has-Labels: Yes
1 0
0 0
[M] Change in onomondo-eim[master]: esipa_rest_utils: add support for PSMOs added in SGP.32 V.1.2
by dexter 06 Aug '26

06 Aug '26
Attention is currently required from: fixeria, jolly. dexter has posted comments on this change by dexter. ( https://gerrit.osmocom.org/c/onomondo-eim/+/42881?usp=email ) Change subject: esipa_rest_utils: add support for PSMOs added in SGP.32 V.1.2 ...................................................................... Patch Set 9: (1 comment) File contrib/rest_api_resource_schema.json: https://gerrit.osmocom.org/c/onomondo-eim/+/42881/comment/452d1efe_6e49b3cd… : PS9, Line 169: , > This would allow 0-9 and A-F and ','. I see it also in other definitions above and below. Are you sure? I always thought the ',' is a delimiter. I used SGP.32, section 6.4.1. as an example. They also use a ',' in their definitions. -- To view, visit https://gerrit.osmocom.org/c/onomondo-eim/+/42881?usp=email To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings?usp=email Gerrit-MessageType: comment Gerrit-Project: onomondo-eim Gerrit-Branch: master Gerrit-Change-Id: I03cdd70065a83dfc611d614cf32d817c13fad347 Gerrit-Change-Number: 42881 Gerrit-PatchSet: 9 Gerrit-Owner: dexter <pmaier(a)sysmocom.de> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: fixeria <vyanitskiy(a)sysmocom.de> Gerrit-Reviewer: laforge <laforge(a)osmocom.org> Gerrit-CC: jolly <andreas(a)eversberg.eu> Gerrit-Attention: jolly <andreas(a)eversberg.eu> Gerrit-Attention: fixeria <vyanitskiy(a)sysmocom.de> Gerrit-Comment-Date: Thu, 06 Aug 2026 15:57:55 +0000 Gerrit-HasComments: Yes Gerrit-Has-Labels: No Comment-In-Reply-To: jolly <andreas(a)eversberg.eu>
1 0
0 0
  • ← Newer
  • 1
  • ...
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • ...
  • 227
  • Older →

HyperKitty Powered by HyperKitty version 1.3.12.