Files
pysim/pySim/esim/es8p.py
Harald Welte 888c6e5647 add contrib/es9p_client: Perform ES9+ client functions like LPA+eUICC
This tool can be used to test the SM-DP+. It implements the full dance
of all HTTPs API operations to get to the downloadProfile, and will
decrypt the BPP to the UPP, which is then subsequently stored as file on
disk.

Needless to say, this will only work if you have an eUICC certificate +
private key that is compatible with the CI of your SM-DP+.

Change-Id: Idf8881e82f9835f5221c58b78ced9937cf5fb520
2024-06-09 12:18:16 +02:00

266 lines
12 KiB
Python

# Implementation of GSMA eSIM RSP (Remote SIM Provisioning) ES8+
# as per SGP22 v3.0 Section 5.5
#
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import Dict, List, Optional
from cryptography.hazmat.primitives.asymmetric import ec
from pySim.utils import b2h, h2b, bertlv_encode_tag, bertlv_encode_len, bertlv_parse_one_rawtag
from pySim.utils import bertlv_return_one_rawtlv
import pySim.esim.rsp as rsp
from pySim.esim.bsp import BspInstance
# Given that GSMA RSP uses ASN.1 in a very weird way, we actually cannot encode the full data type before
# signing, but we have to build parts of it separately first, then sign that, so we can put the signature
# into the same sequence as the signed data. We use the existing pySim TLV code for this.
def wrap_as_der_tlv(tag: int, val: bytes) -> bytes:
"""Wrap the 'value' into a DER-encoded TLV."""
return bertlv_encode_tag(tag) + bertlv_encode_len(len(val)) + val
def gen_init_sec_chan_signed_part(iscsp: Dict) -> bytes:
"""Generate the concatenated remoteOpId, transactionId, controlRefTemplate and smdpOtpk data objects
without the outer SEQUENCE tag / length or the remainder of initialiseSecureChannel, as is required
for signing purpose."""
out = b''
out += wrap_as_der_tlv(0x82, bytes([iscsp['remoteOpId']]))
out += wrap_as_der_tlv(0x80, iscsp['transactionId'])
crt = iscsp['controlRefTemplate']
out_crt = wrap_as_der_tlv(0x80, crt['keyType'])
out_crt += wrap_as_der_tlv(0x81, crt['keyLen'])
out_crt += wrap_as_der_tlv(0x84, crt['hostId'])
out += wrap_as_der_tlv(0xA6, out_crt)
out += wrap_as_der_tlv(0x5F49, iscsp['smdpOtpk'])
return out
# SGP.22 Section 5.5.1
def gen_initialiseSecureChannel(transactionId: str, host_id: bytes, smdp_otpk: bytes, euicc_otpk: bytes, dp_pb):
"""Generate decoded representation of (signed) initialiseSecureChannel (SGP.22 5.5.2)"""
init_scr = { 'remoteOpId': 1, # installBoundProfilePackage
'transactionId': h2b(transactionId),
# GlobalPlatform Card Specification Amendment F [13] section 6.5.2.3 for the Mutual Authentication Data Field
'controlRefTemplate': { 'keyType': bytes([0x88]), 'keyLen': bytes([16]), 'hostId': host_id },
'smdpOtpk': smdp_otpk, # otPK.DP.KA
}
to_sign = gen_init_sec_chan_signed_part(init_scr) + wrap_as_der_tlv(0x5f49, euicc_otpk)
init_scr['smdpSign'] = dp_pb.ecdsa_sign(to_sign)
return init_scr
def gen_replace_session_keys(ppk_enc: bytes, ppk_cmac: bytes, initial_mcv: bytes) -> bytes:
"""Generate encoded (but unsigned) ReplaceSessionKeysReqest DO (SGP.22 5.5.4)"""
rsk = { 'ppkEnc': ppk_enc, 'ppkCmac': ppk_cmac, 'initialMacChainingValue': initial_mcv }
return rsp.asn1.encode('ReplaceSessionKeysRequest', rsk)
class ProfileMetadata:
"""Representation of Profile metadata. Right now only the mandatory bits are
supported, but in general this should follow the StoreMetadataRequest of SGP.22 5.5.3"""
def __init__(self, iccid_bin: bytes, spn: str, profile_name: str):
self.iccid_bin = iccid_bin
self.spn = spn
self.profile_name = profile_name
def gen_store_metadata_request(self) -> bytes:
"""Generate encoded (but unsigned) StoreMetadataReqest DO (SGP.22 5.5.3)"""
smr = {
'iccid': self.iccid_bin,
'serviceProviderName': self.spn,
'profileName': self.profile_name,
}
return rsp.asn1.encode('StoreMetadataRequest', smr)
class ProfilePackage:
def __init__(self, metadata: Optional[ProfileMetadata] = None):
self.metadata = metadata
class UnprotectedProfilePackage(ProfilePackage):
"""Representing an unprotected profile package (UPP) as defined in SGP.22 Section 2.5.2"""
@classmethod
def from_der(cls, der: bytes, metadata: Optional[ProfileMetadata] = None) -> 'UnprotectedProfilePackage':
"""Load an UPP from its DER representation."""
inst = cls(metadata=metadata)
cls.der = der
# TODO: we later certainly want to parse it so we can perform modification (IMSI, key material, ...)
# just like in the traditional SIM/USIM dynamic data phase at the end of personalization
return inst
def to_der(self):
"""Return the DER representation of the UPP."""
# TODO: once we work on decoded structures, we may want to re-encode here
return self.der
class ProtectedProfilePackage(ProfilePackage):
"""Representing a protected profile package (PPP) as defined in SGP.22 Section 2.5.3"""
@classmethod
def from_upp(cls, upp: UnprotectedProfilePackage, bsp: BspInstance) -> 'ProtectedProfilePackage':
"""Generate the PPP as a sequence of encrypted and MACed Command TLVs representing the UPP"""
inst = cls(metadata=upp.metadata)
inst.upp = upp
# store ppk-enc, ppc-mac
inst.ppk_enc = bsp.c_algo.s_enc
inst.ppk_mac = bsp.m_algo.s_mac
inst.initial_mcv = bsp.m_algo.mac_chain
inst.encoded = bsp.encrypt_and_mac(0x86, upp.to_der())
return inst
#def __val__(self):
#return self.encoded
class BoundProfilePackage(ProfilePackage):
"""Representing a bound profile package (BPP) as defined in SGP.22 Section 2.5.4"""
@classmethod
def from_ppp(cls, ppp: ProtectedProfilePackage):
inst = cls()
inst.upp = None
inst.ppp = ppp
return inst
@classmethod
def from_upp(cls, upp: UnprotectedProfilePackage):
inst = cls()
inst.upp = upp
inst.ppp = None
return inst
def encode(self, ss: 'RspSessionState', dp_pb: 'CertAndPrivkey') -> bytes:
"""Generate a bound profile package (SGP.22 2.5.4)."""
def encode_seq(tag: int, sequence: List[bytes]) -> bytes:
"""Encode a "sequenceOfXX" as specified in SGP.22 specifying the raw SEQUENCE OF tag,
and assuming the caller provides the fully-encoded (with TAG + LEN) member TLVs."""
payload = b''.join(sequence)
return bertlv_encode_tag(tag) + bertlv_encode_len(len(payload)) + payload
bsp = BspInstance.from_kdf(ss.shared_secret, 0x88, 16, ss.host_id, h2b(ss.eid))
iscr = gen_initialiseSecureChannel(ss.transactionId, ss.host_id, ss.smdp_otpk, ss.euicc_otpk, dp_pb)
# generate unprotected input data
conf_idsp_bin = rsp.asn1.encode('ConfigureISDPRequest', {})
if self.upp:
smr_bin = self.upp.metadata.gen_store_metadata_request()
else:
smr_bin = self.ppp.metadata.gen_store_metadata_request()
# we don't use rsp.asn1.encode('boundProfilePackage') here, as the BSP already provides
# fully encoded + MACed TLVs including their tag + length values. We cannot put those as
# 'value' input into an ASN.1 encoder, as that would double the TAG + LENGTH :(
# 'initialiseSecureChannelRequest'
bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr)
# firstSequenceOf87
bpp_seq += encode_seq(0xa0, bsp.encrypt_and_mac(0x87, conf_idsp_bin))
# sequenceOF88
bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin))
if self.ppp: # we have to use session keys
rsk_bin = gen_replace_session_keys(self.ppp.ppk_enc, self.ppp.ppk_mac, self.ppp.initial_mcv)
# secondSequenceOf87
bpp_seq += encode_seq(0xa2, bsp.encrypt_and_mac(0x87, rsk_bin))
else:
self.ppp = ProtectedProfilePackage.from_upp(self.upp, bsp)
# 'sequenceOf86'
bpp_seq += encode_seq(0xa3, self.ppp.encoded)
# manual DER encode: wrap in outer SEQUENCE
return bertlv_encode_tag(0xbf36) + bertlv_encode_len(len(bpp_seq)) + bpp_seq
def decode(self, euicc_ot, eid: str, bpp_bin: bytes):
"""Decode a BPP into the PPP and subsequently UPP. This is what happens inside an eUICC."""
def split_bertlv_sequence(sequence: bytes) -> List[bytes]:
remainder = sequence
ret = []
while remainder:
_tag, _l, tlv, remainder = bertlv_return_one_rawtlv(remainder)
ret.append(tlv)
return ret
# we don't use rsp.asn1.decode('boundProfilePackage') here, as the BSP needs
# fully encoded + MACed TLVs including their tag + length values.
#bpp = rsp.asn1.decode('BoundProfilePackage', bpp_bin)
tag, _l, v, _remainder = bertlv_parse_one_rawtag(bpp_bin)
if len(_remainder):
raise ValueError('Excess data at end of TLV')
if tag != 0xbf36:
raise ValueError('Unexpected outer tag: %s' % tag)
# InitialiseSecureChannelRequest
tag, _l, iscr_bin, remainder = bertlv_return_one_rawtlv(v)
iscr = rsp.asn1.decode('InitialiseSecureChannelRequest', iscr_bin)
# configureIsdpRequest
tag, _l, firstSeqOf87, remainder = bertlv_parse_one_rawtag(remainder)
if tag != 0xa0:
raise ValueError("Unexpected 'firstSequenceOf87' tag: %s" % tag)
firstSeqOf87 = split_bertlv_sequence(firstSeqOf87)
# storeMetadataRequest
tag, _l, seqOf88, remainder = bertlv_parse_one_rawtag(remainder)
if tag != 0xa1:
raise ValueError("Unexpected 'sequenceOf88' tag: %s" % tag)
seqOf88 = split_bertlv_sequence(seqOf88)
tag, _l, tlv, remainder = bertlv_parse_one_rawtag(remainder)
if tag == 0xa2:
secondSeqOf87 = split_bertlv_sequence(tlv)
tag2, _l, seqOf86, remainder = bertlv_parse_one_rawtag(remainder)
if tag2 != 0xa3:
raise ValueError("Unexpected 'sequenceOf86' tag: %s" % tag)
seqOf86 = split_bertlv_sequence(seqOf86)
elif tag == 0xa3:
secondSeqOf87 = None
seqOf86 = split_bertlv_sequence(tlv)
else:
raise ValueError("Unexpected 'secondSequenceOf87' tag: %s" % tag)
# extract smdoOtpk from initialiseSecureChannel
smdp_otpk = iscr['smdpOtpk']
# Generate Session Keys using the CRT, opPK.DP.ECKA and otSK.EUICC.ECKA according to annex G
smdp_public_key = ec.EllipticCurvePublicKey.from_encoded_point(euicc_ot.curve, smdp_otpk)
self.shared_secret = euicc_ot.exchange(ec.ECDH(), smdp_public_key)
crt = iscr['controlRefTemplate']
bsp = BspInstance.from_kdf(self.shared_secret, int.from_bytes(crt['keyType'], 'big'), int.from_bytes(crt['keyLen'], 'big'), crt['hostId'], h2b(eid))
self.encoded_configureISDPRequest = bsp.demac_and_decrypt(firstSeqOf87)
self.configureISDPRequest = rsp.asn1.decode('ConfigureISDPRequest', self.encoded_configureISDPRequest)
self.encoded_storeMetadataRequest = bsp.demac_only(seqOf88)
self.storeMetadataRequest = rsp.asn1.decode('StoreMetadataRequest', self.encoded_storeMetadataRequest)
if secondSeqOf87 != None:
rsk_bin = bsp.demac_and_decrypt(secondSeqOf87)
rsk = rsp.asn1.decode('ReplaceSessionKeysRequest', rsk_bin)
# process replace_session_keys!
bsp = BspInstance(rsk['ppkEnc'], rsk['ppkCmac'], rsk['initialMacChainingValue'])
self.replaceSessionKeysRequest = rsk
self.upp = bsp.demac_and_decrypt(seqOf86)
return self.upp