% Copyright (c) 2025 Onomondo ApS & sysmocom - s.f.m.c. GmbH. All rights reserved. % % SPDX-License-Identifier: AGPL-3.0-only % % Author: Philipp Maier / sysmocom - s.f.m.c. GmbH %TODO: This code still lacks the verification of signatures, the reason for this is that it was still not possible to %verify a signature in practice. -module(crypto_utils). -export([ sign_euiccPackageSigned/2, verify_euiccPackageResultSigned/2, store_euicc_pubkey_from_authenticateResponseOk/2, store_euicc_pubkey_from_ipaEuiccDataResponse/2 ]). %Convert from DER encoded signature format to plain format (see also BSI TR03111 5.2.1) der_to_plain(DERSignature) -> {ok, [R, S]} = 'DERSignature':decode('DERSignature', DERSignature), RPlain = utils:lpad_binary(utils:integer_to_bytes(R), <<0>>, 32), SPlain = utils:lpad_binary(utils:integer_to_bytes(S), <<0>>, 32), utils:join_binary_list([RPlain, SPlain]). %Convert from plain format to DER encoded signature format (see also BSI TR03111 5.2.1) plain_to_der(PlainSignature) -> R = binary_part(PlainSignature, 0, 32), S = binary_part(PlainSignature, 32, 32), DERSignature = [binary:decode_unsigned(R), binary:decode_unsigned(S)], {ok, DERSignatureEncoded} = 'DERSignature':encode('DERSignature', DERSignature), DERSignatureEncoded. %Encode the association token as BER TLV IE enc_association_token(AssociationToken) -> %TODO: replace this with a proper ASN.1 encoding function. AssociationTokenBinary = utils:integer_to_bytes(AssociationToken), AssociationTokenLength = utils:integer_to_bytes(byte_size(AssociationTokenBinary)), utils:join_binary_list([<<132>>, AssociationTokenLength, AssociationTokenBinary]). sign_euiccPackageSigned(EuiccPackageSigned, EidValue) -> % Read the AssociationToken {ok, AssociationToken} = mnesia_db:euicc_param_get(EidValue, associationToken), %Format message to be signed {ok, EuiccPackageSignedEnc} = 'SGP32Definitions':encode( 'EuiccPackageSigned', EuiccPackageSigned ), MsgToBeSigned = utils:join_binary_list([ EuiccPackageSignedEnc, enc_association_token(AssociationToken) ]), %Load private key from eIM certificate {ok, EimKeyPath} = application:get_env(onomondo_eim, eim_key), {ok, EimKeyPem} = file:read_file(EimKeyPath), [EimKeyPemEntry] = public_key:pem_decode(EimKeyPem), EimKeyECPrivateKey = public_key:pem_entry_decode(EimKeyPemEntry), %Sign message % We use SHA-256 as signature hash/digest, see also GSMA SGP.22, section 2.6.5 and % https://www.erlang.org/doc/apps/public_key/public_key#sign/4 der_to_plain(public_key:sign(MsgToBeSigned, sha256, EimKeyECPrivateKey)). verify_signature(Message, Signature, EidValue) -> DERSignature = plain_to_der(Signature), {ok, SubjectPublicKeyHex} = mnesia_db:euicc_param_get(EidValue, signPubKey), SubjectPublicKey = utils:hex_to_binary(SubjectPublicKeyHex), {ok, SignAlgo} = mnesia_db:euicc_param_get(EidValue, signAlgo), NamedCurve = case SignAlgo of <<"prime256v1">> -> {1, 2, 840, 10045, 3, 1, 7}; <<"brainpoolP256r1">> -> {1, 3, 36, 3, 3, 2, 8, 1, 1, 7}; _ -> logger:error("invalid SignAlgo configured for eID: ~p~n", [ utils:binary_to_hex(EidValue) ]), {} end, ECPublicKey = {{'ECPoint', SubjectPublicKey}, {namedCurve, NamedCurve}}, Result = public_key:verify(Message, sha256, DERSignature, ECPublicKey), case Result of true -> ok; _ -> logger:error( "Signature verification failed for eID ~p, input parameters:~n" ++ "SubjectPublicKey=~p~n" ++ "NamedCurve=~p~n" ++ "Signature=~p~n" ++ "DERSignature=~p~n" ++ "Message=~p~n", [ utils:binary_to_hex(EidValue), utils:binary_to_hex(SubjectPublicKey), NamedCurve, utils:binary_to_hex(Signature), utils:binary_to_hex(DERSignature), utils:binary_to_hex(Message) ] ), error end. verify_euiccPackageResultSigned(EuiccPackageResult, EidValue) -> {ok, ConsumerEuicc} = mnesia_db:euicc_param_get(EidValue, consumerEuicc), case ConsumerEuicc of false -> % Read the AssociationToken {ok, AssociationToken} = mnesia_db:euicc_param_get(EidValue, associationToken), case EuiccPackageResult of {euiccPackageResultSigned, EuiccPackageResultSigned} -> EuiccPackageResultDataSigned = maps:get( euiccPackageResultDataSigned, EuiccPackageResultSigned ), EuiccSignEPR = maps:get(euiccSignEPR, EuiccPackageResultSigned), {ok, EuiccPackageResultDataSigned_enc} = 'SGP32Definitions':encode( 'EuiccPackageResultDataSigned', EuiccPackageResultDataSigned ), % "euiccSignEPR SHALL apply on the concatenated data objects euiccPackageResultDataSigned and % eimSignature." (see also GSMA SGP.32, section 2.11.2.1) MsgToBeVerfied = utils:join_binary_list([ EuiccPackageResultDataSigned_enc, enc_association_token(AssociationToken) ]), verify_signature(MsgToBeVerfied, EuiccSignEPR, EidValue); {euiccPackageErrorSigned, EuiccPackageErrorSigned} -> EuiccPackageErrorDataSigned = maps:get( euiccPackageErrorDataSigned, EuiccPackageErrorSigned ), EuiccSignEPE = maps:get(euiccSignEPE, EuiccPackageErrorSigned), {ok, EuiccPackageErrorDataSigned_enc} = 'SGP32Definitions':encode( 'EuiccPackageErrorDataSigned', EuiccPackageErrorDataSigned ), % "euiccSignEPE SHALL apply on the concatenated data objects euiccPackageErrorDataSigned and % eimSignature." (see also GSMA SGP.32, section 2.11.2.1) MsgToBeVerfied = utils:join_binary_list([ EuiccPackageErrorDataSigned_enc, enc_association_token(AssociationToken) ]), verify_signature(MsgToBeVerfied, EuiccSignEPE, EidValue); {euiccPackageErrorUnsigned, _} -> % This result has no signature ok; _ -> error end; _ -> logger:info( "omitting signature check for euiccPackageResultSigned from eID ~p (consumer eUICC)~n", [utils:binary_to_hex(EidValue)] ), ok end. pubkey_from_cert(Cert) -> TbsCertificate = maps:get(tbsCertificate, Cert), SubjectPublicKeyInfo = maps:get(subjectPublicKeyInfo, TbsCertificate), Algorithm = maps:get(algorithm, SubjectPublicKeyInfo), SubjectPublicKey = maps:get(subjectPublicKey, SubjectPublicKeyInfo), BrainpoolP256r1 = #{ algorithm => {1, 2, 840, 10045, 2, 1}, parameters => <<6, 9, 43, 36, 3, 3, 2, 8, 1, 1, 7>> }, Prime256v1 = #{ algorithm => {1, 2, 840, 10045, 2, 1}, parameters => <<6, 8, 42, 134, 72, 206, 61, 3, 1, 7>> }, NamedCurve = case Algorithm of Prime256v1 -> {1, 2, 840, 10045, 3, 1, 7}; BrainpoolP256r1 -> {1, 3, 36, 3, 3, 2, 8, 1, 1, 7}; _ -> throw( "Incorrect root CI certificate, only BrainpoolP256r1 or Prime256v1 may be used!" ) end, {{'ECPoint', SubjectPublicKey}, {namedCurve, NamedCurve}}. verify_cert(TrustedCert, VerifyCert) -> {ok, VerifyCertBer} = 'PKIX1Explicit88':encode('Certificate', VerifyCert), ECPublicKey = pubkey_from_cert(TrustedCert), Result = public_key:pkix_verify(VerifyCertBer, ECPublicKey), case Result of true -> ok; _ -> logger:error( "Certificate verification failed,~nVerifyCert=~p,~nECPublicKey=~p~n", [VerifyCert, ECPublicKey] ), error end. get_root_cert(EumCertificate, []) -> logger:error( "Certificate verification failed, no root certificate found,~nEumCertificate=~p~n", [ EumCertificate ] ), error; get_root_cert(EumCertificate, RootCiCertPaths) -> [RootCiCertPath | RootCiCertPathsTail] = RootCiCertPaths, {ok, RootCiCertPem} = file:read_file(RootCiCertPath), [{'Certificate', RootCiCertBer, not_encrypted}] = public_key:pem_decode(RootCiCertPem), {ok, EumCertificateBer} = 'PKIX1Explicit88':encode('Certificate', EumCertificate), case public_key:pkix_is_issuer(EumCertificateBer, RootCiCertBer) of true -> {ok, RootCiCertPem}; _ -> get_root_cert(EumCertificate, RootCiCertPathsTail) end. verify_euicc_cert(EumCertificate, EuiccCertificate) -> {ok, RootCiCertPaths} = application:get_env(onomondo_eim, root_ci_certs), case get_root_cert(EumCertificate, RootCiCertPaths) of {ok, RootCiCertPem} -> [{'Certificate', RootCiCertBer, not_encrypted}] = public_key:pem_decode(RootCiCertPem), {ok, RootCiCert} = 'PKIX1Explicit88':decode('Certificate', RootCiCertBer), % TODO: The certificate chain validation done here only performs a basic signature validation. However, a % spec compliant certifiate chain verification should include: % % * expiration dates: no certificate in the chain must be expired. % * CRL (certificate revocation lists, indicated in the CI cert): no revoked cert should be accepted. % * serial number constraint of EUM certificate: first 8 digits of EID of eUICC certificate must be within % scope of EUM certificate. % * CA certificate must % - have extension for basic constraints CA=true % - have extension for key usage "keyCertSign" % * EUM certificate must % - have extension for basic constraints CA=true, pathLenConstraint == 0 % - have extension for key usage "keyCertSign" % * eUICC certificate must % - have extension for key usage "digitalSignature" case verify_cert(RootCiCert, EumCertificate) of ok -> case verify_cert(EumCertificate, EuiccCertificate) of ok -> ok; _ -> error end; _ -> error end; _ -> error end. store_euicc_pubkey(EumCertificate, EuiccCertificate, EidValue) -> case verify_euicc_cert(EumCertificate, EuiccCertificate) of ok -> {{'ECPoint', SignPubKey}, {namedCurve, NamedCurve}} = pubkey_from_cert( EuiccCertificate ), SignAlgo = case NamedCurve of {1, 2, 840, 10045, 3, 1, 7} -> <<"prime256v1">>; {1, 3, 36, 3, 3, 2, 8, 1, 1, 7} -> <<"brainpoolP256r1">>; _ -> <<"unknown">> end, ok = mnesia_db:euicc_param_set(EidValue, signPubKey, utils:binary_to_hex(SignPubKey)), ok = mnesia_db:euicc_param_set(EidValue, signAlgo, SignAlgo), ok; _ -> error end. store_euicc_pubkey_from_authenticateResponseOk(AuthRespOk, EidValue) -> case mnesia_db:euicc_param_get(EidValue, signPubKey) of {ok, <<>>} -> % There is no public key stored yet for this eUICC, use the public % key provided in the eUICC certificate EumCertificate = maps:get(eumCertificate, AuthRespOk), EuiccCertificate = maps:get(euiccCertificate, AuthRespOk), store_euicc_pubkey(EumCertificate, EuiccCertificate, EidValue); _ -> % There is already a public key stored for this eUICC ok end. store_euicc_pubkey_from_ipaEuiccDataResponse(IpaEuiccDataResponse, EidValue) -> case mnesia_db:euicc_param_get(EidValue, signPubKey) of {ok, <<>>} -> % There is no public key stored yet for this eUICC, use the public % key provided in the eUICC certificate case IpaEuiccDataResponse of {ipaEuiccData, IpaEuiccData} -> EumCertificatePresent = maps:is_key(eumCertificate, IpaEuiccData), EuiccCertificatePresent = maps:is_key(euiccCertificate, IpaEuiccData), if EumCertificatePresent and EuiccCertificatePresent -> EumCertificate = maps:get(eumCertificate, IpaEuiccData), EuiccCertificate = maps:get(euiccCertificate, IpaEuiccData), store_euicc_pubkey(EumCertificate, EuiccCertificate, EidValue); true -> % We need the eumCertificate and the euiccCertificate, if one of the two is missing, we can % not proceed. ok end; _ -> % The message format does not contain any public key information ok end; _ -> % There is already a public key stored for this eUICC ok end.