mirror of
https://gitea.osmocom.org/sim-card/pysim.git
synced 2026-03-23 05:48:34 +03:00
Compare commits
5 Commits
ewild/ossl
...
laforge/ws
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9bb63a2db | ||
|
|
a01e87da77 | ||
|
|
53e840ad86 | ||
|
|
d15c3d1319 | ||
|
|
671b0f19b6 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -7,10 +7,3 @@
|
||||
/.local
|
||||
/build
|
||||
/pySim.egg-info
|
||||
/smdpp-data/sm-dp-sessions*
|
||||
dist
|
||||
tags
|
||||
smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_NIST.pem
|
||||
smdpp-data/certs/DPtls/CERT_S_SM_DP_TLS_BRP.pem
|
||||
smdpp-data/generated
|
||||
smdpp-data/certs/dhparam2048.pem
|
||||
|
||||
29
README.md
29
README.md
@@ -1,29 +1,16 @@
|
||||
pySim - Tools for reading, decoding, browsing SIM/USIM/ISIM/HPSIM/eUICC Cards
|
||||
=============================================================================
|
||||
pySim - Read, Write and Browse Programmable SIM/USIM/ISIM/HPSIM Cards
|
||||
=====================================================================
|
||||
|
||||
This repository contains a number of Python programs related to working with
|
||||
subscriber identity modules of cellular networks, including but not limited
|
||||
to SIM, UICC, USIM, ISIM, HPSIMs and eUICCs.
|
||||
|
||||
* `pySim-shell.py` can be used to interactively explore, read and decode contents
|
||||
of any of the supported card models / card applications. Furthermore, if
|
||||
you have the credentials to your card (ADM PIN), you can also write to the card,
|
||||
i.e. edit its contents.
|
||||
* `pySim-read.py` and `pySim-prog.py` are _legacy_ tools for batch programming
|
||||
some very common parameters to an entire batch of programmable cards
|
||||
* `pySim-trace.py` is a tool to do an in-depth decode of SIM card protocol traces
|
||||
such as those obtained by [Osmocom SIMtrace2](https://osmocom.org/projects/simtrace2/wiki)
|
||||
or [osmo-qcdiag](https://osmocom.org/projects/osmo-qcdiag/wiki).
|
||||
* `osmo-smdpp.py` is a proof-of-concept GSMA SGP.22 Consumer eSIM SM-DP+ for lab/research
|
||||
* there are more related tools, particularly in the `contrib` directory.
|
||||
This repository contains a number of Python programs that can be used
|
||||
to read, program (write) and browse all fields/parameters/files on
|
||||
SIM/USIM/ISIM/HPSIM cards used in 3GPP cellular networks from 2G to 5G.
|
||||
|
||||
Note that the access control configuration of normal production cards
|
||||
issue by operators will restrict significantly which files a normal
|
||||
user can read, and particularly write to.
|
||||
|
||||
The full functionality of pySim hence can only be used with on so-called
|
||||
programmable SIM/USIM/ISIM/HPSIM cards, such as the various
|
||||
[sysmocom programmable card products](https://shop.sysmocom.de/SIM/).
|
||||
programmable SIM/USIM/ISIM/HPSIM cards.
|
||||
|
||||
Such SIM/USIM/ISIM/HPSIM cards are special cards, which - unlike those
|
||||
issued by regular commercial operators - come with the kind of keys that
|
||||
@@ -62,9 +49,9 @@ pySim-shell vs. legacy tools
|
||||
----------------------------
|
||||
|
||||
While you will find a lot of online resources still describing the use of
|
||||
`pySim-prog.py` and `pySim-read.py`, those tools are considered legacy by
|
||||
pySim-prog.py and pySim-read.py, those tools are considered legacy by
|
||||
now and have by far been superseded by the much more capable
|
||||
`pySim-shell.py`. We strongly encourage users to adopt pySim-shell, unless
|
||||
pySim-shell. We strongly encourage users to adopt pySim-shell, unless
|
||||
they have very specific requirements like batch programming of large
|
||||
quantities of cards, which is about the only remaining use case for the
|
||||
legacy tools.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
import copy
|
||||
import argparse
|
||||
from pySim.esim import es2p, ActivationCode
|
||||
from pySim.esim import es2p
|
||||
|
||||
EID_HELP='EID of the eUICC for which eSIM shall be made available'
|
||||
ICCID_HELP='The ICCID of the eSIM that shall be made available'
|
||||
@@ -73,11 +73,6 @@ if __name__ == '__main__':
|
||||
res = peer.call_downloadOrder(data)
|
||||
elif opts.command == 'confirm-order':
|
||||
res = peer.call_confirmOrder(data)
|
||||
matchingId = res.get('matchingId', None)
|
||||
smdpAddress = res.get('smdpAddress', None)
|
||||
if matchingId:
|
||||
ac = ActivationCode(smdpAddress, matchingId, cc_required=bool(opts.confirmationCode))
|
||||
print("Activation Code: '%s'" % ac.to_string())
|
||||
elif opts.command == 'cancel-order':
|
||||
res = peer.call_cancelOrder(data)
|
||||
elif opts.command == 'release-profile':
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Small command line utility program to encode eSIM QR-Codes
|
||||
|
||||
# (C) 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/>.
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
from pySim.esim import ActivationCode
|
||||
|
||||
|
||||
option_parser = argparse.ArgumentParser(description="""
|
||||
eSIM QR code generator. Will encode the given hostname + activation code
|
||||
into the eSIM RSP String format as specified in SGP.22 Section 4.1. If
|
||||
a PNG output file is specified, it will also generate a QR code.""")
|
||||
option_parser.add_argument('hostname', help='FQDN of SM-DP+')
|
||||
option_parser.add_argument('token', help='MatchingID / Token')
|
||||
option_parser.add_argument('--oid', help='SM-DP+ OID in CERT.DPauth.ECDSA')
|
||||
option_parser.add_argument('--confirmation-code-required', action='store_true',
|
||||
help='Whether a Confirmation Code is required')
|
||||
option_parser.add_argument('--png', help='Output PNG file name (no PNG is written if omitted)')
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = option_parser.parse_args()
|
||||
|
||||
ac = ActivationCode(opts.hostname, opts.token, opts.oid, opts.confirmation_code_required)
|
||||
print(ac.to_string())
|
||||
if opts.png:
|
||||
with open(opts.png, 'wb') as f:
|
||||
img = ac.to_qrcode()
|
||||
img.save(f)
|
||||
print("# generated QR code stored to '%s'" % (opts.png))
|
||||
@@ -1,661 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Faithfully reproduces the smdpp certs contained in SGP.26_v1.5_Certificates_18_07_2024.zip
|
||||
available at https://www.gsma.com/solutions-and-impact/technologies/esim/gsma_resources/sgp-26-test-certificate-definition-v1-5/
|
||||
Only usable for testing, it obviously uses a different CI key.
|
||||
"""
|
||||
|
||||
import os
|
||||
import binascii
|
||||
from datetime import datetime
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID, ExtensionOID
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
# Custom OIDs used in certificates
|
||||
OID_CERTIFICATE_POLICIES_CI = "2.23.146.1.2.1.0" # CI cert policy
|
||||
OID_CERTIFICATE_POLICIES_TLS = "2.23.146.1.2.1.3" # DPtls cert policy
|
||||
OID_CERTIFICATE_POLICIES_AUTH = "2.23.146.1.2.1.4" # DPauth cert policy
|
||||
OID_CERTIFICATE_POLICIES_PB = "2.23.146.1.2.1.5" # DPpb cert policy
|
||||
|
||||
# Subject Alternative Name OIDs
|
||||
OID_CI_RID = "2.999.1" # CI Registered ID
|
||||
OID_DP_RID = "2.999.10" # DP+ Registered ID
|
||||
OID_DP2_RID = "2.999.12" # DP+2 Registered ID
|
||||
OID_DP4_RID = "2.999.14" # DP+4 Registered ID
|
||||
OID_DP8_RID = "2.999.18" # DP+8 Registered ID
|
||||
|
||||
|
||||
class SimplifiedCertificateGenerator:
|
||||
def __init__(self):
|
||||
self.backend = default_backend()
|
||||
# Store generated CI keys to sign other certs
|
||||
self.ci_certs = {} # {"BRP": cert, "NIST": cert}
|
||||
self.ci_keys = {} # {"BRP": key, "NIST": key}
|
||||
|
||||
def get_curve(self, curve_type):
|
||||
"""Get the appropriate curve object."""
|
||||
if curve_type == "BRP":
|
||||
return ec.BrainpoolP256R1()
|
||||
else:
|
||||
return ec.SECP256R1()
|
||||
|
||||
def generate_key_pair(self, curve):
|
||||
"""Generate a new EC key pair."""
|
||||
private_key = ec.generate_private_key(curve, self.backend)
|
||||
return private_key
|
||||
|
||||
def load_private_key_from_hex(self, hex_key, curve):
|
||||
"""Load EC private key from hex string."""
|
||||
key_bytes = binascii.unhexlify(hex_key.replace(":", "").replace(" ", "").replace("\n", ""))
|
||||
key_int = int.from_bytes(key_bytes, 'big')
|
||||
return ec.derive_private_key(key_int, curve, self.backend)
|
||||
|
||||
def generate_ci_cert(self, curve_type):
|
||||
"""Generate CI certificate for either BRP or NIST curve."""
|
||||
curve = self.get_curve(curve_type)
|
||||
private_key = self.generate_key_pair(curve)
|
||||
|
||||
# Build subject and issuer (self-signed) - same for both
|
||||
subject = issuer = x509.Name([
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, "Test CI"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "TESTCERT"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSPTEST"),
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, "IT"),
|
||||
])
|
||||
|
||||
# Build certificate - all parameters same for both
|
||||
builder = x509.CertificateBuilder()
|
||||
builder = builder.subject_name(subject)
|
||||
builder = builder.issuer_name(issuer)
|
||||
builder = builder.not_valid_before(datetime(2020, 4, 1, 8, 27, 51))
|
||||
builder = builder.not_valid_after(datetime(2055, 4, 1, 8, 27, 51))
|
||||
builder = builder.serial_number(0xb874f3abfa6c44d3)
|
||||
builder = builder.public_key(private_key.public_key())
|
||||
|
||||
# Add extensions - all same for both
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.BasicConstraints(ca=True, path_length=None),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.CertificatePolicies([
|
||||
x509.PolicyInformation(
|
||||
x509.ObjectIdentifier(OID_CERTIFICATE_POLICIES_CI),
|
||||
policy_qualifiers=None
|
||||
)
|
||||
]),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.KeyUsage(
|
||||
digital_signature=False,
|
||||
content_commitment=False,
|
||||
key_encipherment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
key_cert_sign=True,
|
||||
crl_sign=True,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName([
|
||||
x509.RegisteredID(x509.ObjectIdentifier(OID_CI_RID))
|
||||
]),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.CRLDistributionPoints([
|
||||
x509.DistributionPoint(
|
||||
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-A.crl")],
|
||||
relative_name=None,
|
||||
reasons=None,
|
||||
crl_issuer=None
|
||||
),
|
||||
x509.DistributionPoint(
|
||||
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
|
||||
relative_name=None,
|
||||
reasons=None,
|
||||
crl_issuer=None
|
||||
)
|
||||
]),
|
||||
critical=False
|
||||
)
|
||||
|
||||
certificate = builder.sign(private_key, hashes.SHA256(), self.backend)
|
||||
|
||||
self.ci_keys[curve_type] = private_key
|
||||
self.ci_certs[curve_type] = certificate
|
||||
|
||||
return certificate, private_key
|
||||
|
||||
def generate_dp_cert(self, curve_type, subject_cn, serial, key_hex,
|
||||
cert_policy_oid, rid_oid, validity_start, validity_end):
|
||||
"""Generate a DP certificate signed by CI - works for both BRP and NIST."""
|
||||
curve = self.get_curve(curve_type)
|
||||
private_key = self.load_private_key_from_hex(key_hex, curve)
|
||||
|
||||
ci_cert = self.ci_certs[curve_type]
|
||||
ci_key = self.ci_keys[curve_type]
|
||||
|
||||
subject = x509.Name([
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "ACME"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, subject_cn),
|
||||
])
|
||||
|
||||
builder = x509.CertificateBuilder()
|
||||
builder = builder.subject_name(subject)
|
||||
builder = builder.issuer_name(ci_cert.subject)
|
||||
builder = builder.not_valid_before(validity_start)
|
||||
builder = builder.not_valid_after(validity_end)
|
||||
builder = builder.serial_number(serial)
|
||||
builder = builder.public_key(private_key.public_key())
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.AuthorityKeyIdentifier.from_issuer_public_key(ci_key.public_key()),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName([
|
||||
x509.RegisteredID(x509.ObjectIdentifier(rid_oid))
|
||||
]),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.KeyUsage(
|
||||
digital_signature=True,
|
||||
content_commitment=False,
|
||||
key_encipherment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
key_cert_sign=False,
|
||||
crl_sign=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.CertificatePolicies([
|
||||
x509.PolicyInformation(
|
||||
x509.ObjectIdentifier(cert_policy_oid),
|
||||
policy_qualifiers=None
|
||||
)
|
||||
]),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.CRLDistributionPoints([
|
||||
x509.DistributionPoint(
|
||||
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-A.crl")],
|
||||
relative_name=None,
|
||||
reasons=None,
|
||||
crl_issuer=None
|
||||
),
|
||||
x509.DistributionPoint(
|
||||
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
|
||||
relative_name=None,
|
||||
reasons=None,
|
||||
crl_issuer=None
|
||||
)
|
||||
]),
|
||||
critical=False
|
||||
)
|
||||
|
||||
certificate = builder.sign(ci_key, hashes.SHA256(), self.backend)
|
||||
|
||||
return certificate, private_key
|
||||
|
||||
def generate_tls_cert(self, curve_type, subject_cn, dns_name, serial, key_hex,
|
||||
rid_oid, validity_start, validity_end):
|
||||
"""Generate a TLS certificate signed by CI."""
|
||||
curve = self.get_curve(curve_type)
|
||||
private_key = self.load_private_key_from_hex(key_hex, curve)
|
||||
|
||||
ci_cert = self.ci_certs[curve_type]
|
||||
ci_key = self.ci_keys[curve_type]
|
||||
|
||||
subject = x509.Name([
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "ACME"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, subject_cn),
|
||||
])
|
||||
|
||||
builder = x509.CertificateBuilder()
|
||||
builder = builder.subject_name(subject)
|
||||
builder = builder.issuer_name(ci_cert.subject)
|
||||
builder = builder.not_valid_before(validity_start)
|
||||
builder = builder.not_valid_after(validity_end)
|
||||
builder = builder.serial_number(serial)
|
||||
builder = builder.public_key(private_key.public_key())
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.KeyUsage(
|
||||
digital_signature=True,
|
||||
content_commitment=False,
|
||||
key_encipherment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
key_cert_sign=False,
|
||||
crl_sign=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.ExtendedKeyUsage([
|
||||
x509.oid.ExtendedKeyUsageOID.SERVER_AUTH,
|
||||
x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH
|
||||
]),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.CertificatePolicies([
|
||||
x509.PolicyInformation(
|
||||
x509.ObjectIdentifier(OID_CERTIFICATE_POLICIES_TLS),
|
||||
policy_qualifiers=None
|
||||
)
|
||||
]),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.AuthorityKeyIdentifier.from_issuer_public_key(ci_key.public_key()),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName([
|
||||
x509.DNSName(dns_name),
|
||||
x509.RegisteredID(x509.ObjectIdentifier(rid_oid))
|
||||
]),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.CRLDistributionPoints([
|
||||
x509.DistributionPoint(
|
||||
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-A.crl")],
|
||||
relative_name=None,
|
||||
reasons=None,
|
||||
crl_issuer=None
|
||||
),
|
||||
x509.DistributionPoint(
|
||||
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
|
||||
relative_name=None,
|
||||
reasons=None,
|
||||
crl_issuer=None
|
||||
)
|
||||
]),
|
||||
critical=False
|
||||
)
|
||||
|
||||
certificate = builder.sign(ci_key, hashes.SHA256(), self.backend)
|
||||
|
||||
return certificate, private_key
|
||||
|
||||
def generate_eum_cert(self, curve_type, key_hex):
|
||||
"""Generate EUM certificate signed by CI."""
|
||||
curve = self.get_curve(curve_type)
|
||||
private_key = self.load_private_key_from_hex(key_hex, curve)
|
||||
|
||||
ci_cert = self.ci_certs[curve_type]
|
||||
ci_key = self.ci_keys[curve_type]
|
||||
|
||||
subject = x509.Name([
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, "ES"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSP Test EUM"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, "EUM Test"),
|
||||
])
|
||||
|
||||
builder = x509.CertificateBuilder()
|
||||
builder = builder.subject_name(subject)
|
||||
builder = builder.issuer_name(ci_cert.subject)
|
||||
builder = builder.not_valid_before(datetime(2020, 4, 1, 9, 28, 37))
|
||||
builder = builder.not_valid_after(datetime(2054, 3, 24, 9, 28, 37))
|
||||
builder = builder.serial_number(0x12345678)
|
||||
builder = builder.public_key(private_key.public_key())
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.AuthorityKeyIdentifier.from_issuer_public_key(ci_key.public_key()),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.KeyUsage(
|
||||
digital_signature=False,
|
||||
content_commitment=False,
|
||||
key_encipherment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
key_cert_sign=True,
|
||||
crl_sign=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.CertificatePolicies([
|
||||
x509.PolicyInformation(
|
||||
x509.ObjectIdentifier("2.23.146.1.2.1.2"), # EUM policy
|
||||
policy_qualifiers=None
|
||||
)
|
||||
]),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectAlternativeName([
|
||||
x509.RegisteredID(x509.ObjectIdentifier("2.999.5"))
|
||||
]),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.BasicConstraints(ca=True, path_length=0),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.CRLDistributionPoints([
|
||||
x509.DistributionPoint(
|
||||
full_name=[x509.UniformResourceIdentifier("http://ci.test.example.com/CRL-B.crl")],
|
||||
relative_name=None,
|
||||
reasons=None,
|
||||
crl_issuer=None
|
||||
)
|
||||
]),
|
||||
critical=False
|
||||
)
|
||||
|
||||
# Name Constraints
|
||||
constrained_name = x509.Name([
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSP Test EUM"),
|
||||
x509.NameAttribute(NameOID.SERIAL_NUMBER, "89049032"),
|
||||
])
|
||||
|
||||
name_constraints = x509.NameConstraints(
|
||||
permitted_subtrees=[
|
||||
x509.DirectoryName(constrained_name)
|
||||
],
|
||||
excluded_subtrees=None
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
name_constraints,
|
||||
critical=True
|
||||
)
|
||||
|
||||
certificate = builder.sign(ci_key, hashes.SHA256(), self.backend)
|
||||
|
||||
return certificate, private_key
|
||||
|
||||
def generate_euicc_cert(self, curve_type, eum_cert, eum_key, key_hex):
|
||||
"""Generate eUICC certificate signed by EUM."""
|
||||
curve = self.get_curve(curve_type)
|
||||
private_key = self.load_private_key_from_hex(key_hex, curve)
|
||||
|
||||
subject = x509.Name([
|
||||
x509.NameAttribute(NameOID.COUNTRY_NAME, "ES"),
|
||||
x509.NameAttribute(NameOID.ORGANIZATION_NAME, "RSP Test EUM"),
|
||||
x509.NameAttribute(NameOID.SERIAL_NUMBER, "89049032123451234512345678901235"),
|
||||
x509.NameAttribute(NameOID.COMMON_NAME, "Test eUICC"),
|
||||
])
|
||||
|
||||
builder = x509.CertificateBuilder()
|
||||
builder = builder.subject_name(subject)
|
||||
builder = builder.issuer_name(eum_cert.subject)
|
||||
builder = builder.not_valid_before(datetime(2020, 4, 1, 9, 48, 58))
|
||||
builder = builder.not_valid_after(datetime(7496, 1, 24, 9, 48, 58))
|
||||
builder = builder.serial_number(0x0200000000000001)
|
||||
builder = builder.public_key(private_key.public_key())
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.AuthorityKeyIdentifier.from_issuer_public_key(eum_key.public_key()),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()),
|
||||
critical=False
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.KeyUsage(
|
||||
digital_signature=True,
|
||||
content_commitment=False,
|
||||
key_encipherment=False,
|
||||
data_encipherment=False,
|
||||
key_agreement=False,
|
||||
key_cert_sign=False,
|
||||
crl_sign=False,
|
||||
encipher_only=False,
|
||||
decipher_only=False
|
||||
),
|
||||
critical=True
|
||||
)
|
||||
|
||||
builder = builder.add_extension(
|
||||
x509.CertificatePolicies([
|
||||
x509.PolicyInformation(
|
||||
x509.ObjectIdentifier("2.23.146.1.2.1.1"), # eUICC policy
|
||||
policy_qualifiers=None
|
||||
)
|
||||
]),
|
||||
critical=True
|
||||
)
|
||||
|
||||
certificate = builder.sign(eum_key, hashes.SHA256(), self.backend)
|
||||
|
||||
return certificate, private_key
|
||||
|
||||
def save_cert_and_key(self, cert, key, cert_path_der, cert_path_pem, key_path_sk, key_path_pk):
|
||||
"""Save certificate and key in various formats."""
|
||||
# Create directories if needed
|
||||
os.makedirs(os.path.dirname(cert_path_der), exist_ok=True)
|
||||
|
||||
with open(cert_path_der, "wb") as f:
|
||||
f.write(cert.public_bytes(serialization.Encoding.DER))
|
||||
|
||||
if cert_path_pem:
|
||||
with open(cert_path_pem, "wb") as f:
|
||||
f.write(cert.public_bytes(serialization.Encoding.PEM))
|
||||
|
||||
if key and key_path_sk:
|
||||
with open(key_path_sk, "wb") as f:
|
||||
f.write(key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
))
|
||||
|
||||
if key and key_path_pk:
|
||||
with open(key_path_pk, "wb") as f:
|
||||
f.write(key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
))
|
||||
|
||||
|
||||
def main():
|
||||
gen = SimplifiedCertificateGenerator()
|
||||
|
||||
output_dir = "smdpp-data/generated"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
print("=== Generating CI Certificates ===")
|
||||
|
||||
for curve_type in ["BRP", "NIST"]:
|
||||
ci_cert, ci_key = gen.generate_ci_cert(curve_type)
|
||||
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
|
||||
gen.save_cert_and_key(
|
||||
ci_cert, ci_key,
|
||||
f"{output_dir}/CertificateIssuer/CERT_CI{suffix}.der",
|
||||
f"{output_dir}/CertificateIssuer/CERT_CI{suffix}.pem",
|
||||
None, None
|
||||
)
|
||||
print(f"Generated CI {curve_type} certificate")
|
||||
|
||||
print("\n=== Generating DPauth Certificates ===")
|
||||
|
||||
dpauth_configs = [
|
||||
("BRP", "TEST SM-DP+", 256, "93:fb:33:d0:58:4f:34:9b:07:f8:b5:d2:af:93:d7:c3:e3:54:b3:49:a3:b9:13:50:2e:6a:bc:07:0e:4d:49:29", OID_DP_RID, "DPauth"),
|
||||
("NIST", "TEST SM-DP+", 256, "0a:7c:c1:c2:44:e6:0c:52:cd:5b:78:07:ab:8c:36:0c:26:52:46:01:50:7d:ca:bc:5d:d5:98:b5:a6:16:d5:d5", OID_DP_RID, "DPauth"),
|
||||
("BRP", "TEST SM-DP+2", 512, "0c:17:35:5c:01:1d:0f:e8:d7:da:dd:63:f1:97:85:cf:6c:51:cb:cd:46:6a:e8:8b:e8:f8:1b:c1:05:88:46:f6", OID_DP2_RID, "DP2auth"),
|
||||
("NIST", "TEST SM-DP+2", 512, "9c:32:a0:95:d4:88:42:d9:ff:a4:04:f7:12:51:2a:a2:c5:42:5a:1a:26:38:6a:b6:a1:45:d5:81:1e:03:91:41", OID_DP2_RID, "DP2auth"),
|
||||
]
|
||||
|
||||
for curve_type, cn, serial, key_hex, rid_oid, name_prefix in dpauth_configs:
|
||||
cert, key = gen.generate_dp_cert(
|
||||
curve_type, cn, serial, key_hex,
|
||||
OID_CERTIFICATE_POLICIES_AUTH, rid_oid,
|
||||
datetime(2020, 4, 1, 8, 31, 30),
|
||||
datetime(2030, 3, 30, 8, 31, 30)
|
||||
)
|
||||
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
|
||||
gen.save_cert_and_key(
|
||||
cert, key,
|
||||
f"{output_dir}/DPauth/CERT_S_SM_{name_prefix}{suffix}.der",
|
||||
None,
|
||||
f"{output_dir}/DPauth/SK_S_SM_{name_prefix}{suffix}.pem",
|
||||
f"{output_dir}/DPauth/PK_S_SM_{name_prefix}{suffix}.pem"
|
||||
)
|
||||
print(f"Generated {name_prefix} {curve_type} certificate")
|
||||
|
||||
print("\n=== Generating DPpb Certificates ===")
|
||||
|
||||
dppb_configs = [
|
||||
("BRP", "TEST SM-DP+", 257, "75:ff:32:2f:41:66:16:da:e1:a4:84:ef:71:d4:87:4f:b0:df:32:95:fd:35:c2:cb:a4:89:fb:b2:bb:9c:7b:f6", OID_DP_RID, "DPpb"),
|
||||
("NIST", "TEST SM-DP+", 257, "dc:d6:94:b7:78:95:7e:8e:9a:dd:bd:d9:44:33:e9:ef:8f:73:d1:1e:49:1c:48:d4:25:a3:8a:94:91:bd:3b:ed", OID_DP_RID, "DPpb"),
|
||||
("BRP", "TEST SM-DP+2", 513, "9c:ae:2e:1a:56:07:a9:d5:78:38:2e:ee:93:2e:25:1f:52:30:4f:86:ee:b1:f1:70:8c:db:d3:c0:7b:e2:cd:3d", OID_DP2_RID, "DP2pb"),
|
||||
("NIST", "TEST SM-DP+2", 513, "66:93:11:49:63:9d:ba:ac:1d:c3:d3:06:c5:8b:d2:df:d2:2f:73:bf:63:ac:86:31:98:32:90:b5:7f:90:93:45", OID_DP2_RID, "DP2pb"),
|
||||
]
|
||||
|
||||
for curve_type, cn, serial, key_hex, rid_oid, name_prefix in dppb_configs:
|
||||
cert, key = gen.generate_dp_cert(
|
||||
curve_type, cn, serial, key_hex,
|
||||
OID_CERTIFICATE_POLICIES_PB, rid_oid,
|
||||
datetime(2020, 4, 1, 8, 34, 46),
|
||||
datetime(2030, 3, 30, 8, 34, 46)
|
||||
)
|
||||
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
|
||||
gen.save_cert_and_key(
|
||||
cert, key,
|
||||
f"{output_dir}/DPpb/CERT_S_SM_{name_prefix}{suffix}.der",
|
||||
None,
|
||||
f"{output_dir}/DPpb/SK_S_SM_{name_prefix}{suffix}.pem",
|
||||
f"{output_dir}/DPpb/PK_S_SM_{name_prefix}{suffix}.pem"
|
||||
)
|
||||
print(f"Generated {name_prefix} {curve_type} certificate")
|
||||
|
||||
print("\n=== Generating DPtls Certificates ===")
|
||||
|
||||
dptls_configs = [
|
||||
("BRP", "testsmdpplus1.example.com", "testsmdpplus1.example.com", 9, "3f:67:15:28:02:b3:f4:c7:fa:e6:79:58:55:f6:82:54:1e:45:e3:5e:ff:f4:e8:a0:55:65:a0:f1:91:2a:78:2e", OID_DP_RID, "DP_TLS_BRP"),
|
||||
("NIST", "testsmdpplus1.example.com", "testsmdpplus1.example.com", 9, "a0:3e:7c:e4:55:04:74:be:a4:b7:a8:73:99:ce:5a:8c:9f:66:1b:68:0f:94:01:39:ff:f8:4e:9d:ec:6a:4d:8c", OID_DP_RID, "DP_TLS_NIST"),
|
||||
("NIST", "testsmdpplus2.example.com", "testsmdpplus2.example.com", 12, "4e:65:61:c6:40:88:f6:69:90:7a:db:e3:94:b1:1a:84:24:2e:03:3a:82:a8:84:02:31:63:6d:c9:1b:4e:e3:f5", OID_DP2_RID, "DP2_TLS"),
|
||||
("NIST", "testsmdpplus4.example.com", "testsmdpplus4.example.com", 14, "f2:65:9d:2f:52:8f:4b:11:37:40:d5:8a:0d:2a:f3:eb:2b:48:e1:22:c2:b6:0a:6a:f6:fc:96:ad:86:be:6f:a4", OID_DP4_RID, "DP4_TLS"),
|
||||
("NIST", "testsmdpplus8.example.com", "testsmdpplus8.example.com", 18, "ff:6e:4a:50:9b:ad:db:38:10:88:31:c2:3c:cc:2d:44:30:7a:f2:81:e9:25:96:7f:8c:df:1d:95:54:a0:28:8d", OID_DP8_RID, "DP8_TLS"),
|
||||
]
|
||||
|
||||
for curve_type, cn, dns, serial, key_hex, rid_oid, name_prefix in dptls_configs:
|
||||
cert, key = gen.generate_tls_cert(
|
||||
curve_type, cn, dns, serial, key_hex, rid_oid,
|
||||
datetime(2024, 7, 9, 15, 29, 36),
|
||||
datetime(2025, 8, 11, 15, 29, 36)
|
||||
)
|
||||
gen.save_cert_and_key(
|
||||
cert, key,
|
||||
f"{output_dir}/DPtls/CERT_S_SM_{name_prefix}.der",
|
||||
None,
|
||||
f"{output_dir}/DPtls/SK_S_SM_{name_prefix.replace('_BRP', '_BRP').replace('_NIST', '_NIST')}.pem",
|
||||
f"{output_dir}/DPtls/PK_S_SM_{name_prefix.replace('_BRP', '_BRP').replace('_NIST', '_NIST')}.pem"
|
||||
)
|
||||
print(f"Generated {name_prefix} certificate")
|
||||
|
||||
print("\n=== Generating EUM Certificates ===")
|
||||
|
||||
eum_configs = [
|
||||
("BRP", "12:9b:0a:b1:3f:17:e1:4a:40:b6:fa:4e:d8:23:e0:cf:46:5b:7b:3d:73:24:05:e6:29:5d:3b:23:b0:45:c9:9a"),
|
||||
("NIST", "25:e6:75:77:28:e1:e9:51:13:51:9c:dc:34:55:5c:29:ba:ed:23:77:3a:c5:af:dd:dc:da:d9:84:89:8a:52:f0"),
|
||||
]
|
||||
|
||||
eum_certs = {}
|
||||
eum_keys = {}
|
||||
|
||||
for curve_type, key_hex in eum_configs:
|
||||
cert, key = gen.generate_eum_cert(curve_type, key_hex)
|
||||
eum_certs[curve_type] = cert
|
||||
eum_keys[curve_type] = key
|
||||
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
|
||||
gen.save_cert_and_key(
|
||||
cert, key,
|
||||
f"{output_dir}/EUM/CERT_EUM{suffix}.der",
|
||||
None,
|
||||
f"{output_dir}/EUM/SK_EUM{suffix}.pem",
|
||||
f"{output_dir}/EUM/PK_EUM{suffix}.pem"
|
||||
)
|
||||
print(f"Generated EUM {curve_type} certificate")
|
||||
|
||||
print("\n=== Generating eUICC Certificates ===")
|
||||
|
||||
euicc_configs = [
|
||||
("BRP", "8d:c3:47:a7:6d:b7:bd:d6:22:2d:d7:5e:a1:a1:68:8a:ca:81:1e:4c:bc:6a:7f:6a:ef:a4:b2:64:19:62:0b:90"),
|
||||
("NIST", "11:e1:54:67:dc:19:4f:33:71:83:e4:60:c9:f6:32:60:09:1e:12:e8:10:26:cd:65:61:e1:7c:6d:85:39:cc:9c"),
|
||||
]
|
||||
|
||||
for curve_type, key_hex in euicc_configs:
|
||||
cert, key = gen.generate_euicc_cert(curve_type, eum_certs[curve_type], eum_keys[curve_type], key_hex)
|
||||
suffix = "_ECDSA_BRP" if curve_type == "BRP" else "_ECDSA_NIST"
|
||||
gen.save_cert_and_key(
|
||||
cert, key,
|
||||
f"{output_dir}/eUICC/CERT_EUICC{suffix}.der",
|
||||
None,
|
||||
f"{output_dir}/eUICC/SK_EUICC{suffix}.pem",
|
||||
f"{output_dir}/eUICC/PK_EUICC{suffix}.pem"
|
||||
)
|
||||
print(f"Generated eUICC {curve_type} certificate")
|
||||
|
||||
print("\n=== Certificate generation complete! ===")
|
||||
print(f"All certificates saved to: {output_dir}/")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -82,10 +82,6 @@ case "$JOB_TYPE" in
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
# XXX: workaround for https://github.com/python-cmd2/cmd2/issues/1414
|
||||
# 2.4.3 was the last stable release not affected by this bug (OS#6776)
|
||||
pip install cmd2==2.4.3
|
||||
|
||||
rm -rf docs/_build
|
||||
make -C "docs" html latexpdf
|
||||
|
||||
|
||||
@@ -19,13 +19,14 @@ import os
|
||||
import sys
|
||||
import argparse
|
||||
import logging
|
||||
import zipfile
|
||||
from pathlib import Path as PlPath
|
||||
from typing import List
|
||||
from osmocom.utils import h2b, b2h, swap_nibbles
|
||||
from osmocom.construct import GreedyBytes, StripHeaderAdapter
|
||||
|
||||
from pySim.esim.saip import *
|
||||
from pySim.esim.saip.validation import CheckBasicStructure
|
||||
from pySim import javacard
|
||||
from pySim.pprint import HexBytesPrettyPrinter
|
||||
|
||||
pp = HexBytesPrettyPrinter(indent=4,width=500)
|
||||
@@ -47,14 +48,9 @@ parser_dump.add_argument('--dump-decoded', action='store_true', help='Dump decod
|
||||
|
||||
parser_check = subparsers.add_parser('check', help='Run constraint checkers on PE-Sequence')
|
||||
|
||||
parser_rpe = subparsers.add_parser('extract-pe', help='Extract specified PE to (DER encoded) file')
|
||||
parser_rpe.add_argument('--pe-file', required=True, help='PE file name')
|
||||
parser_rpe.add_argument('--identification', type=int, help='Extract PE matching specified identification')
|
||||
|
||||
parser_rpe = subparsers.add_parser('remove-pe', help='Remove specified PEs from PE-Sequence')
|
||||
parser_rpe.add_argument('--output-file', required=True, help='Output file name')
|
||||
parser_rpe.add_argument('--identification', default=[], type=int, action='append', help='Remove PEs matching specified identification')
|
||||
parser_rpe.add_argument('--type', default=[], action='append', help='Remove PEs matching specified type')
|
||||
parser_rpe.add_argument('--identification', type=int, action='append', help='Remove PEs matching specified identification')
|
||||
|
||||
parser_rn = subparsers.add_parser('remove-naa', help='Remove speciifed NAAs from PE-Sequence')
|
||||
parser_rn.add_argument('--output-file', required=True, help='Output file name')
|
||||
@@ -62,59 +58,13 @@ parser_rn.add_argument('--naa-type', required=True, choices=NAAs.keys(), help='N
|
||||
# TODO: add an --naa-index or the like, so only one given instance can be removed
|
||||
|
||||
parser_info = subparsers.add_parser('info', help='Display information about the profile')
|
||||
parser_info.add_argument('--apps', action='store_true', help='List applications and their related instances')
|
||||
|
||||
parser_eapp = subparsers.add_parser('extract-apps', help='Extract applications as loadblock file')
|
||||
parser_eapp.add_argument('--output-dir', default='.', help='Output directory (where to store files)')
|
||||
parser_eapp.add_argument('--format', default='cap', choices=['ijc', 'cap'], help='Data format of output files')
|
||||
|
||||
parser_aapp = subparsers.add_parser('add-app', help='Add application to PE-Sequence')
|
||||
parser_aapp.add_argument('--output-file', required=True, help='Output file name')
|
||||
parser_aapp.add_argument('--applet-file', required=True, help='Applet file name')
|
||||
parser_aapp.add_argument('--aid', required=True, help='Load package AID')
|
||||
parser_aapp.add_argument('--sd-aid', default=None, help='Security Domain AID')
|
||||
parser_aapp.add_argument('--non-volatile-code-limit', default=None, type=int, help='Non volatile code limit (C6)')
|
||||
parser_aapp.add_argument('--volatile-data-limit', default=None, type=int, help='Volatile data limit (C7)')
|
||||
parser_aapp.add_argument('--non-volatile-data-limit', default=None, type=int, help='Non volatile data limit (C8)')
|
||||
parser_aapp.add_argument('--hash-value', default=None, help='Hash value')
|
||||
|
||||
parser_rapp = subparsers.add_parser('remove-app', help='Remove application from PE-Sequence')
|
||||
parser_rapp.add_argument('--output-file', required=True, help='Output file name')
|
||||
parser_rapp.add_argument('--aid', required=True, help='Load package AID')
|
||||
|
||||
parser_aappi = subparsers.add_parser('add-app-inst', help='Add application instance to Application PE')
|
||||
parser_aappi.add_argument('--output-file', required=True, help='Output file name')
|
||||
parser_aappi.add_argument('--aid', required=True, help='Load package AID')
|
||||
parser_aappi.add_argument('--class-aid', required=True, help='Class AID')
|
||||
parser_aappi.add_argument('--inst-aid', required=True, help='Instance AID (must match Load package AID)')
|
||||
parser_aappi.add_argument('--app-privileges', default='000000', help='Application privileges')
|
||||
parser_aappi.add_argument('--volatile-memory-quota', default=None, type=int, help='Volatile memory quota (C7)')
|
||||
parser_aappi.add_argument('--non-volatile-memory-quota', default=None, type=int, help='Non volatile memory quota (C8)')
|
||||
parser_aappi.add_argument('--app-spec-pars', default='00', help='Application specific parameters (C9)')
|
||||
parser_aappi.add_argument('--uicc-toolkit-app-spec-pars', help='UICC toolkit application specific parameters field')
|
||||
parser_aappi.add_argument('--uicc-access-app-spec-pars', help='UICC Access application specific parameters field')
|
||||
parser_aappi.add_argument('--uicc-adm-access-app-spec-pars', help='UICC Administrative access application specific parameters field')
|
||||
parser_aappi.add_argument('--process-data', default=[], action='append', help='Process personalization APDUs')
|
||||
|
||||
parser_rappi = subparsers.add_parser('remove-app-inst', help='Remove application instance from Application PE')
|
||||
parser_rappi.add_argument('--output-file', required=True, help='Output file name')
|
||||
parser_rappi.add_argument('--aid', required=True, help='Load package AID')
|
||||
parser_rappi.add_argument('--inst-aid', required=True, help='Instance AID')
|
||||
|
||||
esrv_flag_choices = [t.name for t in asn1.types['ServicesList'].type.root_members]
|
||||
parser_esrv = subparsers.add_parser('edit-mand-srv-list', help='Add/Remove service flag from/to mandatory services list')
|
||||
parser_esrv.add_argument('--output-file', required=True, help='Output file name')
|
||||
parser_esrv.add_argument('--add-flag', default=[], choices=esrv_flag_choices, action='append', help='Add flag to mandatory services list')
|
||||
parser_esrv.add_argument('--remove-flag', default=[], choices=esrv_flag_choices, action='append', help='Remove flag from mandatory services list')
|
||||
|
||||
parser_info = subparsers.add_parser('tree', help='Display the filesystem tree')
|
||||
|
||||
def write_pes(pes: ProfileElementSequence, output_file:str):
|
||||
"""write the PE sequence to a file"""
|
||||
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), output_file))
|
||||
with open(output_file, 'wb') as f:
|
||||
f.write(pes.to_der())
|
||||
|
||||
def do_split(pes: ProfileElementSequence, opts):
|
||||
i = 0
|
||||
for pe in pes.pe_list:
|
||||
@@ -170,14 +120,6 @@ def do_check(pes: ProfileElementSequence, opts):
|
||||
checker.check(pes)
|
||||
print("All good!")
|
||||
|
||||
def do_extract_pe(pes: ProfileElementSequence, opts):
|
||||
new_pe_list = []
|
||||
for pe in pes.pe_list:
|
||||
if pe.identification == opts.identification:
|
||||
print("Extracting PE %s (id=%u) to file %s..." % (pe, pe.identification, opts.pe_file))
|
||||
with open(opts.pe_file, 'wb') as f:
|
||||
f.write(pe.to_der())
|
||||
|
||||
def do_remove_pe(pes: ProfileElementSequence, opts):
|
||||
new_pe_list = []
|
||||
for pe in pes.pe_list:
|
||||
@@ -186,14 +128,13 @@ def do_remove_pe(pes: ProfileElementSequence, opts):
|
||||
if identification in opts.identification:
|
||||
print("Removing PE %s (id=%u) from Sequence..." % (pe, identification))
|
||||
continue
|
||||
if pe.type in opts.type:
|
||||
print("Removing PE %s (type=%s) from Sequence..." % (pe, pe.type))
|
||||
continue
|
||||
new_pe_list.append(pe)
|
||||
|
||||
pes.pe_list = new_pe_list
|
||||
pes._process_pelist()
|
||||
write_pes(pes, opts.output_file)
|
||||
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
|
||||
with open(opts.output_file, 'wb') as f:
|
||||
f.write(pes.to_der())
|
||||
|
||||
def do_remove_naa(pes: ProfileElementSequence, opts):
|
||||
if not opts.naa_type in NAAs:
|
||||
@@ -201,83 +142,9 @@ def do_remove_naa(pes: ProfileElementSequence, opts):
|
||||
naa = NAAs[opts.naa_type]
|
||||
print("Removing NAAs of type '%s' from Sequence..." % opts.naa_type)
|
||||
pes.remove_naas_of_type(naa)
|
||||
write_pes(pes, opts.output_file)
|
||||
|
||||
def info_apps(pes:ProfileElementSequence):
|
||||
def show_member(dictionary:Optional[dict], member:str, indent:str="\t", mandatory:bool = False, limit:bool = False):
|
||||
if dictionary is None:
|
||||
return
|
||||
value = dictionary.get(member, None)
|
||||
if value is None and mandatory == True:
|
||||
print("%s%s: (missing!)" % (indent, member))
|
||||
return
|
||||
elif value is None:
|
||||
return
|
||||
|
||||
if limit and len(value) > 40:
|
||||
print("%s%s: '%s...%s' (%u bytes)" % (indent, member, b2h(value[:20]), b2h(value[-20:]), len(value)))
|
||||
else:
|
||||
print("%s%s: '%s' (%u bytes)" % (indent, member, b2h(value), len(value)))
|
||||
|
||||
apps = pes.pe_by_type.get('application', [])
|
||||
if len(apps) == 0:
|
||||
print("No Application PE present!")
|
||||
return;
|
||||
|
||||
for app_pe in enumerate(apps):
|
||||
print("Application #%u:" % app_pe[0])
|
||||
print("\tloadBlock:")
|
||||
load_block = app_pe[1].decoded['loadBlock']
|
||||
show_member(load_block, 'loadPackageAID', "\t\t", True)
|
||||
show_member(load_block, 'securityDomainAID', "\t\t")
|
||||
show_member(load_block, 'nonVolatileCodeLimitC6', "\t\t")
|
||||
show_member(load_block, 'volatileDataLimitC7', "\t\t")
|
||||
show_member(load_block, 'nonVolatileDataLimitC8', "\t\t")
|
||||
show_member(load_block, 'hashValue', "\t\t")
|
||||
show_member(load_block, 'loadBlockObject', "\t\t", True, True)
|
||||
for inst in enumerate(app_pe[1].decoded.get('instanceList', [])):
|
||||
print("\tinstanceList[%u]:" % inst[0])
|
||||
show_member(inst[1], 'applicationLoadPackageAID', "\t\t", True)
|
||||
if inst[1].get('applicationLoadPackageAID', None) != load_block.get('loadPackageAID', None):
|
||||
print("\t\t(applicationLoadPackageAID should be the same as loadPackageAID!)")
|
||||
show_member(inst[1], 'classAID', "\t\t", True)
|
||||
show_member(inst[1], 'instanceAID', "\t\t", True)
|
||||
show_member(inst[1], 'extraditeSecurityDomainAID', "\t\t")
|
||||
show_member(inst[1], 'applicationPrivileges', "\t\t", True)
|
||||
show_member(inst[1], 'lifeCycleState', "\t\t", True)
|
||||
show_member(inst[1], 'applicationSpecificParametersC9', "\t\t", True)
|
||||
sys_specific_pars = inst[1].get('systemSpecificParameters', None)
|
||||
if sys_specific_pars:
|
||||
print("\t\tsystemSpecificParameters:")
|
||||
show_member(sys_specific_pars, 'volatileMemoryQuotaC7', "\t\t\t")
|
||||
show_member(sys_specific_pars, 'nonVolatileMemoryQuotaC8', "\t\t\t")
|
||||
show_member(sys_specific_pars, 'globalServiceParameters', "\t\t\t")
|
||||
show_member(sys_specific_pars, 'implicitSelectionParameter', "\t\t\t")
|
||||
show_member(sys_specific_pars, 'volatileReservedMemory', "\t\t\t")
|
||||
show_member(sys_specific_pars, 'nonVolatileReservedMemory', "\t\t\t")
|
||||
show_member(sys_specific_pars, 'ts102226SIMFileAccessToolkitParameter', "\t\t\t")
|
||||
additional_cl_pars = inst.get('ts102226AdditionalContactlessParameters', None)
|
||||
if additional_cl_pars:
|
||||
print("\t\t\tts102226AdditionalContactlessParameters:")
|
||||
show_member(additional_cl_pars, 'protocolParameterData', "\t\t\t\t")
|
||||
show_member(sys_specific_pars, 'userInteractionContactlessParameters', "\t\t\t")
|
||||
show_member(sys_specific_pars, 'cumulativeGrantedVolatileMemory', "\t\t\t")
|
||||
show_member(sys_specific_pars, 'cumulativeGrantedNonVolatileMemory', "\t\t\t")
|
||||
app_pars = inst[1].get('applicationParameters', None)
|
||||
if app_pars:
|
||||
print("\t\tapplicationParameters:")
|
||||
show_member(app_pars, 'uiccToolkitApplicationSpecificParametersField', "\t\t\t")
|
||||
show_member(app_pars, 'uiccAccessApplicationSpecificParametersField', "\t\t\t")
|
||||
show_member(app_pars, 'uiccAdministrativeAccessApplicationSpecificParametersField', "\t\t\t")
|
||||
ctrl_ref_tp = inst[1].get('controlReferenceTemplate', None)
|
||||
if ctrl_ref_tp:
|
||||
print("\t\tcontrolReferenceTemplate:")
|
||||
show_member(ctrl_ref_tp, 'applicationProviderIdentifier', "\t\t\t", True)
|
||||
process_data = inst[1].get('processData', None)
|
||||
if process_data:
|
||||
print("\t\tprocessData:")
|
||||
for proc in process_data:
|
||||
print("\t\t\t" + b2h(proc))
|
||||
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
|
||||
with open(opts.output_file, 'wb') as f:
|
||||
f.write(pes.to_der())
|
||||
|
||||
def do_info(pes: ProfileElementSequence, opts):
|
||||
def get_naa_count(pes: ProfileElementSequence) -> dict:
|
||||
@@ -287,10 +154,6 @@ def do_info(pes: ProfileElementSequence, opts):
|
||||
ret[naa_type] = len(pes.pes_by_naa[naa_type])
|
||||
return ret
|
||||
|
||||
if opts.apps:
|
||||
info_apps(pes)
|
||||
return;
|
||||
|
||||
pe_hdr_dec = pes.pe_by_type['header'][0].decoded
|
||||
print()
|
||||
print("SAIP Profile Version: %u.%u" % (pe_hdr_dec['major-version'], pe_hdr_dec['minor-version']))
|
||||
@@ -350,98 +213,16 @@ def do_extract_apps(pes:ProfileElementSequence, opts):
|
||||
apps = pes.pe_by_type.get('application', [])
|
||||
for app_pe in apps:
|
||||
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
|
||||
|
||||
fname = os.path.join(opts.output_dir, '%s-%s.%s' % (pes.iccid, package_aid, opts.format))
|
||||
load_block_obj = app_pe.decoded['loadBlock']['loadBlockObject']
|
||||
print("Writing Load Package AID: %s to file %s" % (package_aid, fname))
|
||||
app_pe.to_file(fname)
|
||||
|
||||
def do_add_app(pes:ProfileElementSequence, opts):
|
||||
print("Applying applet file: '%s'..." % opts.applet_file)
|
||||
app_pe = ProfileElementApplication.from_file(opts.applet_file,
|
||||
opts.aid,
|
||||
opts.sd_aid,
|
||||
opts.non_volatile_code_limit,
|
||||
opts.volatile_data_limit,
|
||||
opts.non_volatile_data_limit,
|
||||
opts.hash_value)
|
||||
|
||||
security_domain = pes.pe_by_type.get('securityDomain', [])
|
||||
if len(security_domain) == 0:
|
||||
print("profile package does not contain a securityDomain, please add a securityDomain PE first!")
|
||||
elif len(security_domain) > 1:
|
||||
print("adding an application PE to profiles with multiple securityDomain is not supported yet!")
|
||||
else:
|
||||
pes.insert_after_pe(security_domain[0], app_pe)
|
||||
print("application PE inserted into PE Sequence after securityDomain PE AID: %s" %
|
||||
b2h(security_domain[0].decoded['instance']['instanceAID']))
|
||||
write_pes(pes, opts.output_file)
|
||||
|
||||
def do_remove_app(pes:ProfileElementSequence, opts):
|
||||
apps = pes.pe_by_type.get('application', [])
|
||||
for app_pe in apps:
|
||||
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
|
||||
if opts.aid == package_aid:
|
||||
identification = app_pe.identification
|
||||
opts_remove_pe = argparse.Namespace()
|
||||
opts_remove_pe.identification = [app_pe.identification]
|
||||
opts_remove_pe.type = []
|
||||
opts_remove_pe.output_file = opts.output_file
|
||||
print("Found Load Package AID: %s, removing related PE (id=%u) from Sequence..." %
|
||||
(package_aid, identification))
|
||||
do_remove_pe(pes, opts_remove_pe)
|
||||
return
|
||||
print("Load Package AID: %s not found in PE Sequence" % opts.aid)
|
||||
|
||||
def do_add_app_inst(pes:ProfileElementSequence, opts):
|
||||
apps = pes.pe_by_type.get('application', [])
|
||||
for app_pe in apps:
|
||||
package_aid = b2h(app_pe.decoded['loadBlock']['loadPackageAID'])
|
||||
if opts.aid == package_aid:
|
||||
print("Found Load Package AID: %s, adding new instance AID: %s to Application PE..." %
|
||||
(opts.aid, opts.inst_aid))
|
||||
app_pe.add_instance(opts.aid,
|
||||
opts.class_aid,
|
||||
opts.inst_aid,
|
||||
opts.app_privileges,
|
||||
opts.app_spec_pars,
|
||||
opts.uicc_toolkit_app_spec_pars,
|
||||
opts.uicc_access_app_spec_pars,
|
||||
opts.uicc_adm_access_app_spec_pars,
|
||||
opts.volatile_memory_quota,
|
||||
opts.non_volatile_memory_quota,
|
||||
opts.process_data)
|
||||
write_pes(pes, opts.output_file)
|
||||
return
|
||||
print("Load Package AID: %s not found in PE Sequence" % opts.aid)
|
||||
|
||||
def do_remove_app_inst(pes:ProfileElementSequence, opts):
|
||||
apps = pes.pe_by_type.get('application', [])
|
||||
for app_pe in apps:
|
||||
if opts.aid == b2h(app_pe.decoded['loadBlock']['loadPackageAID']):
|
||||
print("Found Load Package AID: %s, removing instance AID: %s from Application PE..." %
|
||||
(opts.aid, opts.inst_aid))
|
||||
app_pe.remove_instance(opts.inst_aid)
|
||||
write_pes(pes, opts.output_file)
|
||||
return
|
||||
print("Load Package AID: %s not found in PE Sequence" % opts.aid)
|
||||
|
||||
def do_edit_mand_srv_list(pes: ProfileElementSequence, opts):
|
||||
header = pes.pe_by_type.get('header', [])[0]
|
||||
|
||||
for s in opts.add_flag:
|
||||
print("Adding service '%s' to mandatory services list..." % s)
|
||||
header.mandatory_service_add(s)
|
||||
for s in opts.remove_flag:
|
||||
if s in header.decoded['eUICC-Mandatory-services'].keys():
|
||||
print("Removing service '%s' from mandatory services list..." % s)
|
||||
header.mandatory_service_remove(s)
|
||||
if opts.format == 'ijc':
|
||||
with open(fname, 'wb') as f:
|
||||
f.write(load_block_obj)
|
||||
else:
|
||||
print("Service '%s' not present in mandatory services list, cannot remove!" % s)
|
||||
|
||||
print("The following services are now set mandatory:")
|
||||
for s in header.decoded['eUICC-Mandatory-services'].keys():
|
||||
print("\t%s" % s)
|
||||
|
||||
write_pes(pes, opts.output_file)
|
||||
with io.BytesIO(load_block_obj) as f, zipfile.ZipFile(fname, 'w') as z:
|
||||
javacard.ijc_to_cap(f, z, package_aid)
|
||||
|
||||
def do_tree(pes:ProfileElementSequence, opts):
|
||||
pes.mf.print_tree()
|
||||
@@ -465,8 +246,6 @@ if __name__ == '__main__':
|
||||
do_dump(pes, opts)
|
||||
elif opts.command == 'check':
|
||||
do_check(pes, opts)
|
||||
elif opts.command == 'extract-pe':
|
||||
do_extract_pe(pes, opts)
|
||||
elif opts.command == 'remove-pe':
|
||||
do_remove_pe(pes, opts)
|
||||
elif opts.command == 'remove-naa':
|
||||
@@ -475,15 +254,5 @@ if __name__ == '__main__':
|
||||
do_info(pes, opts)
|
||||
elif opts.command == 'extract-apps':
|
||||
do_extract_apps(pes, opts)
|
||||
elif opts.command == 'add-app':
|
||||
do_add_app(pes, opts)
|
||||
elif opts.command == 'remove-app':
|
||||
do_remove_app(pes, opts)
|
||||
elif opts.command == 'add-app-inst':
|
||||
do_add_app_inst(pes, opts)
|
||||
elif opts.command == 'remove-app-inst':
|
||||
do_remove_app_inst(pes, opts)
|
||||
elif opts.command == 'edit-mand-srv-list':
|
||||
do_edit_mand_srv_list(pes, opts)
|
||||
elif opts.command == 'tree':
|
||||
do_tree(pes, opts)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/bin/bash
|
||||
# This is an example script to illustrate how to add JAVA card applets to an existing eUICC profile package.
|
||||
|
||||
PYSIMPATH=../
|
||||
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE.der
|
||||
OUTPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
|
||||
APPPATH=./HelloSTK_09122024.cap
|
||||
|
||||
# Download example applet (see also https://gitea.osmocom.org/sim-card/hello-stk):
|
||||
if ! [ -f $APPPATH ]; then
|
||||
wget https://osmocom.org/attachments/download/8931/HelloSTK_09122024.cap
|
||||
fi
|
||||
|
||||
# Step #1: Create the application PE and load the ijc contents from the .cap file:
|
||||
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH add-app \
|
||||
--output-file $OUTPATH --applet-file $APPPATH --aid 'D07002CA44'
|
||||
|
||||
# Step #2: Create the application instance inside the application PE created in step #1:
|
||||
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH add-app-inst --output-file $OUTPATH \
|
||||
--aid 'D07002CA44' \
|
||||
--class-aid 'D07002CA44900101' \
|
||||
--inst-aid 'D07002CA44900101' \
|
||||
--app-privileges '00' \
|
||||
--app-spec-pars '00' \
|
||||
--uicc-toolkit-app-spec-pars '01001505000000000000000000000000'
|
||||
|
||||
# Display the contents of the resulting application PE:
|
||||
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH info --apps
|
||||
|
||||
# For an explaination of --uicc-toolkit-app-spec-pars, see:
|
||||
# ETSI TS 102 226, section 8.2.1.3.2.2.1
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
# This is an example script to illustrate how to extract JAVA card applets from an existing eUICC profile package.
|
||||
|
||||
PYSIMPATH=../
|
||||
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
|
||||
OUTPATH=./
|
||||
|
||||
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH extract-apps --output-dir ./ --format ijc
|
||||
@@ -1,14 +0,0 @@
|
||||
#!/bin/bash
|
||||
# This is an example script to illustrate how to remove a JAVA card applet instance from an application PE inside an
|
||||
# existing eUICC profile package.
|
||||
|
||||
PYSIMPATH=../
|
||||
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
|
||||
OUTPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello-no-inst.der
|
||||
|
||||
# Remove application PE entirely
|
||||
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH remove-app-inst \
|
||||
--output-file $OUTPATH --aid 'd07002ca44' --inst-aid 'd07002ca44900101'
|
||||
|
||||
# Display the contents of the resulting application PE:
|
||||
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH info --apps
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/bin/bash
|
||||
# This is an example script to illustrate how to remove a JAVA card applet from an existing eUICC profile package.
|
||||
|
||||
PYSIMPATH=../
|
||||
INPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-hello.der
|
||||
OUTPATH=../smdpp-data/upp/TS48V1-A-UNIQUE-no-hello.der
|
||||
|
||||
# Remove application PE entirely
|
||||
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $INPATH remove-app \
|
||||
--output-file $OUTPATH --aid 'D07002CA44'
|
||||
|
||||
# Display the contents of the resulting application PE:
|
||||
PYTHONPATH=$PYSIMPATH python3 $PYSIMPATH/contrib/saip-tool.py $OUTPATH info --apps
|
||||
@@ -1,52 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# small utility program to deal with 5G SUCI key material, at least for the ECIES Protection Scheme
|
||||
# Profile A (curve25519) and B (secp256r1)
|
||||
|
||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||
# SPDX-License-Identifier: GPL-2.0+
|
||||
|
||||
import argparse
|
||||
|
||||
from osmocom.utils import b2h
|
||||
from Cryptodome.PublicKey import ECC
|
||||
# if used with pycryptodome < v3.21.0 you will get the following error when using curve25519:
|
||||
# "Cryptodome.PublicKey.ECC.UnsupportedEccFeature: Unsupported ECC purpose (OID: 1.3.101.110)"
|
||||
|
||||
def gen_key(opts):
|
||||
# FIXME: avoid overwriting key files
|
||||
mykey = ECC.generate(curve=opts.curve)
|
||||
data = mykey.export_key(format='PEM')
|
||||
with open(opts.key_file, "wt") as f:
|
||||
f.write(data)
|
||||
|
||||
def dump_pkey(opts):
|
||||
|
||||
#with open("curve25519-1.key", "r") as f:
|
||||
|
||||
with open(opts.key_file, "r") as f:
|
||||
data = f.read()
|
||||
mykey = ECC.import_key(data)
|
||||
|
||||
der = mykey.public_key().export_key(format='raw', compress=opts.compressed)
|
||||
print(b2h(der))
|
||||
|
||||
arg_parser = argparse.ArgumentParser(description="""Generate or export SUCI keys for 5G SA networks""")
|
||||
arg_parser.add_argument('--key-file', help='The key file to use', required=True)
|
||||
|
||||
subparsers = arg_parser.add_subparsers(dest='command', help="The command to perform", required=True)
|
||||
|
||||
parser_genkey = subparsers.add_parser('generate-key', help='Generate a new key pair')
|
||||
parser_genkey.add_argument('--curve', help='The ECC curve to use', choices=['secp256r1','curve25519'], required=True)
|
||||
|
||||
parser_dump_pkey = subparsers.add_parser('dump-pub-key', help='Dump the public key')
|
||||
parser_dump_pkey.add_argument('--compressed', help='Use point compression', action='store_true')
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
opts = arg_parser.parse_args()
|
||||
|
||||
if opts.command == 'generate-key':
|
||||
gen_key(opts)
|
||||
elif opts.command == 'dump-pub-key':
|
||||
dump_pkey(opts)
|
||||
112
contrib/wsrc_card_client.py
Executable file
112
contrib/wsrc_card_client.py
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""Connect smartcard to a remote server, so the remote server can take control and
|
||||
perform commands on it."""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import logging
|
||||
import argparse
|
||||
from osmocom.utils import b2h
|
||||
import websockets
|
||||
|
||||
from pySim.transport import init_reader, argparse_add_reader_args, LinkBase
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.wsrc.client_blocking import WsClientBlocking
|
||||
from pySim.exceptions import NoCardError
|
||||
from pySim.wsrc import WSRC_DEFAULT_PORT_CARD
|
||||
|
||||
logging.basicConfig(format="[%(levelname)s] %(asctime)s %(message)s", level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CardWsClientBlocking(WsClientBlocking):
|
||||
"""Implementation of the card (reader) client of the WSRC (WebSocket Remote Card) protocol"""
|
||||
|
||||
def __init__(self, ws_uri, tp: LinkBase):
|
||||
super().__init__('card', ws_uri)
|
||||
self.tp = tp
|
||||
|
||||
def perform_outbound_hello(self):
|
||||
hello_data = {
|
||||
'atr': b2h(self.tp.get_atr()),
|
||||
# TODO: include various card information in the HELLO message
|
||||
}
|
||||
super().perform_outbound_hello(hello_data)
|
||||
|
||||
def handle_rx_c_apdu(self, rx: dict):
|
||||
"""handle an inbound APDU transceive command"""
|
||||
data, sw = self.tp.send_apdu(rx['command'])
|
||||
tx = {
|
||||
'response': data,
|
||||
'sw': sw,
|
||||
}
|
||||
self.tx_json('r_apdu', tx)
|
||||
|
||||
def handle_rx_disconnect(self, rx: dict):
|
||||
"""server tells us to disconnect"""
|
||||
self.tx_json('disconnect_ack')
|
||||
# FIXME: tear down connection and/or terminate entire program
|
||||
|
||||
def handle_rx_state_notification(self, rx: dict):
|
||||
logger.info("State Notification: %s" % rx['new_state'])
|
||||
|
||||
def handle_rx_print(self, rx: dict):
|
||||
"""print a message (text) given by server to the local console/log"""
|
||||
logger.info("SERVER MSG: %s" % rx['message'])
|
||||
# no response
|
||||
|
||||
def handle_rx_reset_req(self, rx: dict):
|
||||
"""server tells us to reset the card"""
|
||||
self.tp.reset_card()
|
||||
self.tx_json('reset_resp', {'atr': b2h(self.tp.get_atr())})
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
argparse_add_reader_args(parser)
|
||||
parser.add_argument("--uri", default="ws://localhost:%u/" % (WSRC_DEFAULT_PORT_CARD),
|
||||
help="URI of the sever to which to connect")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = parser.parse_args()
|
||||
|
||||
# open the card reader / slot
|
||||
logger.info("Initializing Card Reader...")
|
||||
try:
|
||||
tp = init_reader(opts)
|
||||
except Exception as e:
|
||||
logger.fatal("Error opening reader: %s" % e)
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("Connecting to Card...")
|
||||
try:
|
||||
tp.connect()
|
||||
except NoCardError as e:
|
||||
logger.fatal("Error opening card! Is a card inserted in the reader?")
|
||||
sys.exit(1)
|
||||
|
||||
scc = SimCardCommands(transport=tp)
|
||||
logger.info("Detected Card with ATR: %s" % b2h(tp.get_atr()))
|
||||
|
||||
# TODO: gather various information about the card; print it
|
||||
|
||||
# create + connect the client to the server
|
||||
cl = CardWsClientBlocking(opts.uri, tp)
|
||||
logger.info("Connecting to remote server...")
|
||||
try:
|
||||
cl.connect()
|
||||
logger.info("Successfully connected to Server")
|
||||
except ConnectionRefusedError as e:
|
||||
logger.fatal(e)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
while True:
|
||||
# endless loop: wait for inbound command from server + execute it
|
||||
cl.rx_and_execute_cmd()
|
||||
except websockets.exceptions.ConnectionClosedOK as e:
|
||||
print(e)
|
||||
sys.exit(1)
|
||||
except KeyboardInterrupt as e:
|
||||
print(e.__class__.__name__)
|
||||
sys.exit(2)
|
||||
354
contrib/wsrc_server.py
Executable file
354
contrib/wsrc_server.py
Executable file
@@ -0,0 +1,354 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
import argparse
|
||||
from typing import Optional, Tuple
|
||||
from websockets.asyncio.server import serve
|
||||
from websockets.exceptions import ConnectionClosedError
|
||||
from osmocom.utils import Hexstr, swap_nibbles
|
||||
|
||||
from pySim.utils import SwMatchstr, ResTuple, sw_match, dec_iccid
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.wsrc import WSRC_DEFAULT_PORT_USER, WSRC_DEFAULT_PORT_CARD
|
||||
|
||||
logging.basicConfig(format="[%(levelname)s] %(asctime)s %(message)s", level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
card_clients = set()
|
||||
user_clients = set()
|
||||
|
||||
class WsClientLogAdapter(logging.LoggerAdapter):
|
||||
"""LoggerAdapter adding context (for now: remote IP/Port of client) to log"""
|
||||
def process(self, msg, kwargs):
|
||||
return '%s([%s]:%u) %s' % (self.extra['type'], self.extra['remote_addr'][0],
|
||||
self.extra['remote_addr'][1], msg), kwargs
|
||||
|
||||
class WsClient:
|
||||
def __init__(self, websocket, hello: dict):
|
||||
self.websocket = websocket
|
||||
self.hello = hello
|
||||
self.identity = {}
|
||||
self.logger = WsClientLogAdapter(logger, {'type': self.__class__.__name__,
|
||||
'remote_addr': websocket.remote_address})
|
||||
|
||||
def __str__(self):
|
||||
return '%s([%s]:%u)' % (self.__class__.__name__, self.websocket.remote_address[0],
|
||||
self.websocket.remote_address[1])
|
||||
|
||||
async def rx_json(self):
|
||||
rx = await self.websocket.recv()
|
||||
rx_js = json.loads(rx)
|
||||
self.logger.debug("Rx: %s", rx_js)
|
||||
assert 'msg_type' in rx
|
||||
return rx_js
|
||||
|
||||
async def tx_json(self, msg_type:str, d: dict = {}):
|
||||
"""Transmit a json-serializable dict to the peer"""
|
||||
d['msg_type'] = msg_type
|
||||
d_js = json.dumps(d)
|
||||
self.logger.debug("Tx: %s", d_js)
|
||||
await self.websocket.send(d_js)
|
||||
|
||||
async def tx_hello_ack(self):
|
||||
await self.tx_json('hello_ack')
|
||||
|
||||
async def xceive_json(self, msg_type:str, d:dict = {}, exp_msg_type:Optional[str] = None) -> dict:
|
||||
await self.tx_json(msg_type, d)
|
||||
rx = await self.rx_json()
|
||||
if exp_msg_type:
|
||||
assert rx['msg_type'] == exp_msg_type
|
||||
return rx;
|
||||
|
||||
async def tx_error(self, message: str):
|
||||
"""Transmit an error message to the peer"""
|
||||
event = {
|
||||
"message": message,
|
||||
}
|
||||
self.logger.error("Transmitting error message: '%s'" % message)
|
||||
await self.tx_json('error', event)
|
||||
|
||||
# async def ws_hdlr(self):
|
||||
# """kind of a 'main' function for the websocket client: wait for incoming message,
|
||||
# and handle it."""
|
||||
# try:
|
||||
# async for message in self.websocket:
|
||||
# method = getattr(self, 'handle_rx_%s' % message['msg_type'], None)
|
||||
# if not method:
|
||||
# await self.tx_error("Unknonw msg_type: %s" % message['msg_type'])
|
||||
# else:
|
||||
# method(message)
|
||||
# except ConnectionClosedError:
|
||||
# # we handle this in the outer loop
|
||||
# pass
|
||||
|
||||
class CardClient(WsClient):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.identity['ATR'] = self.hello['atr']
|
||||
self.state = 'init'
|
||||
|
||||
def __str__(self):
|
||||
eid = self.identity.get('EID', None)
|
||||
if eid:
|
||||
return '%s(EID=%s)' % (self.__class__.__name__, eid)
|
||||
iccid = self.identity.get('ICCID', None)
|
||||
if iccid:
|
||||
return '%s(ICCID=%s)' % (self.__class__.__name__, iccid)
|
||||
return super().__str__()
|
||||
|
||||
def listing_entry(self) -> dict:
|
||||
return {'remote_addr': self.websocket.remote_address,
|
||||
'identities': self.identity,
|
||||
'state': self.state}
|
||||
|
||||
"""A websocket client that represents a reader/card. This is what we use to talk to a card"""
|
||||
async def xceive_apdu_raw(self, cmd: Hexstr) -> ResTuple:
|
||||
"""transceive a single APDU with the card"""
|
||||
message = await self.xceive_json('c_apdu', {'command': cmd}, 'r_apdu')
|
||||
return message['response'], message['sw']
|
||||
|
||||
async def xceive_apdu(self, pdu: Hexstr) -> ResTuple:
|
||||
"""transceive an APDU with the card, handling T=0 GET_RESPONSE cases"""
|
||||
prev_pdu = pdu
|
||||
data, sw = await self.xceive_apdu_raw(pdu)
|
||||
|
||||
if sw is not None:
|
||||
while (sw[0:2] in ['9f', '61', '62', '63']):
|
||||
# SW1=9F: 3GPP TS 51.011 9.4.1, Responses to commands which are correctly executed
|
||||
# SW1=61: ISO/IEC 7816-4, Table 5 — General meaning of the interindustry values of SW1-SW2
|
||||
# SW1=62: ETSI TS 102 221 7.3.1.1.4 Clause 4b): 62xx, 63xx, 9xxx != 9000
|
||||
pdu_gr = pdu[0:2] + 'c00000' + sw[2:4]
|
||||
prev_pdu = pdu_gr
|
||||
d, sw = await self.xceive_apdu_raw(pdu_gr)
|
||||
data += d
|
||||
if sw[0:2] == '6c':
|
||||
# SW1=6C: ETSI TS 102 221 Table 7.1: Procedure byte coding
|
||||
pdu_gr = prev_pdu[0:8] + sw[2:4]
|
||||
data, sw = await self.xceive_apdu_raw(pdu_gr)
|
||||
|
||||
return data, sw
|
||||
|
||||
async def xceive_apdu_checksw(self, pdu: Hexstr, sw: SwMatchstr = "9000") -> ResTuple:
|
||||
"""like xceive_apdu, but checking the status word matches the expected pattern"""
|
||||
rv = await self.xceive_apdu(pdu)
|
||||
last_sw = rv[1]
|
||||
|
||||
if not sw_match(rv[1], sw):
|
||||
raise SwMatchError(rv[1], sw.lower())
|
||||
return rv
|
||||
|
||||
async def card_reset(self):
|
||||
"""reset the card"""
|
||||
rx = await self.xceive_json('reset_req', exp_msg_type='reset_resp')
|
||||
self.identity['ATR'] = rx['atr']
|
||||
|
||||
async def notify_state(self):
|
||||
"""notify the card client of a state [change]"""
|
||||
await self.tx_json('state_notification', {'new_state': self.state})
|
||||
|
||||
async def get_iccid(self):
|
||||
"""high-level method to obtain the ICCID of the card"""
|
||||
await self.xceive_apdu_checksw('00a40000023f00') # SELECT MF
|
||||
await self.xceive_apdu_checksw('00a40000022fe2') # SELECT EF.ICCID
|
||||
res, sw = await self.xceive_apdu_checksw('00b0000000') # READ BINARY
|
||||
return dec_iccid(res)
|
||||
|
||||
async def get_eid_sgp22(self):
|
||||
"""high-level method to obtain the EID of a SGP.22 consumer eUICC"""
|
||||
await self.xceive_apdu_checksw('00a4040410a0000005591010ffffffff8900000100')
|
||||
res, sw = await self.xceive_apdu_checksw('80e2910006bf3e035c015a')
|
||||
return res[-32:]
|
||||
|
||||
async def set_state(self, new_state:str):
|
||||
self.logger.info("Card now in '%s' state" % new_state)
|
||||
self.state = new_state
|
||||
await self.notify_state()
|
||||
|
||||
async def identify(self):
|
||||
# identify the card by asking for its EID and/or ICCID
|
||||
try:
|
||||
eid = await self.get_eid_sgp22()
|
||||
self.logger.debug("EID: %s", eid)
|
||||
self.identity['EID'] = eid
|
||||
except SwMatchError:
|
||||
pass
|
||||
try:
|
||||
iccid = await self.get_iccid()
|
||||
self.logger.debug("ICCID: %s", iccid)
|
||||
self.identity['ICCID'] = iccid
|
||||
except SwMatchError:
|
||||
pass
|
||||
await self.set_state('ready')
|
||||
|
||||
async def associate_user(self, user):
|
||||
assert self.state == 'ready'
|
||||
self.user = user
|
||||
await self.set_state('associated')
|
||||
|
||||
async def disassociate_user(self):
|
||||
assert self.user
|
||||
assert self.state == 'associated'
|
||||
await self.set_state('ready')
|
||||
|
||||
@staticmethod
|
||||
def find_client_for_id(id_type: str, id_str: str) -> Optional['CardClient']:
|
||||
for c in card_clients:
|
||||
if c.state != 'ready':
|
||||
continue
|
||||
c_id = c.identity.get(id_type.upper(), None)
|
||||
if c_id and c_id.lower() == id_str.lower():
|
||||
return c
|
||||
return None
|
||||
|
||||
class UserClient(WsClient):
|
||||
"""A websocket client representing a user application like pySim-shell."""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.state = 'init'
|
||||
self.card = None
|
||||
|
||||
async def associate_card(self, card: CardClient):
|
||||
assert self.state == 'init'
|
||||
self.card = card
|
||||
self.state = 'associated'
|
||||
|
||||
async def disassociate_card(self):
|
||||
assert self.state == 'associated'
|
||||
self.card = None
|
||||
self.state = 'init'
|
||||
|
||||
async def state_init(self):
|
||||
"""Wait for incoming 'select_card' and process it."""
|
||||
while True:
|
||||
rx = await self.rx_json()
|
||||
if rx['msg_type'] == 'select_card':
|
||||
# look-up if the card can be found
|
||||
card = CardClient.find_client_for_id(rx['id_type'], rx['id_str'])
|
||||
if not card:
|
||||
await self.tx_error('No CardClient found for %s == %s' % (rx['id_type'], rx['id_str']))
|
||||
continue
|
||||
# transition to next statee
|
||||
await card.associate_user(self)
|
||||
await self.associate_card(card)
|
||||
await self.tx_json('select_card_ack', {'identities': card.identity})
|
||||
break
|
||||
elif rx['msg_type'] == 'list_cards':
|
||||
res = [x.listing_entry() for x in card_clients]
|
||||
await self.tx_json('list_cards_ack', {'cards': res})
|
||||
else:
|
||||
self.tx_error('Unknown message type %s' % rx['msg_type'])
|
||||
|
||||
async def state_selected(self):
|
||||
while True:
|
||||
rx = await self.rx_json()
|
||||
if rx['msg_type'] == 'c_apdu':
|
||||
rsp, sw = await self.card.xceive_apdu_raw(rx['command'])
|
||||
await self.tx_json('r_apdu', {'response': rsp, 'sw': sw})
|
||||
elif rx['msg_type'] == 'reset_req':
|
||||
await self.card.card_reset()
|
||||
await self.tx_json('reset_resp')
|
||||
else:
|
||||
self.logger.warning("Unknown/unsupported command '%s' received" % rx['msg_type'])
|
||||
|
||||
|
||||
|
||||
async def card_conn_hdlr(websocket):
|
||||
"""Handler for incoming connection to 'card' port."""
|
||||
# receive first message, which should be a 'hello'
|
||||
rx_raw = await websocket.recv()
|
||||
rx = json.loads(rx_raw)
|
||||
assert rx['msg_type'] == 'hello'
|
||||
client_type = rx['client_type']
|
||||
|
||||
if client_type != 'card':
|
||||
logger.error("Rejecting client (unknown type %s) connection", client_type)
|
||||
raise ValueError("client_type '%s' != expected 'card'" % client_type)
|
||||
|
||||
card = CardClient(websocket, rx)
|
||||
card.logger.info("connection accepted")
|
||||
# first go through identity phase
|
||||
async with asyncio.timeout(30):
|
||||
await card.tx_hello_ack()
|
||||
card.logger.info("hello-handshake completed")
|
||||
card_clients.add(card)
|
||||
# first obtain the identity of the card
|
||||
await card.identify()
|
||||
# then go into the "main loop"
|
||||
try:
|
||||
# wait 'indefinitely'. We cannot call websocket.recv() here, as we will call another
|
||||
# recv() while waiting for the R-APDU after forwarding one from the user client.
|
||||
await websocket.wait_closed()
|
||||
finally:
|
||||
card.logger.info("connection closed")
|
||||
card_clients.remove(card)
|
||||
|
||||
|
||||
async def user_conn_hdlr(websocket):
|
||||
"""Handler for incoming connection to 'user' port."""
|
||||
# receive first message, which should be a 'hello'
|
||||
rx_raw = await websocket.recv()
|
||||
rx = json.loads(rx_raw)
|
||||
assert rx['msg_type'] == 'hello'
|
||||
client_type = rx['client_type']
|
||||
|
||||
if client_type != 'user':
|
||||
logger.error("Rejecting client (unknown type %s) connection", client_type)
|
||||
raise ValueError("client_type '%s' != expected 'card'" % client_type)
|
||||
|
||||
user = UserClient(websocket, rx)
|
||||
user.logger.info("connection accepted")
|
||||
# first go through hello phase
|
||||
async with asyncio.timeout(10):
|
||||
await user.tx_hello_ack()
|
||||
user.logger.info("hello-handshake completed")
|
||||
user_clients.add(user)
|
||||
# first wait for the user to specify the select the card
|
||||
try:
|
||||
await user.state_init()
|
||||
except ConnectionClosedError:
|
||||
user.logger.info("connection closed")
|
||||
user_clients.remove(user)
|
||||
return
|
||||
except asyncio.exceptions.CancelledError: # direct cause of TimeoutError
|
||||
user.logger.error("User failed to transition to 'associated' state within 10s")
|
||||
user_clients.remove(user)
|
||||
return
|
||||
except Exception as e:
|
||||
user.logger.error("Unknown exception %s", e)
|
||||
raise e
|
||||
user_clients.remove(user)
|
||||
return
|
||||
# then perform APDU exchanges without any time limit
|
||||
try:
|
||||
await user.state_selected()
|
||||
except ConnectionClosedError:
|
||||
pass
|
||||
finally:
|
||||
user.logger.info("connection closed")
|
||||
await user.card.disassociate_user()
|
||||
await user.disassociate_card()
|
||||
user_clients.remove(user)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(description="""Osmocom WebSocket Remote Card server""")
|
||||
parser.add_argument('--user-bind-port', type=int, default=WSRC_DEFAULT_PORT_USER,
|
||||
help="port number to bind for connections from users")
|
||||
parser.add_argument('--user-bind-host', type=str, default='localhost',
|
||||
help="local Host/IP to bind for connections from users")
|
||||
parser.add_argument('--card-bind-port', type=int, default=WSRC_DEFAULT_PORT_CARD,
|
||||
help="port number to bind for connections from cards")
|
||||
parser.add_argument('--card-bind-host', type=str, default='localhost',
|
||||
help="local Host/IP to bind for connections from cards")
|
||||
|
||||
if __name__ == '__main__':
|
||||
opts = parser.parse_args()
|
||||
|
||||
async def main():
|
||||
# we use different ports for user + card connections to ensure they can
|
||||
# have different packet filter rules apply to them
|
||||
async with serve(card_conn_hdlr, opts.card_bind_host, opts.card_bind_port), serve(user_conn_hdlr, opts.user_bind_host, opts.user_bind_port):
|
||||
await asyncio.get_running_loop().create_future() # run forever
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -1,103 +0,0 @@
|
||||
|
||||
Guide: Installing JAVA-card applets
|
||||
===================================
|
||||
|
||||
Almost all modern-day UICC cards have some form of JAVA-card / Sim-Toolkit support, which allows the installation
|
||||
of customer specific JAVA-card applets. The installation of JAVA-card applets is usually done via the standardized
|
||||
GlobalPlatform (GPC_SPE_034) ISD (Issuer Security Domain) application interface during the card provisioning process.
|
||||
(it is also possible to load JAVA-card applets in field via OTA-SMS, but that is beyond the scope of this guide). In
|
||||
this guide we will go through the individual steps that are required to load JAVA-card applet onto an UICC card.
|
||||
|
||||
|
||||
Preparation
|
||||
~~~~~~~~~~~
|
||||
|
||||
In this example we will install the CAP file HelloSTK_09122024.cap [1] on an sysmoISIM-SJA2 card. Since the interface
|
||||
is standardized, the exact card model does not matter.
|
||||
|
||||
The example applet makes use of the STK (Sim-Toolkit), so we must supply STK installation parameters. Those
|
||||
parameters are supplied in the form of a hexstring and should be provided by the applet manufacturer. The available
|
||||
parameters and their exact encoding is specified in ETSI TS 102 226, section 8.2.1.3.2.1. The installation of
|
||||
HelloSTK_09122024.cap [1], will require the following STK installation parameters: "010001001505000000000000000000000000"
|
||||
|
||||
During the installation, we also have to set a memory quota for the volatile and for the non volatile card memory.
|
||||
Those values also should be provided by the applet manufacturer. In this example, we will allow 255 bytes of volatile
|
||||
memory and 255 bytes of non volatile memory to be consumed by the applet.
|
||||
|
||||
To install JAVA-card applets, one must be in the possession of the key material belonging to the card. The keys are
|
||||
usually provided by the card manufacturer. The following example will use the following keyset:
|
||||
|
||||
+---------+----------------------------------+
|
||||
| Keyname | Keyvalue |
|
||||
+=========+==================================+
|
||||
| DEK/KIK | 5524F4BECFE96FB63FC29D6BAAC6058B |
|
||||
+---------+----------------------------------+
|
||||
| ENC/KIC | 542C37A6043679F2F9F71116418B1CD5 |
|
||||
+---------+----------------------------------+
|
||||
| MAC/KID | 34F11BAC8E5390B57F4E601372339E3C |
|
||||
+---------+----------------------------------+
|
||||
|
||||
[1] https://osmocom.org/projects/cellular-infrastructure/wiki/HelloSTK
|
||||
|
||||
|
||||
Applet Installation
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To prepare the installation, a secure channel to the ISD must be established first:
|
||||
|
||||
::
|
||||
|
||||
pySIM-shell (00:MF)> select ADF.ISD
|
||||
{
|
||||
"application_id": "a000000003000000",
|
||||
"proprietary_data": {
|
||||
"maximum_length_of_data_field_in_command_message": 255
|
||||
}
|
||||
}
|
||||
pySIM-shell (00:MF/ADF.ISD)> establish_scp02 --key-dek 5524F4BECFE96FB63FC29D6BAAC6058B --key-enc 542C37A6043679F2F9F71116418B1CD5 --key-mac 34F11BAC8E5390B57F4E601372339E3C --security-level 1
|
||||
Successfully established a SCP02[01] secure channel
|
||||
|
||||
.. warning:: In case you get an "EXCEPTION of type 'ValueError' occurred with message: card cryptogram doesn't match" error message, it is very likely that there is a problem with the key material. The card may lock the ISD access after a certain amount of failed tries. Carefully check the key material any try again.
|
||||
|
||||
|
||||
When the secure channel is established, we are ready to install the applet. The installation normally is a multi step
|
||||
procedure, where the loading of an executable load file is announced first, then loaded and then installed in a final
|
||||
step. The pySim-shell command ``install_cap`` automatically takes care of those three steps.
|
||||
|
||||
::
|
||||
|
||||
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> install_cap /home/user/HelloSTK_09122024.cap --install-parameters-non-volatile-memory-quota 255 --install-parameters-volatile-memory-quota 255 --install-parameters-stk 010001001505000000000000000000000000
|
||||
loading cap file: /home/user/HelloSTK_09122024.cap ...
|
||||
parameters:
|
||||
security-domain-aid: a000000003000000
|
||||
load-file: 569 bytes
|
||||
load-file-aid: d07002ca44
|
||||
module-aid: d07002ca44900101
|
||||
application-aid: d07002ca44900101
|
||||
install-parameters: c900ef1cc80200ffc70200ffca12010001001505000000000000000000000000
|
||||
step #1: install for load...
|
||||
step #2: load...
|
||||
Loaded a total of 573 bytes in 3 blocks. Don't forget install_for_install (and make selectable) now!
|
||||
step #3: install_for_install (and make selectable)...
|
||||
done.
|
||||
|
||||
The applet is now installed on the card. We can now quit pySim-shell and remove the card from the reader and test the
|
||||
applet in a mobile phone. There should be a new STK application with one menu entry shown, that will greet the user
|
||||
when pressed.
|
||||
|
||||
|
||||
Applet Removal
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
To remove the applet, we must establish a secure channel to the ISD (see above). Then we can delete the applet using the
|
||||
``delete_card_content`` command.
|
||||
|
||||
::
|
||||
|
||||
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> delete_card_content D07002CA44 --delete-related-objects
|
||||
|
||||
The parameter "D07002CA44" is the load-file-AID of the applet. The load-file-AID is encoded in the .cap file and also
|
||||
displayed during the installation process. It is also important to note that when the applet is installed, it cannot
|
||||
be installed (under the same AID) again until it is removed.
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
Retrieving card-individual keys via CardKeyProvider
|
||||
===================================================
|
||||
==================================================
|
||||
|
||||
When working with a batch of cards, or more than one card in general, it
|
||||
is a lot of effort to manually retrieve the card-specific PIN (like
|
||||
|
||||
@@ -41,13 +41,9 @@ pySim consists of several parts:
|
||||
shell
|
||||
trace
|
||||
legacy
|
||||
smpp2sim
|
||||
library
|
||||
library-esim
|
||||
osmo-smdpp
|
||||
sim-rest
|
||||
suci-keytool
|
||||
saip-tool
|
||||
wsrc
|
||||
|
||||
|
||||
Indices and tables
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
pySim eSIM libraries
|
||||
====================
|
||||
|
||||
The pySim eSIM libraries implement a variety of functionality related to the GSMA eSIM universe,
|
||||
including the various interfaces of SGP.21 + SGP.22, as well as Interoperable Profile decioding,
|
||||
validation, personalization and encoding.
|
||||
|
||||
.. automodule:: pySim.esim
|
||||
:members:
|
||||
|
||||
|
||||
GSMA SGP.21/22 Remote SIM Provisioning (RSP) - High Level
|
||||
---------------------------------------------------------
|
||||
|
||||
pySim.esim.rsp
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.rsp
|
||||
:members:
|
||||
|
||||
pySim.esim.es2p
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.es2p
|
||||
:members:
|
||||
|
||||
|
||||
pySim.esim.es8p
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.es8p
|
||||
:members:
|
||||
|
||||
|
||||
pySim.esim.es9p
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.es9p
|
||||
:members:
|
||||
|
||||
|
||||
GSMA SGP.21/22 Remote SIM Provisioning (RSP) - Low Level
|
||||
--------------------------------------------------------
|
||||
|
||||
pySim.esim.bsp
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.bsp
|
||||
:members:
|
||||
|
||||
pySim.esim.http_json_api
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.http_json_api
|
||||
:members:
|
||||
|
||||
pySim.esim.x509_cert
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.x509_cert
|
||||
:members:
|
||||
|
||||
SIMalliance / TCA Interoperable Profile
|
||||
---------------------------------------
|
||||
|
||||
|
||||
pySim.esim.saip
|
||||
~~~~~~~~~~~~~~~
|
||||
.. automodule:: pySim.esim.saip
|
||||
:members:
|
||||
|
||||
|
||||
pySim.esim.saip.oid
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.saip.oid
|
||||
:members:
|
||||
|
||||
pySim.esim.saip.personalization
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.saip.personalization
|
||||
:members:
|
||||
|
||||
pySim.esim.saip.templates
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.saip.templates
|
||||
:members:
|
||||
|
||||
pySim.esim.saip.validation
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. automodule:: pySim.esim.saip.validation
|
||||
:members:
|
||||
@@ -82,7 +82,7 @@ software.
|
||||
supplementary files
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The `smdpp-data/certs` directory contains the DPtls, DPauth and DPpb as well as CI certificates
|
||||
The `smdpp-data/certs`` directory contains the DPtls, DPauth and DPpb as well as CI certificates
|
||||
used; they are copied from GSMA SGP.26 v2. You can of course replace them with custom certificates
|
||||
if you're operating eSIM with a *private root CA*.
|
||||
|
||||
@@ -92,20 +92,10 @@ The `smdpp-data/upp` directory contains the UPP (Unprotected Profile Package) us
|
||||
commandline options
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Typically, you just run it without any arguments, and it will bind its plain-HTTP ES9+ interface to
|
||||
`localhost` TCP port 8000.
|
||||
osmo-smdpp currently doesn't have any configuration file or command line options. You just run it,
|
||||
and it will bind its plain-HTTP ES9+ interface to local TCP port 8000.
|
||||
|
||||
osmo-smdpp currently doesn't have any configuration file.
|
||||
|
||||
There are command line options for binding:
|
||||
|
||||
Bind the HTTP ES9+ to a port other than 8000::
|
||||
|
||||
./osmo-smdpp.py -p 8001
|
||||
|
||||
Bind the HTTP ES9+ to a different local interface::
|
||||
|
||||
./osmo-smdpp.py -H 127.0.0.1
|
||||
|
||||
DNS setup for your LPA
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
saip-tool
|
||||
=========
|
||||
|
||||
eSIM profiles are stored as a sequence of profile element (PE) objects in an ASN.1 DER encoded binary file. To inspect,
|
||||
verify or make changes to those files, the `saip-tool.py` utility can be used.
|
||||
|
||||
NOTE: The file format, eSIM SAIP (SimAlliance Interoperable Profile) is specified in `TCA eUICC Profile Package:
|
||||
Interoperable Format Technical Specification`
|
||||
|
||||
|
||||
Profile Package Examples
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
pySim ships with a set of TS48 profile package examples. Those examples can be found in `pysim/smdpp-data/upp`. The
|
||||
files can be used as input for `saip-tool.py`. (see also GSMA TS.48 - Generic eUICC Test Profile for Device Testing)
|
||||
|
||||
See also: https://github.com/GSMATerminals/Generic-eUICC-Test-Profile-for-Device-Testing-Public
|
||||
|
||||
JAVA card applets
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
The `saip-tool.py` can also be used to manage JAVA-card applets (Application PE) inside a profile package. The user has
|
||||
the option to add, remove and inspect applications and their instances. In the following we will discuss a few JAVA-card
|
||||
related use-cases of `saip-tool.py`
|
||||
|
||||
NOTE: see also `contrib` folder for script examples (`saip-tool_example_*.sh`)
|
||||
|
||||
Inserting applications
|
||||
----------------------
|
||||
|
||||
An application is usually inserted in two steps. In the first step, the application PE is created and populated with
|
||||
the executable code from a provided `.cap` or `.ijc` file. The user also has to pick a suitable load block AID.
|
||||
|
||||
The application instance, which exists inside the application PE, is created in a second step. Here the user must
|
||||
reference the load block AID and pick, among other application related parameters, a suitable class and instance AID.
|
||||
|
||||
Example: Adding a JAVA-card applet to an existing profile package
|
||||
::
|
||||
|
||||
# Step #1: Create the application PE and load the ijc contents from the .cap file:
|
||||
$ ./contrib/saip-tool.py upp.der add-app --output-file upp_with_app.der --applet-file app.cap --aid '1122334455'
|
||||
Read 28 PEs from file 'upp.der'
|
||||
Applying applet file: 'app.cap'...
|
||||
application PE inserted into PE Sequence after securityDomain PE AID: a000000151000000
|
||||
Writing 29 PEs to file 'upp_with_app.der'...
|
||||
|
||||
# Step #2: Create the application instance inside the application PE created in step #1:
|
||||
$ ./contrib/saip-tool.py upp_with_app.der add-app-inst --output-file upp_with_app_and_instance.der \
|
||||
--aid '1122334455' \
|
||||
--class-aid '112233445501' \
|
||||
--inst-aid '112233445501' \
|
||||
--app-privileges '00' \
|
||||
--app-spec-pars '00' \
|
||||
--uicc-toolkit-app-spec-pars '01001505000000000000000000000000'
|
||||
Read 29 PEs from file 'upp_with_app.der'
|
||||
Found Load Package AID: 1122334455, adding new instance AID: 112233445501 to Application PE...
|
||||
Writing 29 PEs to file 'upp_with_app_and_instance.der'...
|
||||
|
||||
NOTE: The parameters of the sub-commands `add-app` and `add-app-inst` are application specific. It is up to the application
|
||||
developer to pick parameters that suit the application correctly. For an exact command reference see section
|
||||
`saip-tool syntax`. For parameter details see `TCA eUICC Profile Package: Interoperable Format Technical Specification`,
|
||||
section 8.7 and ETSI TS 102 226, section 8.2.1.3.2
|
||||
|
||||
|
||||
Inspecting applications
|
||||
-----------------------
|
||||
|
||||
To inspect the application PE contents of an existing profile package, sub-command `info` with parameter '--apps' can
|
||||
be used. This command lists out all application and their parameters in detail. This allows an application developer
|
||||
to check if the applet insertaion was carried out as expected.
|
||||
|
||||
Example: Listing applications and their parameters
|
||||
::
|
||||
|
||||
$ ./contrib/saip-tool.py upp_with_app_and_instance.der info --apps
|
||||
Read 29 PEs from file 'upp_with_app_and_instance.der'
|
||||
Application #0:
|
||||
loadBlock:
|
||||
loadPackageAID: '1122334455' (5 bytes)
|
||||
loadBlockObject: '01000fdecaffed010204000105d07002ca440200...681080056810a00633b44104b431066800a10231' (569 bytes)
|
||||
instanceList[0]:
|
||||
applicationLoadPackageAID: '1122334455' (5 bytes)
|
||||
classAID: '112233445501' (8 bytes)
|
||||
instanceAID: '112233445501' (8 bytes)
|
||||
applicationPrivileges: '00' (1 bytes)
|
||||
lifeCycleState: '07' (1 bytes)
|
||||
applicationSpecificParametersC9: '00' (1 bytes)
|
||||
applicationParameters:
|
||||
uiccToolkitApplicationSpecificParametersField: '01001505000000000000000000000000' (16 bytes)
|
||||
|
||||
In case further analysis with external tools or transfer of applications from one profile package to another is
|
||||
necessary, the executable code in the `loadBlockObject` field can be extracted to an `.ijc` or an `.cap` file.
|
||||
|
||||
Example: Extracting applications from a profile package
|
||||
::
|
||||
|
||||
$ ./contrib/saip-tool.py upp_with_app_and_instance.der extract-apps --output-dir ./apps --format ijc
|
||||
Read 29 PEs from file 'upp_with_app_and_instance.der'
|
||||
Writing Load Package AID: 1122334455 to file ./apps/8949449999999990023f-1122334455.ijc
|
||||
|
||||
|
||||
Removing applications
|
||||
---------------------
|
||||
|
||||
An application PE can be removed using sub-command `remove-app`. The user passes the load package AID as parameter. Then
|
||||
`saip-tool.py` will search for the related application PE and delete it from the PE sequence.
|
||||
|
||||
Example: Remove an application from a profile package
|
||||
::
|
||||
|
||||
$ ./contrib/saip-tool.py upp_with_app_and_instance.der remove-app --output-file upp_without_app.der --aid '1122334455'
|
||||
Read 29 PEs from file 'upp_with_app_and_instance.der'
|
||||
Found Load Package AID: 1122334455, removing related PE (id=23) from Sequence...
|
||||
Removing PE application (id=23) from Sequence...
|
||||
Writing 28 PEs to file 'upp_without_app.der'...
|
||||
|
||||
In some cases it is useful to remove only an instance from an existing application PE. This may be the case when the
|
||||
an application developer wants to modify parameters of an application by removing and re-adding the instance. The
|
||||
operation basically rolls the state back to step 1 explained in section :ref:`Inserting applications`
|
||||
|
||||
Example: Remove an application instance from an application PE
|
||||
::
|
||||
|
||||
$ ./contrib/saip-tool.py upp_with_app_and_instance.der remove-app-inst --output-file upp_without_app.der --aid '1122334455' --inst-aid '112233445501'
|
||||
Read 29 PEs from file 'upp_with_app_and_instance.der'
|
||||
Found Load Package AID: 1122334455, removing instance AID: 112233445501 from Application PE...
|
||||
Removing instance from Application PE...
|
||||
Writing 29 PEs to file 'upp_with_app.der'...
|
||||
|
||||
|
||||
saip-tool syntax
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. argparse::
|
||||
:module: contrib.saip-tool
|
||||
:func: parser
|
||||
:prog: contrib/saip-tool.py
|
||||
@@ -1,4 +1,4 @@
|
||||
pySim-shell
|
||||
pySim-shell
|
||||
===========
|
||||
|
||||
pySim-shell is an interactive command line shell for all kind of interactions with SIM cards,
|
||||
@@ -67,7 +67,6 @@ Usage Examples
|
||||
:caption: Tutorials for pySIM-shell:
|
||||
|
||||
suci-tutorial
|
||||
cap-tutorial
|
||||
|
||||
|
||||
Advanced Topics
|
||||
@@ -1006,24 +1005,6 @@ ARA-M applet. Use it with caution, there is no undo. Any rules later
|
||||
intended must be manually inserted again using :ref:`aram_store_ref_ar_do`
|
||||
|
||||
|
||||
aram_lock
|
||||
~~~~~~~~~
|
||||
This command allows to lock the access to the STORE DATA command. This renders
|
||||
all access rules stored within the ARA-M applet effectively read-only. The lock
|
||||
can only be removed via a secure channel to the security domain and is therefore
|
||||
suitable to prevent unauthorized changes to ARA-M rules.
|
||||
|
||||
Removal of the lock:
|
||||
::
|
||||
|
||||
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> install_for_personalization A00000015141434C00
|
||||
pySIM-shell (SCP02[01]:00:MF/ADF.ISD)> apdu --expect-sw 9000 80E2900001A2
|
||||
|
||||
NOTE: ARA-M Locking is a proprietary feature that is specific to sysmocom's
|
||||
fork of Bertrand Martel's ARA-M implementation. ARA-M Locking is supported in
|
||||
newer (2025) applet versions from v0.1.0 onward.
|
||||
|
||||
|
||||
GlobalPlatform commands
|
||||
-----------------------
|
||||
|
||||
@@ -1066,18 +1047,6 @@ delete_key
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.del_key_parser
|
||||
|
||||
load
|
||||
~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.load_parser
|
||||
|
||||
install_cap
|
||||
~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.install_cap_parser
|
||||
|
||||
install_for_personalization
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
@@ -1090,12 +1059,6 @@ install_for_install
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.inst_inst_parser
|
||||
|
||||
install_for_load
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:module: pySim.global_platform
|
||||
:func: ADF_SD.AddlShellCommands.inst_load_parser
|
||||
|
||||
delete_card_content
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
@@ -1155,7 +1118,7 @@ es10x_store_data
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.es10x_store_data_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.es10x_store_data_parser
|
||||
|
||||
get_euicc_configured_addresses
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -1174,7 +1137,7 @@ set_default_dp_address
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.set_def_dp_addr_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.set_def_dp_addr_parser
|
||||
|
||||
get_euicc_challenge
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -1317,7 +1280,7 @@ remove_notification_from_list
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.rem_notif_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.rem_notif_parser
|
||||
|
||||
Example::
|
||||
|
||||
@@ -1366,7 +1329,7 @@ enable_profile
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.en_prof_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.en_prof_parser
|
||||
|
||||
Example (successful)::
|
||||
|
||||
@@ -1388,7 +1351,7 @@ disable_profile
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.dis_prof_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.dis_prof_parser
|
||||
|
||||
Example (successful)::
|
||||
|
||||
@@ -1402,7 +1365,7 @@ delete_profile
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.del_prof_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.del_prof_parser
|
||||
|
||||
Example::
|
||||
|
||||
@@ -1411,13 +1374,6 @@ Example::
|
||||
"delete_result": "ok"
|
||||
}
|
||||
|
||||
euicc_memory_reset
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.mem_res_parser
|
||||
|
||||
|
||||
get_eid
|
||||
~~~~~~~
|
||||
@@ -1436,7 +1392,7 @@ set_nickname
|
||||
|
||||
.. argparse::
|
||||
:module: pySim.euicc
|
||||
:func: CardApplicationISDR.AddlShellCommands.set_nickname_parser
|
||||
:func: ADF_ISDR.AddlShellCommands.set_nickname_parser
|
||||
|
||||
Example::
|
||||
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
sim-rest-server
|
||||
===============
|
||||
|
||||
Sometimes there are use cases where a [remote] application will need
|
||||
access to a USIM for authentication purposes. This is, for example, in
|
||||
case an IMS test client needs to perform USIM based authentication
|
||||
against an IMS core.
|
||||
|
||||
The pysim repository contains two programs: `sim-rest-server.py` and
|
||||
`sim-rest-client.py` that implement a simple approach to achieve the
|
||||
above:
|
||||
|
||||
`sim-rest-server.py` speaks to a [usually local] USIM via the PC/SC
|
||||
API and provides a high-level REST API towards [local or remote]
|
||||
applications that wish to perform UMTS AKA using the USIM.
|
||||
|
||||
`sim-rest-client.py` implements a small example client program to
|
||||
illustrate how the REST API provided by `sim-rest-server.py` can be
|
||||
used.
|
||||
|
||||
REST API Calls
|
||||
--------------
|
||||
|
||||
POST /sim-auth-api/v1/slot/SLOT_NR
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
where SLOT_NR is the integer-encoded slot number (corresponds to PC/SC
|
||||
reader number). When using a single sysmoOCTSIM board, this is in the range of 0..7
|
||||
|
||||
Example: `/sim-auth-api/v1/slot/0` for the first slot.
|
||||
|
||||
Request Body
|
||||
############
|
||||
|
||||
The request body is a JSON document, comprising of
|
||||
1. the RAND and AUTN parameters as hex-encoded string
|
||||
2. the application against which to authenticate (USIM, ISIM)
|
||||
|
||||
Example:
|
||||
::
|
||||
|
||||
{
|
||||
"rand": "bb685a4b2fc4d697b9d6a129dd09a091",
|
||||
"autn": "eea7906f8210000004faf4a7df279b56"
|
||||
}
|
||||
|
||||
HTTP Status Codes
|
||||
#################
|
||||
|
||||
HTTP status codes are used to represent errors within the REST server
|
||||
and the SIM reader hardware. They are not used to communicate protocol
|
||||
level errors reported by the SIM Card. An unsuccessful authentication
|
||||
will hence have a `200 OK` HTTP Status code and then encode the SIM
|
||||
specific error information in the Response Body.
|
||||
|
||||
====== =========== ================================
|
||||
Status Code Description
|
||||
------ ----------- --------------------------------
|
||||
200 OK Successful execution
|
||||
400 Bad Request Request body is malformed
|
||||
404 Not Found Specified SIM Slot doesn't exist
|
||||
410 Gone No SIM card inserted in slot
|
||||
====== =========== ================================
|
||||
|
||||
Response Body
|
||||
#############
|
||||
|
||||
The response body is a JSON document, either
|
||||
|
||||
#. a successful outcome; encoding RES, CK, IK as hex-encoded string
|
||||
#. a sync failure; encoding AUTS as hex-encoded string
|
||||
#. errors
|
||||
#. authentication error (incorrect MAC)
|
||||
#. authentication error (security context not supported)
|
||||
#. key freshness failure
|
||||
#. unspecified card error
|
||||
|
||||
Example (succcess):
|
||||
::
|
||||
|
||||
{
|
||||
"successful_3g_authentication": {
|
||||
"res": "b15379540ec93985",
|
||||
"ck": "713fde72c28cbd282a4cd4565f3d6381",
|
||||
"ik": "2e641727c95781f1020d319a0594f31a",
|
||||
"kc": "771a2c995172ac42"
|
||||
}
|
||||
}
|
||||
|
||||
Example (re-sync case):
|
||||
::
|
||||
|
||||
{
|
||||
"synchronisation_failure": {
|
||||
"auts": "dc2a591fe072c92d7c46ecfe97e5"
|
||||
}
|
||||
}
|
||||
|
||||
Concrete example using the included sysmoISIM-SJA2
|
||||
--------------------------------------------------
|
||||
|
||||
This was tested using SIMs ending in IMSI numbers 45890...45899
|
||||
|
||||
The following command were executed successfully:
|
||||
|
||||
Slot 0
|
||||
::
|
||||
|
||||
$ /usr/local/src/pysim/contrib/sim-rest-client.py -c 1 -n 0 -k 841EAD87BC9D974ECA1C167409357601 -o 3211CACDD64F51C3FD3013ECD9A582A0
|
||||
-> {'rand': 'fb195c7873b20affa278887920b9dd57', 'autn': 'd420895a6aa2000089cd016f8d8ae67c'}
|
||||
<- {'successful_3g_authentication': {'res': '131004db2ff1ce8e', 'ck': 'd42eb5aa085307903271b2422b698bad', 'ik': '485f81e6fd957fe3cad374adf12fe1ca', 'kc': '64d3f2a32f801214'}}
|
||||
|
||||
Slot 1
|
||||
::
|
||||
|
||||
$ /usr/local/src/pysim/contrib/sim-rest-client.py -c 1 -n 1 -k 5C2CE9633FF9B502B519A4EACD16D9DF -o 9834D619E71A02CD76F00CC7AA34FB32
|
||||
-> {'rand': '433dc5553db95588f1d8b93870930b66', 'autn': '126bafdcbe9e00000026a208da61075d'}
|
||||
<- {'successful_3g_authentication': {'res': '026d7ac42d379207', 'ck': '83a90ba331f47a95c27a550b174c4a1f', 'ik': '31e1d10329ffaf0ca1684a1bf0b0a14a', 'kc': 'd15ac5b0fff73ecc'}}
|
||||
@@ -1,57 +0,0 @@
|
||||
pySim-smpp2sim
|
||||
==============
|
||||
|
||||
This is a program to emulate the entire communication path SMSC-CN-RAN-ME
|
||||
that is usually between an OTA backend and the SIM card. This allows
|
||||
to play with SIM OTA technology without using a mobile network or even
|
||||
a mobile phone.
|
||||
|
||||
An external application can act as SMPP ESME and must encode (and
|
||||
encrypt/sign) the OTA SMS and submit them via SMPP to this program, just
|
||||
like it would submit it normally to a SMSC (SMS Service Centre). The
|
||||
program then re-formats the SMPP-SUBMIT into a SMS DELIVER TPDU and
|
||||
passes it via an ENVELOPE APDU to the SIM card that is locally inserted
|
||||
into a smart card reader.
|
||||
|
||||
The path from SIM to external OTA application works the opposite way.
|
||||
|
||||
The default SMPP system_id is `test`. Likewise, the default SMPP
|
||||
password is `test`
|
||||
|
||||
Running pySim-smpp2sim
|
||||
----------------------
|
||||
|
||||
The command accepts the same command line arguments for smart card interface device selection as pySim-shell,
|
||||
as well as a few SMPP specific arguments:
|
||||
|
||||
.. argparse::
|
||||
:module: pySim-smpp2sim
|
||||
:func: option_parser
|
||||
:prog: pySim-smpp2sim.py
|
||||
|
||||
|
||||
Example execution with sample output
|
||||
------------------------------------
|
||||
|
||||
So for a simple system with a single PC/SC device, you would typically use something like
|
||||
`./pySim-smpp2sim.py -p0` to start the program. You will see output like this at start-up
|
||||
::
|
||||
|
||||
Using reader PCSC[HID Global OMNIKEY 3x21 Smart Card Reader [OMNIKEY 3x21 Smart Card Reader] 00 00]
|
||||
INFO root: Binding Virtual SMSC to TCP Port 2775 at ::
|
||||
|
||||
The application has hence bound to local TCP port 2775 and expects your SMS-sending applications to send their
|
||||
SMS there. Once you do, you will see log output like below:
|
||||
::
|
||||
|
||||
WARNING smpp.twisted.protocol: SMPP connection established from ::ffff:127.0.0.1 to port 2775
|
||||
INFO smpp.twisted.server: Added CommandId.bind_transceiver bind for 'test'. Active binds: CommandId.bind_transceiver: 1, CommandId.bind_transmitter: 0, CommandId.bind_receiver: 0. Max binds: 2
|
||||
INFO smpp.twisted.protocol: Bind request succeeded for test. 1 active binds
|
||||
|
||||
And once your external program is sending SMS to the simulated SMSC, it will log something like
|
||||
::
|
||||
|
||||
INFO root: SMS_DELIVER(MTI=0, MMS=False, LP=False, RP=False, UDHI=True, SRI=False, OA=AddressField(TON=international, NPI=unknown, 12), PID=7f, DCS=f6, SCTS=bytearray(b'"pR\x00\x00\x00\x00'), UDL=45, UD=b"\x02p\x00\x00(\x15\x16\x19\x12\x12\xb0\x00\x01'\xfa(\xa5\xba\xc6\x9d<^\x9d\xf2\xc7\x15]\xfd\xdeD\x9c\x82k#b\x15Ve0x{0\xe8\xbe]")
|
||||
SMSPPDownload(DeviceIdentities({'source_dev_id': 'network', 'dest_dev_id': 'uicc'}),Address({'ton_npi': 0, 'call_number': '0123456'}),SMS_TPDU({'tpdu': '400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d'}))
|
||||
INFO root: ENVELOPE: d147820283818604001032548b3b400290217ff6227052000000002d02700000281516191212b0000127fa28a5bac69d3c5e9df2c7155dfdde449c826b236215566530787b30e8be5d
|
||||
INFO root: SW 9000: 027100002412b000019a551bb7c28183652de0ace6170d0e563c5e949a3ba56747fe4c1dbbef16642c
|
||||
@@ -1,58 +0,0 @@
|
||||
suci-keytool
|
||||
============
|
||||
|
||||
Subscriber concealment is an important feature of the 5G SA architecture: It avoids the many privacy
|
||||
issues associated with having a permanent identifier (SUPI, traditionally the IMSI) transmitted in plain text
|
||||
over the air interface. Using SUCI solves this issue not just for the air interface; it even ensures the SUPI/IMSI
|
||||
is not known to the visited network (VPLMN) at all.
|
||||
|
||||
In principle, the SUCI mechanism works by encrypting the SUPI by asymmetric (public key) cryptography:
|
||||
Only the HPLMN is in possession of the private key and hence can decrypt the SUCI to the SUPI, while
|
||||
each subscriber has the public key in order to encrypt their SUPI into the SUCI. In reality, the
|
||||
details are more complex, as there are ephemeral keys and cryptographic MAC involved.
|
||||
|
||||
In any case, in order to operate a SUCI-enabled 5G SA network, you will have to
|
||||
|
||||
#. generate a ECC key pair of public + private key
|
||||
#. deploy the public key on your USIMs
|
||||
#. deploy the private key on your 5GC, specifically the UDM function
|
||||
|
||||
pysim contains (int its `contrib` directory) a small utility program that can make it easy to generate
|
||||
such keys: `suci-keytool.py`
|
||||
|
||||
Generating keys
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Example: Generating a *secp256r1* ECC public key pair and storing it to `/tmp/suci.key`:
|
||||
::
|
||||
|
||||
$ ./contrib/suci-keytool.py --key-file /tmp/suci.key generate-key --curve secp256r1
|
||||
|
||||
Dumping public keys
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In order to store the key to SIM cards as part of `ADF.USIM/DF.5GS/EF.SUCI_Calc_Info`, you will need
|
||||
a hexadecimal representation of the public key. You can achieve that using the `dump-pub-key` operation
|
||||
of suci-keytool:
|
||||
|
||||
Example: Dumping the public key part from a previously generated key file:
|
||||
::
|
||||
|
||||
$ ./contrib/suci-keytool.py --key-file /tmp/suci.key dump-pub-key
|
||||
0473152f32523725f5175d255da2bd909de97b1d06449a9277bc629fe42112f8643e6b69aa6dce6c86714ccbe6f2e0f4f4898d102e2b3f0c18ce26626f052539bb
|
||||
|
||||
If you want the point-compressed representation, you can use the `--compressed` option:
|
||||
::
|
||||
|
||||
$ ./contrib/suci-keytool.py --key-file /tmp/suci.key dump-pub-key --compressed
|
||||
0373152f32523725f5175d255da2bd909de97b1d06449a9277bc629fe42112f864
|
||||
|
||||
|
||||
|
||||
suci-keytool syntax
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. argparse::
|
||||
:module: contrib.suci-keytool
|
||||
:func: arg_parser
|
||||
:prog: contrib/suci-keytool.py
|
||||
57
docs/wsrc.rst
Normal file
57
docs/wsrc.rst
Normal file
@@ -0,0 +1,57 @@
|
||||
WebSocket Remote Card (WSRC)
|
||||
============================
|
||||
|
||||
WSRC (*Web Socket Remote Card*) is a mechanism by which card readers can be made remotely available
|
||||
via a computer network. The transport mechanism is (as the name implies) a WebSocket. This transport
|
||||
method was chosen to be as firewall/NAT friendly as possible.
|
||||
|
||||
WSRC Network Architecture
|
||||
-------------------------
|
||||
|
||||
In a WSRC network, there are three major elements:
|
||||
|
||||
* The **WSRC Card Client** which exposes a locally attached smart card (usually via a Smart Card Reader)
|
||||
to a remote *WSRC Server*
|
||||
* The **WSRC Server** manges incoming connections from both *WSRC Card Clients* as well as *WSRC User Clients*
|
||||
* The **WSRC User Client** is a user application, like for example pySim-shell, which is accessing a remote
|
||||
card by connecting to the *WSRC Server* which relays the information to the selected *WSRC Card Client*
|
||||
|
||||
WSRC Protocol
|
||||
-------------
|
||||
|
||||
The WSRC protocl consits of JSON objects being sent over a websocket. The websocket communication itself
|
||||
is based on HTTP and should usually operate via TLS for security reasons.
|
||||
|
||||
The detailed protocol is currently still WIP. The plan is to document it here.
|
||||
|
||||
|
||||
pySim implementations
|
||||
---------------------
|
||||
|
||||
|
||||
wsrc_server
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:filename: ../contrib/wsrc_server.py
|
||||
:func: parser
|
||||
:prog: contrib/wsrc_server.py
|
||||
|
||||
|
||||
wsrc_card_client
|
||||
~~~~~~~~~~~~~~~~
|
||||
.. argparse::
|
||||
:filename: ../contrib/wsrc_card_client.py
|
||||
:func: parser
|
||||
:prog: contrib/wsrc_card_client.py
|
||||
|
||||
pySim-shell
|
||||
~~~~~~~~~~~
|
||||
|
||||
pySim-shell can talk to a remote card via WSRC if you use the *wsrc transport*, for example like this:
|
||||
|
||||
::
|
||||
|
||||
./pySim-shell.py --wsrc-eid 89882119900000000000000000007280 --wsrc-serer-url ws://localhost:4220/
|
||||
|
||||
|
||||
You can specify `--wsrc-eid` or `--wsrc-iccid` to identify the remote eUICC or UICC you would like to select.
|
||||
1219
osmo-smdpp.py
1219
osmo-smdpp.py
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@ from pySim.commands import SimCardCommands
|
||||
from pySim.transport import init_reader, argparse_add_reader_args
|
||||
from pySim.legacy.cards import _cards_classes, card_detect
|
||||
from pySim.utils import derive_milenage_opc, calculate_luhn, dec_iccid
|
||||
from pySim.ts_51_011 import EF_AD
|
||||
from pySim.profile.ts_51_011 import EF_AD
|
||||
from pySim.legacy.ts_51_011 import EF
|
||||
from pySim.card_handler import *
|
||||
from pySim.utils import *
|
||||
|
||||
@@ -31,7 +31,7 @@ import sys
|
||||
|
||||
from osmocom.utils import h2b, h2s, swap_nibbles, rpad
|
||||
|
||||
from pySim.ts_51_011 import EF_SST_map, EF_AD
|
||||
from pySim.profile.ts_51_011 import EF_SST_map, EF_AD
|
||||
from pySim.legacy.ts_51_011 import EF, DF
|
||||
from pySim.ts_31_102 import EF_UST_map
|
||||
from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
|
||||
|
||||
@@ -60,7 +60,6 @@ from pySim.card_handler import CardHandler, CardHandlerAuto
|
||||
from pySim.filesystem import CardMF, CardEF, CardDF, CardADF, LinFixedEF, TransparentEF, BerTlvEF
|
||||
from pySim.ts_102_221 import pin_names
|
||||
from pySim.ts_102_222 import Ts102222Commands
|
||||
from pySim.gsm_r import DF_EIRENE
|
||||
from pySim.cat import ProactiveCommand
|
||||
|
||||
from pySim.card_key_provider import CardKeyProviderCsv, card_key_provider_register, card_key_provider_get_field
|
||||
@@ -155,7 +154,6 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
# When a card object and a runtime state is present, (re)equip pySim-shell with everything that is
|
||||
# needed to operate on cards.
|
||||
if self.card and self.rs:
|
||||
self.rs.reset()
|
||||
self.lchan = self.rs.lchan[0]
|
||||
self._onchange_conserve_write(
|
||||
'conserve_write', False, self.conserve_write)
|
||||
@@ -219,23 +217,18 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
self.cmd2.poutput("<- %s: %s" % (sw, resp))
|
||||
|
||||
def update_prompt(self):
|
||||
if self.rs and self.rs.adm_verified:
|
||||
prompt_char = '#'
|
||||
else:
|
||||
prompt_char = '>'
|
||||
|
||||
if self.lchan:
|
||||
path_str = self.lchan.selected_file.fully_qualified_path_str(not self.numeric_path)
|
||||
scp = self.lchan.scc.scp
|
||||
if scp:
|
||||
self.prompt = 'pySIM-shell (%s:%02u:%s)%c ' % (str(scp), self.lchan.lchan_nr, path_str, prompt_char)
|
||||
self.prompt = 'pySIM-shell (%s:%02u:%s)> ' % (str(scp), self.lchan.lchan_nr, path_str)
|
||||
else:
|
||||
self.prompt = 'pySIM-shell (%02u:%s)%c ' % (self.lchan.lchan_nr, path_str, prompt_char)
|
||||
self.prompt = 'pySIM-shell (%02u:%s)> ' % (self.lchan.lchan_nr, path_str)
|
||||
else:
|
||||
if self.card:
|
||||
self.prompt = 'pySIM-shell (no card profile)%c ' % prompt_char
|
||||
self.prompt = 'pySIM-shell (no card profile)> '
|
||||
else:
|
||||
self.prompt = 'pySIM-shell (no card)%c ' % prompt_char
|
||||
self.prompt = 'pySIM-shell (no card)> '
|
||||
|
||||
@cmd2.with_category(CUSTOM_CATEGORY)
|
||||
def do_intro(self, _):
|
||||
@@ -294,7 +287,7 @@ Online manual available at https://downloads.osmocom.org/docs/pysim/master/html/
|
||||
if self.rs is None:
|
||||
# In case no runtime state is available we go the direct route
|
||||
self.card._scc.reset_card()
|
||||
atr = self.card._scc.get_atr()
|
||||
atr = b2h(self.card._scc.get_atr())
|
||||
else:
|
||||
atr = self.rs.reset(self)
|
||||
self.poutput('Card ATR: %s' % atr)
|
||||
@@ -860,8 +853,6 @@ class PySimCommands(CommandSet):
|
||||
self._cmd.lchan.scc.verify_chv(adm_chv_num, h2b(pin_adm))
|
||||
else:
|
||||
raise ValueError("error: cannot authenticate, no adm-pin!")
|
||||
self._cmd.rs.adm_verified = True
|
||||
self._cmd.update_prompt()
|
||||
|
||||
def do_cardinfo(self, opts):
|
||||
"""Display information about the currently inserted card"""
|
||||
@@ -1159,18 +1150,14 @@ if __name__ == '__main__':
|
||||
# Run optional commands
|
||||
for c in opts.execute_command:
|
||||
if not startup_errors:
|
||||
stop = app.onecmd_plus_hooks(c)
|
||||
if stop == True:
|
||||
sys.exit(0)
|
||||
app.onecmd_plus_hooks(c)
|
||||
else:
|
||||
print("Errors during startup, refusing to execute command (%s)" % c)
|
||||
|
||||
# Run optional command
|
||||
if opts.command:
|
||||
if not startup_errors:
|
||||
stop = app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
|
||||
if stop == True:
|
||||
sys.exit(0)
|
||||
app.onecmd_plus_hooks('{} {}'.format(opts.command, ' '.join(opts.command_args)))
|
||||
else:
|
||||
print("Errors during startup, refusing to execute command (%s)" % opts.command)
|
||||
|
||||
@@ -1181,9 +1168,7 @@ if __name__ == '__main__':
|
||||
print("Error: script file (%s) not readable!" % opts.script)
|
||||
startup_errors = True
|
||||
else:
|
||||
stop = app.onecmd_plus_hooks('{} {}'.format('run_script', opts.script), add_to_history = False)
|
||||
if stop == True:
|
||||
sys.exit(0)
|
||||
app.onecmd_plus_hooks('{} {}'.format('run_script', opts.script), add_to_history = False)
|
||||
else:
|
||||
print("Errors during startup, refusing to execute script (%s)" % opts.script)
|
||||
|
||||
|
||||
@@ -1,428 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Program to emulate the entire communication path SMSC-MSC-BSC-BTS-ME
|
||||
# that is usually between an OTA backend and the SIM card. This allows
|
||||
# to play with SIM OTA technology without using a mobile network or even
|
||||
# a mobile phone.
|
||||
#
|
||||
# An external application must encode (and encrypt/sign) the OTA SMS
|
||||
# and submit them via SMPP to this program, just like it would submit
|
||||
# it normally to a SMSC (SMS Service Centre). The program then re-formats
|
||||
# the SMPP-SUBMIT into a SMS DELIVER TPDU and passes it via an ENVELOPE
|
||||
# APDU to the SIM card that is locally inserted into a smart card reader.
|
||||
#
|
||||
# The path from SIM to external OTA application works the opposite way.
|
||||
|
||||
# (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 General Public License as published by
|
||||
# the Free Software Foundation, either version 2 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import colorlog
|
||||
|
||||
from twisted.protocols import basic
|
||||
from twisted.internet import defer, endpoints, protocol, reactor, task
|
||||
from twisted.cred.portal import IRealm
|
||||
from twisted.cred.checkers import InMemoryUsernamePasswordDatabaseDontUse
|
||||
from twisted.cred.portal import Portal
|
||||
from zope.interface import implementer
|
||||
|
||||
from smpp.twisted.config import SMPPServerConfig
|
||||
from smpp.twisted.server import SMPPServerFactory, SMPPBindManager
|
||||
from smpp.twisted.protocol import SMPPSessionStates, DataHandlerResponse
|
||||
|
||||
from smpp.pdu import pdu_types, operations, pdu_encoding
|
||||
|
||||
from pySim.sms import SMS_DELIVER, SMS_SUBMIT, AddressField
|
||||
|
||||
from pySim.transport import LinkBase, ProactiveHandler, argparse_add_reader_args, init_reader, ApduTracer
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.cards import UiccCardBase
|
||||
from pySim.exceptions import *
|
||||
from pySim.cat import ProactiveCommand, SendShortMessage, SMS_TPDU, SMSPPDownload, BearerDescription
|
||||
from pySim.cat import DeviceIdentities, Address, OtherAddress, UiccTransportLevel, BufferSize
|
||||
from pySim.cat import ChannelStatus, ChannelData, ChannelDataLength
|
||||
from pySim.utils import b2h, h2b
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# MSISDNs to use when generating proactive SMS messages
|
||||
SIM_MSISDN='23'
|
||||
ESME_MSISDN='12'
|
||||
|
||||
# HACK: we need some kind of mapping table between system_id and card-reader
|
||||
# or actually route based on MSISDNs
|
||||
hackish_global_smpp = None
|
||||
|
||||
class MyApduTracer(ApduTracer):
|
||||
def trace_response(self, cmd, sw, resp):
|
||||
print("-> %s %s" % (cmd[:10], cmd[10:]))
|
||||
print("<- %s: %s" % (sw, resp))
|
||||
|
||||
class TcpProtocol(protocol.Protocol):
|
||||
def dataReceived(self, data):
|
||||
pass
|
||||
|
||||
def connectionLost(self, reason):
|
||||
pass
|
||||
|
||||
|
||||
def tcp_connected_callback(p: protocol.Protocol):
|
||||
"""called by twisted TCP client."""
|
||||
logger.error("%s: connected!" % p)
|
||||
|
||||
class ProactChannel:
|
||||
"""Representation of a single proective channel."""
|
||||
def __init__(self, channels: 'ProactChannels', chan_nr: int):
|
||||
self.channels = channels
|
||||
self.chan_nr = chan_nr
|
||||
self.ep = None
|
||||
|
||||
def close(self):
|
||||
"""Close the channel."""
|
||||
if self.ep:
|
||||
self.ep.disconnect()
|
||||
self.channels.channel_delete(self.chan_nr)
|
||||
|
||||
class ProactChannels:
|
||||
"""Wrapper class for maintaining state of proactive channels."""
|
||||
def __init__(self):
|
||||
self.channels = {}
|
||||
|
||||
def channel_create(self) -> ProactChannel:
|
||||
"""Create a new proactive channel, allocating its integer number."""
|
||||
for i in range(1, 9):
|
||||
if not i in self.channels:
|
||||
self.channels[i] = ProactChannel(self, i)
|
||||
return self.channels[i]
|
||||
raise ValueError('Cannot allocate another channel: All channels active')
|
||||
|
||||
def channel_delete(self, chan_nr: int):
|
||||
del self.channels[chan_nr]
|
||||
|
||||
class Proact(ProactiveHandler):
|
||||
#def __init__(self, smpp_factory):
|
||||
# self.smpp_factory = smpp_factory
|
||||
def __init__(self):
|
||||
self.channels = ProactChannels()
|
||||
|
||||
@staticmethod
|
||||
def _find_first_element_of_type(instlist, cls):
|
||||
for i in instlist:
|
||||
if isinstance(i, cls):
|
||||
return i
|
||||
return None
|
||||
|
||||
"""Call-back which the pySim transport core calls whenever it receives a
|
||||
proactive command from the SIM."""
|
||||
def handle_SendShortMessage(self, pcmd: ProactiveCommand):
|
||||
# {'smspp_download': [{'device_identities': {'source_dev_id': 'network',
|
||||
# 'dest_dev_id': 'uicc'}},
|
||||
# {'address': {'ton_npi': {'ext': True,
|
||||
# 'type_of_number': 'international',
|
||||
# 'numbering_plan_id': 'isdn_e164'},
|
||||
# 'call_number': '79'}},
|
||||
# {'sms_tpdu': {'tpdu': '40048111227ff6407070611535004d02700000481516011212000001fe4c0943aea42e45021c078ae06c66afc09303608874b72f58bacadb0dcf665c29349c799fbb522e61709c9baf1890015e8e8e196e36153106c8b92f95153774'}}
|
||||
# ]}
|
||||
"""Card requests sending a SMS. We need to pass it on to the ESME via SMPP."""
|
||||
logger.info("SendShortMessage")
|
||||
logger.info(pcmd)
|
||||
# Relevant parts in pcmd: Address, SMS_TPDU
|
||||
addr_ie = Proact._find_first_element_of_type(pcmd.children, Address)
|
||||
sms_tpdu_ie = Proact._find_first_element_of_type(pcmd.children, SMS_TPDU)
|
||||
raw_tpdu = sms_tpdu_ie.decoded['tpdu']
|
||||
submit = SMS_SUBMIT.from_bytes(raw_tpdu)
|
||||
submit.tp_da = AddressField(addr_ie.decoded['call_number'], addr_ie.decoded['ton_npi']['type_of_number'],
|
||||
addr_ie.decoded['ton_npi']['numbering_plan_id'])
|
||||
logger.info(submit)
|
||||
self.send_sms_via_smpp(submit)
|
||||
|
||||
def handle_OpenChannel(self, pcmd: ProactiveCommand):
|
||||
"""Card requests opening a new channel via a UDP/TCP socket."""
|
||||
# {'open_channel': [{'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'open_channel',
|
||||
# 'command_qualifier': 3}},
|
||||
# {'device_identities': {'source_dev_id': 'uicc',
|
||||
# 'dest_dev_id': 'terminal'}},
|
||||
# {'bearer_description': {'bearer_type': 'default',
|
||||
# 'bearer_parameters': ''}},
|
||||
# {'buffer_size': 1024},
|
||||
# {'uicc_transport_level': {'protocol_type': 'tcp_uicc_client_remote',
|
||||
# 'port_number': 32768}},
|
||||
# {'other_address': {'type_of_address': 'ipv4',
|
||||
# 'address': '01020304'}}
|
||||
# ]}
|
||||
logger.info("OpenChannel")
|
||||
logger.info(pcmd)
|
||||
transp_lvl_ie = Proact._find_first_element_of_type(pcmd.children, UiccTransportLevel)
|
||||
other_addr_ie = Proact._find_first_element_of_type(pcmd.children, OtherAddress)
|
||||
bearer_desc_ie = Proact._find_first_element_of_type(pcmd.children, BearerDescription)
|
||||
buffer_size_ie = Proact._find_first_element_of_type(pcmd.children, BufferSize)
|
||||
if transp_lvl_ie.decoded['protocol_type'] != 'tcp_uicc_client_remote':
|
||||
raise ValueError('Unsupported protocol_type')
|
||||
if other_addr_ie.decoded.get('type_of_address', None) != 'ipv4':
|
||||
raise ValueError('Unsupported type_of_address')
|
||||
ipv4_bytes = h2b(other_addr_ie.decoded['address'])
|
||||
ipv4_str = '%u.%u.%u.%u' % (ipv4_bytes[0], ipv4_bytes[1], ipv4_bytes[2], ipv4_bytes[3])
|
||||
port_nr = transp_lvl_ie.decoded['port_number']
|
||||
print("%s:%u" % (ipv4_str, port_nr))
|
||||
channel = self.channels.channel_create()
|
||||
channel.ep = endpoints.TCP4ClientEndpoint(reactor, ipv4_str, port_nr)
|
||||
channel.prot = TcpProtocol()
|
||||
d = endpoints.connectProtocol(channel.ep, channel.prot)
|
||||
# FIXME: why is this never called despite the client showing the inbound connection?
|
||||
d.addCallback(tcp_connected_callback)
|
||||
|
||||
# Terminal Response example: [
|
||||
# {'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'open_channel',
|
||||
# 'command_qualifier': 3}},
|
||||
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
||||
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
|
||||
# {'channel_status': '8100'},
|
||||
# {'bearer_description': {'bearer_type': 'default', 'bearer_parameters': ''}},
|
||||
# {'buffer_size': 1024}
|
||||
# ]
|
||||
return self.prepare_response(pcmd) + [ChannelStatus(decoded='8100'), bearer_desc_ie, buffer_size_ie]
|
||||
|
||||
def handle_CloseChannel(self, pcmd: ProactiveCommand):
|
||||
"""Close a channel."""
|
||||
logger.info("CloseChannel")
|
||||
logger.info(pcmd)
|
||||
|
||||
def handle_ReceiveData(self, pcmd: ProactiveCommand):
|
||||
"""Receive/read data from the socket."""
|
||||
# {'receive_data': [{'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'receive_data',
|
||||
# 'command_qualifier': 0}},
|
||||
# {'device_identities': {'source_dev_id': 'uicc',
|
||||
# 'dest_dev_id': 'channel_1'}},
|
||||
# {'channel_data_length': 9}
|
||||
# ]}
|
||||
logger.info("ReceiveData")
|
||||
logger.info(pcmd)
|
||||
# Terminal Response example: [
|
||||
# {'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'receive_data',
|
||||
# 'command_qualifier': 0}},
|
||||
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
||||
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
|
||||
# {'channel_data': '16030100040e000000'},
|
||||
# {'channel_data_length': 0}
|
||||
# ]
|
||||
return self.prepare_response(pcmd) + []
|
||||
|
||||
def handle_SendData(self, pcmd: ProactiveCommand):
|
||||
"""Send/write data received from the SIM to the socket."""
|
||||
# {'send_data': [{'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'send_data',
|
||||
# 'command_qualifier': 1}},
|
||||
# {'device_identities': {'source_dev_id': 'uicc',
|
||||
# 'dest_dev_id': 'channel_1'}},
|
||||
# {'channel_data': '160301003c010000380303d0f45e12b52ce5bb522750dd037738195334c87a46a847fe2b6886cada9ea6bf00000a00ae008c008b00b0002c010000050001000101'}
|
||||
# ]}
|
||||
logger.info("SendData")
|
||||
logger.info(pcmd)
|
||||
dev_id_ie = Proact._find_first_element_of_type(pcmd.children, DeviceIdentities)
|
||||
chan_data_ie = Proact._find_first_element_of_type(pcmd.children, ChannelData)
|
||||
chan_str = dev_id_ie.decoded['dest_dev_id']
|
||||
chan_nr = 1 # FIXME
|
||||
chan = self.channels.channels.get(chan_nr, None)
|
||||
# FIXME chan.prot.transport.write(h2b(chan_data_ie.decoded))
|
||||
# Terminal Response example: [
|
||||
# {'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'send_data',
|
||||
# 'command_qualifier': 1}},
|
||||
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
||||
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}},
|
||||
# {'channel_data_length': 255}
|
||||
# ]
|
||||
return self.prepare_response(pcmd) + [ChannelDataLength(decoded=255)]
|
||||
|
||||
def handle_SetUpEventList(self, pcmd: ProactiveCommand):
|
||||
# {'set_up_event_list': [{'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'set_up_event_list',
|
||||
# 'command_qualifier': 0}},
|
||||
# {'device_identities': {'source_dev_id': 'uicc',
|
||||
# 'dest_dev_id': 'terminal'}},
|
||||
# {'event_list': ['data_available', 'channel_status']}
|
||||
# ]}
|
||||
logger.info("SetUpEventList")
|
||||
logger.info(pcmd)
|
||||
# Terminal Response example: [
|
||||
# {'command_details': {'command_number': 1,
|
||||
# 'type_of_command': 'set_up_event_list',
|
||||
# 'command_qualifier': 0}},
|
||||
# {'device_identities': {'source_dev_id': 'terminal', 'dest_dev_id': 'uicc'}},
|
||||
# {'result': {'general_result': 'performed_successfully', 'additional_information': ''}}
|
||||
# ]
|
||||
return self.prepare_response(pcmd)
|
||||
|
||||
def getChannelStatus(self, pcmd: ProactiveCommand):
|
||||
logger.info("GetChannelStatus")
|
||||
logger.info(pcmd)
|
||||
return self.prepare_response(pcmd) + []
|
||||
|
||||
def send_sms_via_smpp(self, submit: SMS_SUBMIT):
|
||||
# while in a normal network the phone/ME would *submit* a message to the SMSC,
|
||||
# we are actually emulating the SMSC itself, so we must *deliver* the message
|
||||
# to the ESME
|
||||
deliver = SMS_DELIVER.from_submit(submit)
|
||||
deliver_smpp = deliver.to_smpp()
|
||||
|
||||
hackish_global_smpp.sendDataRequest(deliver_smpp)
|
||||
# # obtain the connection/binding of system_id to be used for delivering MO-SMS to the ESME
|
||||
# connection = smpp_server.getBoundConnections[system_id].getNextBindingForDelivery()
|
||||
# connection.sendDataRequest(deliver_smpp)
|
||||
|
||||
|
||||
|
||||
def dcs_is_8bit(dcs):
|
||||
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
|
||||
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED):
|
||||
return True
|
||||
if dcs == pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
|
||||
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED_COMMON):
|
||||
return True
|
||||
# pySim-smpp2sim.py:150:21: E1101: Instance of 'DataCodingScheme' has no 'GSM_MESSAGE_CLASS' member (no-member)
|
||||
# pylint: disable=no-member
|
||||
if dcs.scheme == pdu_types.DataCodingScheme.GSM_MESSAGE_CLASS and dcs.schemeData['msgCoding'] == pdu_types.DataCodingGsmMsgCoding.DATA_8BIT:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class MyServer:
|
||||
|
||||
@implementer(IRealm)
|
||||
class SmppRealm:
|
||||
def requestAvatar(self, avatarId, mind, *interfaces):
|
||||
return ('SMPP', avatarId, lambda: None)
|
||||
|
||||
def __init__(self, tcp_port:int = 2775, bind_ip = '::', system_id:str = 'test', password:str = 'test'):
|
||||
smpp_config = SMPPServerConfig(msgHandler=self._msgHandler,
|
||||
systems={system_id: {'max_bindings': 2}})
|
||||
portal = Portal(self.SmppRealm())
|
||||
credential_checker = InMemoryUsernamePasswordDatabaseDontUse()
|
||||
credential_checker.addUser(system_id, password)
|
||||
portal.registerChecker(credential_checker)
|
||||
self.factory = SMPPServerFactory(smpp_config, auth_portal=portal)
|
||||
logger.info('Binding Virtual SMSC to TCP Port %u at %s' % (tcp_port, bind_ip))
|
||||
smppEndpoint = endpoints.TCP6ServerEndpoint(reactor, tcp_port, interface=bind_ip)
|
||||
smppEndpoint.listen(self.factory)
|
||||
self.tp = self.scc = self.card = None
|
||||
|
||||
def connect_to_card(self, tp: LinkBase):
|
||||
self.tp = tp
|
||||
self.scc = SimCardCommands(self.tp)
|
||||
self.card = UiccCardBase(self.scc)
|
||||
# this should be part of UiccCardBase, but FairewavesSIM breaks with that :/
|
||||
self.scc.cla_byte = "00"
|
||||
self.scc.sel_ctrl = "0004"
|
||||
self.card.read_aids()
|
||||
self.card.select_adf_by_aid(adf='usim')
|
||||
# FIXME: create a more realistic profile than ffffff
|
||||
self.scc.terminal_profile('ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff')
|
||||
|
||||
def _msgHandler(self, system_id, smpp, pdu):
|
||||
"""Handler for incoming messages received via SMPP from ESME."""
|
||||
# HACK: we need some kind of mapping table between system_id and card-reader
|
||||
# or actually route based on MSISDNs
|
||||
global hackish_global_smpp
|
||||
hackish_global_smpp = smpp
|
||||
if pdu.id == pdu_types.CommandId.submit_sm:
|
||||
return self.handle_submit_sm(system_id, smpp, pdu)
|
||||
else:
|
||||
logger.warning('Rejecting non-SUBMIT commandID')
|
||||
return pdu_types.CommandStatus.ESME_RINVCMDID
|
||||
|
||||
def handle_submit_sm(self, system_id, smpp, pdu):
|
||||
"""SUBMIT-SM was received via SMPP from ESME. We need to deliver it to the SIM."""
|
||||
# check for valid data coding scheme + PID
|
||||
if not dcs_is_8bit(pdu.params['data_coding']):
|
||||
logger.warning('Rejecting non-8bit DCS')
|
||||
return pdu_types.CommandStatus.ESME_RINVDCS
|
||||
if pdu.params['protocol_id'] != 0x7f:
|
||||
logger.warning('Rejecting non-SIM PID')
|
||||
return pdu_types.CommandStatus.ESME_RINVDCS
|
||||
|
||||
# 1) build a SMS-DELIVER (!) from the SMPP-SUBMIT
|
||||
tpdu = SMS_DELIVER.from_smpp_submit(pdu)
|
||||
logger.info(tpdu)
|
||||
# 2) wrap into the CAT ENVELOPE for SMS-PP-Download
|
||||
tpdu_ie = SMS_TPDU(decoded={'tpdu': b2h(tpdu.to_bytes())})
|
||||
addr_ie = Address(decoded={'ton_npi': {'ext':False, 'type_of_number':'unknown', 'numbering_plan_id':'unknown'}, 'call_number': '0123456'})
|
||||
dev_ids = DeviceIdentities(decoded={'source_dev_id': 'network', 'dest_dev_id': 'uicc'})
|
||||
sms_dl = SMSPPDownload(children=[dev_ids, addr_ie, tpdu_ie])
|
||||
# 3) send to the card
|
||||
envelope_hex = b2h(sms_dl.to_tlv())
|
||||
logger.info("ENVELOPE: %s" % envelope_hex)
|
||||
(data, sw) = self.scc.envelope(envelope_hex)
|
||||
logger.info("SW %s: %s" % (sw, data))
|
||||
if sw in ['9200', '9300']:
|
||||
# TODO send back RP-ERROR message with TP-FCS == 'SIM Application Toolkit Busy'
|
||||
return pdu_types.CommandStatus.ESME_RSUBMITFAIL
|
||||
elif sw == '9000' or sw[0:2] in ['6f', '62', '63'] and len(data):
|
||||
# data something like 027100000e0ab000110000000000000001612f or
|
||||
# 027100001c12b000119660ebdb81be189b5e4389e9e7ab2bc0954f963ad869ed7c
|
||||
# which is the user-data portion of the SMS starting with the UDH (027100)
|
||||
# TODO: return the response back to the sender in an RP-ACK; PID/DCS like in CMD
|
||||
deliver = operations.DeliverSM(service_type=pdu.params['service_type'],
|
||||
source_addr_ton=pdu.params['dest_addr_ton'],
|
||||
source_addr_npi=pdu.params['dest_addr_npi'],
|
||||
source_addr=pdu.params['destination_addr'],
|
||||
dest_addr_ton=pdu.params['source_addr_ton'],
|
||||
dest_addr_npi=pdu.params['source_addr_npi'],
|
||||
destination_addr=pdu.params['source_addr'],
|
||||
esm_class=pdu.params['esm_class'],
|
||||
protocol_id=pdu.params['protocol_id'],
|
||||
priority_flag=pdu.params['priority_flag'],
|
||||
data_coding=pdu.params['data_coding'],
|
||||
short_message=h2b(data))
|
||||
smpp.sendDataRequest(deliver)
|
||||
return pdu_types.CommandStatus.ESME_ROK
|
||||
else:
|
||||
return pdu_types.CommandStatus.ESME_RSUBMITFAIL
|
||||
|
||||
|
||||
option_parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
||||
argparse_add_reader_args(option_parser)
|
||||
smpp_group = option_parser.add_argument_group('SMPP Options')
|
||||
smpp_group.add_argument('--smpp-bind-port', type=int, default=2775,
|
||||
help='TCP Port to bind the SMPP socket to')
|
||||
smpp_group.add_argument('--smpp-bind-ip', default='::',
|
||||
help='IPv4/IPv6 address to bind the SMPP socket to')
|
||||
smpp_group.add_argument('--smpp-system-id', default='test',
|
||||
help='SMPP System-ID used by ESME to bind')
|
||||
smpp_group.add_argument('--smpp-password', default='test',
|
||||
help='SMPP Password used by ESME to bind')
|
||||
|
||||
if __name__ == '__main__':
|
||||
log_format='%(log_color)s%(levelname)-8s%(reset)s %(name)s: %(message)s'
|
||||
colorlog.basicConfig(level=logging.INFO, format = log_format)
|
||||
logger = colorlog.getLogger()
|
||||
|
||||
opts = option_parser.parse_args()
|
||||
|
||||
tp = init_reader(opts, proactive_handler = Proact())
|
||||
if tp is None:
|
||||
exit(1)
|
||||
tp.connect()
|
||||
|
||||
global g_ms
|
||||
g_ms = MyServer(opts.smpp_bind_port, opts.smpp_bind_ip, opts.smpp_system_id, opts.smpp_password)
|
||||
g_ms.connect_to_card(tp)
|
||||
reactor.run()
|
||||
|
||||
@@ -13,7 +13,7 @@ from osmocom.utils import JsonEncoder
|
||||
from pySim.cards import UiccCardBase
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.profile import CardProfile
|
||||
from pySim.ts_102_221 import CardProfileUICC
|
||||
from pySim.profile.ts_102_221 import CardProfileUICC
|
||||
from pySim.ts_31_102 import CardApplicationUSIM
|
||||
from pySim.ts_31_103 import CardApplicationISIM
|
||||
from pySim.euicc import CardApplicationISDR, CardApplicationECASD
|
||||
|
||||
@@ -29,7 +29,7 @@ import abc
|
||||
import typing
|
||||
from typing import List, Dict, Optional
|
||||
from termcolor import colored
|
||||
from construct import Byte
|
||||
from construct import Byte, GreedyBytes
|
||||
from construct import Optional as COptional
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import *
|
||||
|
||||
@@ -9,7 +9,7 @@ APDU commands of 3GPP TS 31.102 V16.6.0
|
||||
"""
|
||||
from typing import Dict
|
||||
|
||||
from construct import BitStruct, Enum, BitsInteger, Int8ub, this, Struct, If, Switch, Const
|
||||
from construct import BitStruct, Enum, BitsInteger, Int8ub, Bytes, this, Struct, If, Switch, Const
|
||||
from construct import Optional as COptional
|
||||
from osmocom.construct import *
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ from pySim.filesystem import CardModel, CardApplication
|
||||
from pySim.cards import card_detect, SimCardBase, UiccCardBase, CardBase
|
||||
from pySim.runtime import RuntimeState
|
||||
from pySim.profile import CardProfile
|
||||
from pySim.cdma_ruim import CardProfileRUIM
|
||||
from pySim.ts_102_221 import CardProfileUICC
|
||||
from pySim.profile.cdma_ruim import CardProfileRUIM
|
||||
from pySim.profile.ts_102_221 import CardProfileUICC
|
||||
from pySim.utils import all_subclasses
|
||||
from pySim.exceptions import SwMatchError
|
||||
|
||||
@@ -40,7 +40,7 @@ import pySim.ts_31_103
|
||||
import pySim.ts_31_104
|
||||
import pySim.ara_m
|
||||
import pySim.global_platform
|
||||
import pySim.euicc
|
||||
import pySim.profile.euicc
|
||||
|
||||
def init_card(sl: LinkBase, skip_card_init: bool = False) -> Tuple[RuntimeState, SimCardBase]:
|
||||
"""
|
||||
|
||||
@@ -26,7 +26,7 @@ Support for the Secure Element Access Control, specifically the ARA-M inside an
|
||||
#
|
||||
|
||||
|
||||
from construct import GreedyString, Struct, Enum, Int8ub, Int16ub
|
||||
from construct import GreedyBytes, GreedyString, Struct, Enum, Int8ub, Int16ub
|
||||
from construct import Optional as COptional
|
||||
from osmocom.construct import *
|
||||
from osmocom.tlv import *
|
||||
@@ -38,17 +38,17 @@ import pySim.global_platform
|
||||
|
||||
|
||||
class AidRefDO(BER_TLV_IE, tag=0x4f):
|
||||
# GPD_SPE_013 v1.1 Table 6-3
|
||||
# SEID v1.1 Table 6-3
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
|
||||
class AidRefEmptyDO(BER_TLV_IE, tag=0xc0):
|
||||
# GPD_SPE_013 v1.1 Table 6-3
|
||||
# SEID v1.1 Table 6-3
|
||||
pass
|
||||
|
||||
|
||||
class DevAppIdRefDO(BER_TLV_IE, tag=0xc1):
|
||||
# GPD_SPE_013 v1.1 Table 6-4
|
||||
# SEID v1.1 Table 6-4
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
|
||||
@@ -58,12 +58,12 @@ class PkgRefDO(BER_TLV_IE, tag=0xca):
|
||||
|
||||
|
||||
class RefDO(BER_TLV_IE, tag=0xe1, nested=[AidRefDO, AidRefEmptyDO, DevAppIdRefDO, PkgRefDO]):
|
||||
# GPD_SPE_013 v1.1 Table 6-5
|
||||
# SEID v1.1 Table 6-5
|
||||
pass
|
||||
|
||||
|
||||
class ApduArDO(BER_TLV_IE, tag=0xd0):
|
||||
# GPD_SPE_013 v1.1 Table 6-8
|
||||
# SEID v1.1 Table 6-8
|
||||
def _from_bytes(self, do: bytes):
|
||||
if len(do) == 1:
|
||||
if do[0] == 0x00:
|
||||
@@ -108,7 +108,7 @@ class ApduArDO(BER_TLV_IE, tag=0xd0):
|
||||
|
||||
|
||||
class NfcArDO(BER_TLV_IE, tag=0xd1):
|
||||
# GPD_SPE_013 v1.1 Table 6-9
|
||||
# SEID v1.1 Table 6-9
|
||||
_construct = Struct('nfc_event_access_rule' /
|
||||
Enum(Int8ub, never=0, always=1))
|
||||
|
||||
@@ -120,122 +120,122 @@ class PermArDO(BER_TLV_IE, tag=0xdb):
|
||||
|
||||
|
||||
class ArDO(BER_TLV_IE, tag=0xe3, nested=[ApduArDO, NfcArDO, PermArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 6-7
|
||||
# SEID v1.1 Table 6-7
|
||||
pass
|
||||
|
||||
|
||||
class RefArDO(BER_TLV_IE, tag=0xe2, nested=[RefDO, ArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 6-6
|
||||
# SEID v1.1 Table 6-6
|
||||
pass
|
||||
|
||||
|
||||
class ResponseAllRefArDO(BER_TLV_IE, tag=0xff40, nested=[RefArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 4-2
|
||||
# SEID v1.1 Table 4-2
|
||||
pass
|
||||
|
||||
|
||||
class ResponseArDO(BER_TLV_IE, tag=0xff50, nested=[ArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 4-3
|
||||
# SEID v1.1 Table 4-3
|
||||
pass
|
||||
|
||||
|
||||
class ResponseRefreshTagDO(BER_TLV_IE, tag=0xdf20):
|
||||
# GPD_SPE_013 v1.1 Table 4-4
|
||||
# SEID v1.1 Table 4-4
|
||||
_construct = Struct('refresh_tag'/HexAdapter(Bytes(8)))
|
||||
|
||||
|
||||
class DeviceInterfaceVersionDO(BER_TLV_IE, tag=0xe6):
|
||||
# GPD_SPE_013 v1.1 Table 6-12
|
||||
# SEID v1.1 Table 6-12
|
||||
_construct = Struct('major'/Int8ub, 'minor'/Int8ub, 'patch'/Int8ub)
|
||||
|
||||
|
||||
class DeviceConfigDO(BER_TLV_IE, tag=0xe4, nested=[DeviceInterfaceVersionDO]):
|
||||
# GPD_SPE_013 v1.1 Table 6-10
|
||||
# SEID v1.1 Table 6-10
|
||||
pass
|
||||
|
||||
|
||||
class ResponseDeviceConfigDO(BER_TLV_IE, tag=0xff7f, nested=[DeviceConfigDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-14
|
||||
# SEID v1.1 Table 5-14
|
||||
pass
|
||||
|
||||
|
||||
class AramConfigDO(BER_TLV_IE, tag=0xe5, nested=[DeviceInterfaceVersionDO]):
|
||||
# GPD_SPE_013 v1.1 Table 6-11
|
||||
# SEID v1.1 Table 6-11
|
||||
pass
|
||||
|
||||
|
||||
class ResponseAramConfigDO(BER_TLV_IE, tag=0xdf21, nested=[AramConfigDO]):
|
||||
# GPD_SPE_013 v1.1 Table 4-5
|
||||
# SEID v1.1 Table 4-5
|
||||
pass
|
||||
|
||||
|
||||
class CommandStoreRefArDO(BER_TLV_IE, tag=0xf0, nested=[RefArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-2
|
||||
# SEID v1.1 Table 5-2
|
||||
pass
|
||||
|
||||
|
||||
class CommandDelete(BER_TLV_IE, tag=0xf1, nested=[AidRefDO, AidRefEmptyDO, RefDO, RefArDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-4
|
||||
# SEID v1.1 Table 5-4
|
||||
pass
|
||||
|
||||
|
||||
class CommandUpdateRefreshTagDO(BER_TLV_IE, tag=0xf2):
|
||||
# GPD_SPE_013 V1.1 Table 5-6
|
||||
# SEID V1.1 Table 5-6
|
||||
pass
|
||||
|
||||
|
||||
class CommandRegisterClientAidsDO(BER_TLV_IE, tag=0xf7, nested=[AidRefDO, AidRefEmptyDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-7
|
||||
# SEID v1.1 Table 5-7
|
||||
pass
|
||||
|
||||
|
||||
class CommandGet(BER_TLV_IE, tag=0xf3, nested=[AidRefDO, AidRefEmptyDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-8
|
||||
# SEID v1.1 Table 5-8
|
||||
pass
|
||||
|
||||
|
||||
class CommandGetAll(BER_TLV_IE, tag=0xf4):
|
||||
# GPD_SPE_013 v1.1 Table 5-9
|
||||
# SEID v1.1 Table 5-9
|
||||
pass
|
||||
|
||||
|
||||
class CommandGetClientAidsDO(BER_TLV_IE, tag=0xf6):
|
||||
# GPD_SPE_013 v1.1 Table 5-10
|
||||
# SEID v1.1 Table 5-10
|
||||
pass
|
||||
|
||||
|
||||
class CommandGetNext(BER_TLV_IE, tag=0xf5):
|
||||
# GPD_SPE_013 v1.1 Table 5-11
|
||||
# SEID v1.1 Table 5-11
|
||||
pass
|
||||
|
||||
|
||||
class CommandGetDeviceConfigDO(BER_TLV_IE, tag=0xf8):
|
||||
# GPD_SPE_013 v1.1 Table 5-12
|
||||
# SEID v1.1 Table 5-12
|
||||
pass
|
||||
|
||||
|
||||
class ResponseAracAidDO(BER_TLV_IE, tag=0xff70, nested=[AidRefDO, AidRefEmptyDO]):
|
||||
# GPD_SPE_013 v1.1 Table 5-13
|
||||
# SEID v1.1 Table 5-13
|
||||
pass
|
||||
|
||||
|
||||
class BlockDO(BER_TLV_IE, tag=0xe7):
|
||||
# GPD_SPE_013 v1.1 Table 6-13
|
||||
# SEID v1.1 Table 6-13
|
||||
_construct = Struct('offset'/Int16ub, 'length'/Int8ub)
|
||||
|
||||
|
||||
# GPD_SPE_013 v1.1 Table 4-1
|
||||
# SEID v1.1 Table 4-1
|
||||
class GetCommandDoCollection(TLV_IE_Collection, nested=[RefDO, DeviceConfigDO]):
|
||||
pass
|
||||
|
||||
|
||||
# GPD_SPE_013 v1.1 Table 4-2
|
||||
# SEID v1.1 Table 4-2
|
||||
class GetResponseDoCollection(TLV_IE_Collection, nested=[ResponseAllRefArDO, ResponseArDO,
|
||||
ResponseRefreshTagDO, ResponseAramConfigDO]):
|
||||
pass
|
||||
|
||||
|
||||
# GPD_SPE_013 v1.1 Table 5-1
|
||||
# SEID v1.1 Table 5-1
|
||||
class StoreCommandDoCollection(TLV_IE_Collection,
|
||||
nested=[BlockDO, CommandStoreRefArDO, CommandDelete,
|
||||
CommandUpdateRefreshTagDO, CommandRegisterClientAidsDO,
|
||||
@@ -244,7 +244,7 @@ class StoreCommandDoCollection(TLV_IE_Collection,
|
||||
pass
|
||||
|
||||
|
||||
# GPD_SPE_013 v1.1 Section 5.1.2
|
||||
# SEID v1.1 Section 5.1.2
|
||||
class StoreResponseDoCollection(TLV_IE_Collection,
|
||||
nested=[ResponseAllRefArDO, ResponseAracAidDO, ResponseDeviceConfigDO]):
|
||||
pass
|
||||
@@ -320,9 +320,9 @@ class ADF_ARAM(CardADF):
|
||||
'--device-app-id', required=True, help='Identifies the specific device application that the rule appplies to. Hash of Certificate of Application Provider, or UUID. (20/32 hex bytes)')
|
||||
aid_grp = store_ref_ar_do_parse.add_mutually_exclusive_group()
|
||||
aid_grp.add_argument(
|
||||
'--aid', help='Identifies the specific SE application for which rules are to be stored. Can be a partial AID, containing for example only the RID. (5-16 or 0 hex bytes)')
|
||||
'--aid', help='Identifies the specific SE application for which rules are to be stored. Can be a partial AID, containing for example only the RID. (5-16 hex bytes)')
|
||||
aid_grp.add_argument('--aid-empty', action='store_true',
|
||||
help='No specific SE application, applies to implicitly selected application (all channels)')
|
||||
help='No specific SE application, applies to all applications')
|
||||
store_ref_ar_do_parse.add_argument(
|
||||
'--pkg-ref', help='Full Android Java package name (up to 127 chars ASCII)')
|
||||
# AR-DO
|
||||
@@ -389,11 +389,6 @@ class ADF_ARAM(CardADF):
|
||||
if res_do:
|
||||
self._cmd.poutput_json(res_do.to_dict())
|
||||
|
||||
def do_aram_lock(self, opts):
|
||||
"""Lock STORE DATA command to prevent unauthorized changes
|
||||
(Proprietary feature that is specific to sysmocom's fork of Bertrand Martel’s ARA-M implementation.)"""
|
||||
self._cmd.lchan.scc.send_apdu_checksw('80e2900001A1', '9000')
|
||||
|
||||
|
||||
# SEAC v1.1 Section 4.1.2.2 + 5.1.2.2
|
||||
sw_aram = {
|
||||
@@ -428,13 +423,10 @@ class CardApplicationARAM(CardApplication):
|
||||
# matching key.
|
||||
if dictlist is None:
|
||||
return None
|
||||
obj = None
|
||||
for d in dictlist:
|
||||
if key in d:
|
||||
obj = d.get(key)
|
||||
if obj is None:
|
||||
return ""
|
||||
return obj
|
||||
return None
|
||||
obj = d.get(key, obj)
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def __export_ref_ar_do_list(ref_ar_do_list):
|
||||
@@ -445,7 +437,6 @@ class CardApplicationARAM(CardApplication):
|
||||
if ref_do_list and ar_do_list:
|
||||
# Get ref_do parameters
|
||||
aid_ref_do = CardApplicationARAM.__export_get_from_dictlist('aid_ref_do', ref_do_list)
|
||||
aid_ref_empty_do = CardApplicationARAM.__export_get_from_dictlist('aid_ref_empty_do', ref_do_list)
|
||||
dev_app_id_ref_do = CardApplicationARAM.__export_get_from_dictlist('dev_app_id_ref_do', ref_do_list)
|
||||
pkg_ref_do = CardApplicationARAM.__export_get_from_dictlist('pkg_ref_do', ref_do_list)
|
||||
|
||||
@@ -456,11 +447,9 @@ class CardApplicationARAM(CardApplication):
|
||||
|
||||
# Write command-line
|
||||
export_str += "aram_store_ref_ar_do"
|
||||
if aid_ref_do is not None and len(aid_ref_do) > 0:
|
||||
if aid_ref_do:
|
||||
export_str += (" --aid %s" % aid_ref_do)
|
||||
elif aid_ref_do is not None:
|
||||
export_str += " --aid \"\""
|
||||
if aid_ref_empty_do is not None:
|
||||
else:
|
||||
export_str += " --aid-empty"
|
||||
if dev_app_id_ref_do:
|
||||
export_str += (" --device-app-id %s" % dev_app_id_ref_do)
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
from typing import Optional, Tuple
|
||||
from osmocom.utils import *
|
||||
|
||||
from pySim.ts_102_221 import EF_DIR, CardProfileUICC
|
||||
from pySim.ts_51_011 import DF_GSM
|
||||
from pySim.profile.ts_102_221 import EF_DIR, CardProfileUICC
|
||||
from pySim.profile.ts_51_011 import DF_GSM
|
||||
from pySim.utils import SwHexstr
|
||||
from pySim.commands import Path, SimCardCommands
|
||||
|
||||
@@ -73,16 +73,6 @@ class CardBase:
|
||||
# callers having to do hasattr('read_aids') ahead of every call.
|
||||
return []
|
||||
|
||||
def adf_present(self, adf: str = "usim") -> bool:
|
||||
# a non-UICC doesn't have any applications. Convenience helper to avoid
|
||||
# callers having to do hasattr('adf_present') ahead of every call.
|
||||
return False
|
||||
|
||||
def select_adf_by_aid(self, adf: str = "usim", scc: Optional[SimCardCommands] = None) -> Tuple[Optional[Hexstr], Optional[SwHexstr]]:
|
||||
# a non-UICC doesn't have any applications. Convenience helper to avoid
|
||||
# callers having to do hasattr('select_adf_by_aid') ahead of every call.
|
||||
return (None, None)
|
||||
|
||||
|
||||
class SimCardBase(CardBase):
|
||||
"""Here we only add methods for commands specified in TS 51.011, without
|
||||
|
||||
78
pySim/cat.py
78
pySim/cat.py
@@ -20,19 +20,19 @@ as described in 3GPP TS 31.111."""
|
||||
|
||||
from typing import List
|
||||
from bidict import bidict
|
||||
from construct import Int8ub, Int16ub, Byte, BitsInteger
|
||||
from construct import Int8ub, Int16ub, Byte, Bytes, BitsInteger
|
||||
from construct import Struct, Enum, BitStruct, this
|
||||
from construct import Switch, GreedyRange, FlagsEnum
|
||||
from construct import GreedyBytes, Switch, GreedyRange, FlagsEnum
|
||||
from osmocom.tlv import TLV_IE, COMPR_TLV_IE, BER_TLV_IE, TLV_IE_Collection
|
||||
from osmocom.construct import PlmnAdapter, BcdAdapter, GsmStringAdapter, TonNpi, GsmString, Bytes, GreedyBytes
|
||||
from osmocom.utils import b2h, h2b
|
||||
from osmocom.construct import PlmnAdapter, BcdAdapter, HexAdapter, GsmStringAdapter, TonNpi, GsmString
|
||||
from osmocom.utils import b2h
|
||||
from pySim.utils import dec_xplmn_w_act
|
||||
|
||||
# Tag values as per TS 101 220 Table 7.23
|
||||
|
||||
# TS 102 223 Section 8.1
|
||||
class Address(COMPR_TLV_IE, tag=0x86):
|
||||
_construct = Struct('ton_npi'/TonNpi,
|
||||
_construct = Struct('ton_npi'/Int8ub,
|
||||
'call_number'/BcdAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.2
|
||||
@@ -255,24 +255,24 @@ class Result(COMPR_TLV_IE, tag=0x83):
|
||||
'launch_browser_generic_error': AddlInfoLaunchBrowser,
|
||||
'bearer_independent_protocol_error': AddlInfoBip,
|
||||
'frames_error': AddlInfoFrames
|
||||
}, default=GreedyBytes))
|
||||
}, default=HexAdapter(GreedyBytes)))
|
||||
|
||||
# TS 102 223 Section 8.13 + TS 31.111 Section 8.13
|
||||
class SMS_TPDU(COMPR_TLV_IE, tag=0x8B):
|
||||
_construct = Struct('tpdu'/GreedyBytes)
|
||||
_construct = Struct('tpdu'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 31.111 Section 8.14
|
||||
class SsString(COMPR_TLV_IE, tag=0x89):
|
||||
_construct = Struct('ton_npi'/TonNpi, 'ss_string'/GreedyBytes)
|
||||
_construct = Struct('ton_npi'/TonNpi, 'ss_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
|
||||
# TS 102 223 Section 8.15
|
||||
class TextString(COMPR_TLV_IE, tag=0x8D):
|
||||
_test_de_encode = [
|
||||
( '8d090470617373776f7264', {'dcs': 4, 'text_string': b'password'} )
|
||||
( '8d090470617373776f7264', {'dcs': 4, 'text_string': '70617373776f7264'} ),
|
||||
]
|
||||
_construct = Struct('dcs'/Int8ub, # TS 03.38
|
||||
'text_string'/GreedyBytes)
|
||||
'text_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.16
|
||||
class Tone(COMPR_TLV_IE, tag=0x8E):
|
||||
@@ -308,11 +308,11 @@ class Tone(COMPR_TLV_IE, tag=0x8E):
|
||||
# TS 31 111 Section 8.17
|
||||
class USSDString(COMPR_TLV_IE, tag=0x8A):
|
||||
_construct = Struct('dcs'/Int8ub,
|
||||
'ussd_string'/GreedyBytes)
|
||||
'ussd_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.18
|
||||
class FileList(COMPR_TLV_IE, tag=0x92):
|
||||
FileId=Bytes(2)
|
||||
FileId=HexAdapter(Bytes(2))
|
||||
_construct = Struct('number_of_files'/Int8ub,
|
||||
'files'/GreedyRange(FileId))
|
||||
|
||||
@@ -335,7 +335,7 @@ class NetworkMeasurementResults(COMPR_TLV_IE, tag=0x96):
|
||||
# TS 102 223 Section 8.23
|
||||
class DefaultText(COMPR_TLV_IE, tag=0x97):
|
||||
_construct = Struct('dcs'/Int8ub,
|
||||
'text_string'/GreedyBytes)
|
||||
'text_string'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.24
|
||||
class ItemsNextActionIndicator(COMPR_TLV_IE, tag=0x98):
|
||||
@@ -394,7 +394,7 @@ class ItemIconIdentifierList(COMPR_TLV_IE, tag=0x9f):
|
||||
|
||||
# TS 102 223 Section 8.35
|
||||
class CApdu(COMPR_TLV_IE, tag=0xA2):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.37
|
||||
class TimerIdentifier(COMPR_TLV_IE, tag=0xA4):
|
||||
@@ -406,7 +406,7 @@ class TimerValue(COMPR_TLV_IE, tag=0xA5):
|
||||
|
||||
# TS 102 223 Section 8.40
|
||||
class AtCommand(COMPR_TLV_IE, tag=0xA8):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.43
|
||||
class ImmediateResponse(COMPR_TLV_IE, tag=0xAB):
|
||||
@@ -418,7 +418,7 @@ class DtmfString(COMPR_TLV_IE, tag=0xAC):
|
||||
|
||||
# TS 102 223 Section 8.45
|
||||
class Language(COMPR_TLV_IE, tag=0xAD):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 31.111 Section 8.46
|
||||
class TimingAdvance(COMPR_TLV_IE, tag=0xC6):
|
||||
@@ -440,7 +440,7 @@ class Bearer(COMPR_TLV_IE, tag=0xB2):
|
||||
|
||||
# TS 102 223 Section 8.50
|
||||
class ProvisioningFileReference(COMPR_TLV_IE, tag=0xB3):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.51
|
||||
class BrowserTerminationCause(COMPR_TLV_IE, tag=0xB4):
|
||||
@@ -449,7 +449,7 @@ class BrowserTerminationCause(COMPR_TLV_IE, tag=0xB4):
|
||||
# TS 102 223 Section 8.52
|
||||
class BearerDescription(COMPR_TLV_IE, tag=0xB5):
|
||||
_test_de_encode = [
|
||||
( 'b50103', {'bearer_parameters': b'', 'bearer_type': 'default'} ),
|
||||
( 'b50103', {'bearer_parameters': '', 'bearer_type': 'default'} ),
|
||||
]
|
||||
# TS 31.111 Section 8.52.1
|
||||
BearerParsCs = Struct('data_rate'/Int8ub,
|
||||
@@ -492,11 +492,11 @@ class BearerDescription(COMPR_TLV_IE, tag=0xB5):
|
||||
'packet_grps_utran_eutran': BearerParsPacket,
|
||||
'packet_with_extd_params': BearerParsPacketExt,
|
||||
'ng_ran': BearerParsNgRan,
|
||||
}, default=GreedyBytes))
|
||||
}, default=HexAdapter(GreedyBytes)))
|
||||
|
||||
# TS 102 223 Section 8.53
|
||||
class ChannelData(COMPR_TLV_IE, tag = 0xB6):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.54
|
||||
class ChannelDataLength(COMPR_TLV_IE, tag = 0xB7):
|
||||
@@ -510,15 +510,15 @@ class BufferSize(COMPR_TLV_IE, tag = 0xB9):
|
||||
class ChannelStatus(COMPR_TLV_IE, tag = 0xB8):
|
||||
# complex decoding, depends on out-of-band context/knowledge :(
|
||||
# for default / TCP Client mode: bit 8 of first byte indicates connected, 3 LSB indicate channel nr
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.58
|
||||
class OtherAddress(COMPR_TLV_IE, tag = 0xBE):
|
||||
_test_de_encode = [
|
||||
( 'be052101020304', {'address': h2b('01020304'), 'type_of_address': 'ipv4'} ),
|
||||
( 'be052101020304', {'address': '01020304', 'type_of_address': 'ipv4'} ),
|
||||
]
|
||||
_construct = Struct('type_of_address'/Enum(Int8ub, ipv4=0x21, ipv6=0x57),
|
||||
'address'/GreedyBytes)
|
||||
'address'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.59
|
||||
class UiccTransportLevel(COMPR_TLV_IE, tag = 0xBC):
|
||||
@@ -532,7 +532,7 @@ class UiccTransportLevel(COMPR_TLV_IE, tag = 0xBC):
|
||||
|
||||
# TS 102 223 Section 8.60
|
||||
class Aid(COMPR_TLV_IE, tag=0xAF):
|
||||
_construct = Struct('aid'/GreedyBytes)
|
||||
_construct = Struct('aid'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.61
|
||||
class AccessTechnology(COMPR_TLV_IE, tag=0xBF):
|
||||
@@ -546,35 +546,35 @@ class ServiceRecord(COMPR_TLV_IE, tag=0xC1):
|
||||
BearerTechId = Enum(Int8ub, technology_independent=0, bluetooth=1, irda=2, rs232=3, usb=4)
|
||||
_construct = Struct('local_bearer_technology'/BearerTechId,
|
||||
'service_identifier'/Int8ub,
|
||||
'service_record'/GreedyBytes)
|
||||
'service_record'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.64
|
||||
class DeviceFilter(COMPR_TLV_IE, tag=0xC2):
|
||||
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
||||
'device_filter'/GreedyBytes)
|
||||
'device_filter'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.65
|
||||
class ServiceSearchIE(COMPR_TLV_IE, tag=0xC3):
|
||||
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
||||
'service_search'/GreedyBytes)
|
||||
'service_search'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.66
|
||||
class AttributeInformation(COMPR_TLV_IE, tag=0xC4):
|
||||
_construct = Struct('local_bearer_technology'/ServiceRecord.BearerTechId,
|
||||
'attribute_information'/GreedyBytes)
|
||||
'attribute_information'/HexAdapter(GreedyBytes))
|
||||
|
||||
|
||||
# TS 102 223 Section 8.68
|
||||
class RemoteEntityAddress(COMPR_TLV_IE, tag=0xC9):
|
||||
_construct = Struct('coding_type'/Enum(Int8ub, ieee802_16=0, irda=1),
|
||||
'address'/GreedyBytes)
|
||||
'address'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 102 223 Section 8.70
|
||||
class NetworkAccessName(COMPR_TLV_IE, tag=0xC7):
|
||||
_test_de_encode = [
|
||||
( 'c704036e6161', h2b('036e6161') ),
|
||||
( 'c704036e6161', '036e6161' ),
|
||||
]
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.72
|
||||
class TextAttribute(COMPR_TLV_IE, tag=0xD0):
|
||||
@@ -618,15 +618,15 @@ class FrameIdentifier(COMPR_TLV_IE, tag=0xE8):
|
||||
|
||||
# TS 102 223 Section 8.82
|
||||
class MultimediaMessageReference(COMPR_TLV_IE, tag=0xEA):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.83
|
||||
class MultimediaMessageIdentifier(COMPR_TLV_IE, tag=0xEB):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.85
|
||||
class MmContentIdentifier(COMPR_TLV_IE, tag=0xEE):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.89
|
||||
class ActivateDescriptor(COMPR_TLV_IE, tag=0xFB):
|
||||
@@ -649,7 +649,7 @@ class ContactlessFunctionalityState(COMPR_TLV_IE, tag=0xD4):
|
||||
# TS 31.111 Section 8.91
|
||||
class RoutingAreaIdentification(COMPR_TLV_IE, tag=0xF3):
|
||||
_construct = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)),
|
||||
'lac'/Bytes(2),
|
||||
'lac'/HexAdapter(Bytes(2)),
|
||||
'rac'/Int8ub)
|
||||
|
||||
# TS 31.111 Section 8.92
|
||||
@@ -709,15 +709,15 @@ class EcatSequenceNumber(COMPR_TLV_IE, tag=0xA1):
|
||||
|
||||
# TS 102 223 Section 8.99
|
||||
class EncryptedTlvList(COMPR_TLV_IE, tag=0xA2):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.100
|
||||
class Mac(COMPR_TLV_IE, tag=0xE0):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.101
|
||||
class SaTemplate(COMPR_TLV_IE, tag=0xA3):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.103
|
||||
class RefreshEnforcementPolicy(COMPR_TLV_IE, tag=0xBA):
|
||||
@@ -725,7 +725,7 @@ class RefreshEnforcementPolicy(COMPR_TLV_IE, tag=0xBA):
|
||||
|
||||
# TS 102 223 Section 8.104
|
||||
class DnsServerAddress(COMPR_TLV_IE, tag=0xC0):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 102 223 Section 8.105
|
||||
class SupportedRadioAccessTechnologies(COMPR_TLV_IE, tag=0xB4):
|
||||
|
||||
@@ -32,6 +32,7 @@ from osmocom.tlv import bertlv_encode_len
|
||||
from pySim.utils import sw_match, expand_hex, SwHexstr, ResTuple, SwMatchstr
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.ts_102_221 import decode_select_response
|
||||
|
||||
# A path can be either just a FID or a list of FID
|
||||
Path = typing.Union[Hexstr, List[Hexstr]]
|
||||
@@ -176,40 +177,6 @@ class SimCardCommands:
|
||||
raise SwMatchError(sw, sw_exp.lower(), self._tp.sw_interpreter)
|
||||
return (rsp, sw)
|
||||
|
||||
# Extract a single FCP item from TLV
|
||||
def __parse_fcp(self, fcp: Hexstr):
|
||||
# see also: ETSI TS 102 221, chapter 11.1.1.3.1 Response for MF,
|
||||
# DF or ADF
|
||||
from pytlv.TLV import TLV
|
||||
tlvparser = TLV(['82', '83', '84', 'a5', '8a', '8b',
|
||||
'8c', '80', 'ab', 'c6', '81', '88'])
|
||||
|
||||
# pytlv is case sensitive!
|
||||
fcp = fcp.lower()
|
||||
|
||||
if fcp[0:2] != '62':
|
||||
raise ValueError(
|
||||
'Tag of the FCP template does not match, expected 62 but got %s' % fcp[0:2])
|
||||
|
||||
# Unfortunately the spec is not very clear if the FCP length is
|
||||
# coded as one or two byte vale, so we have to try it out by
|
||||
# checking if the length of the remaining TLV string matches
|
||||
# what we get in the length field.
|
||||
# See also ETSI TS 102 221, chapter 11.1.1.3.0 Base coding.
|
||||
# TODO: this likely just is normal BER-TLV ("All data objects are BER-TLV except if otherwise # defined.")
|
||||
exp_tlv_len = int(fcp[2:4], 16)
|
||||
if len(fcp[4:]) // 2 == exp_tlv_len:
|
||||
skip = 4
|
||||
else:
|
||||
exp_tlv_len = int(fcp[2:6], 16)
|
||||
if len(fcp[4:]) // 2 == exp_tlv_len:
|
||||
skip = 6
|
||||
raise ValueError('Cannot determine length of TLV-length')
|
||||
|
||||
# Skip FCP tag and length
|
||||
tlv = fcp[skip:]
|
||||
return tlvparser.parse(tlv)
|
||||
|
||||
# Tell the length of a record by the card response
|
||||
# USIMs respond with an FCP template, which is different
|
||||
# from what SIMs responds. See also:
|
||||
@@ -217,10 +184,8 @@ class SimCardCommands:
|
||||
# SIM: GSM 11.11, chapter 9.2.1 SELECT
|
||||
def __record_len(self, r) -> int:
|
||||
if self.sel_ctrl == "0004":
|
||||
tlv_parsed = self.__parse_fcp(r[-1])
|
||||
file_descriptor = tlv_parsed['82']
|
||||
# See also ETSI TS 102 221, chapter 11.1.1.4.3 File Descriptor
|
||||
return int(file_descriptor[4:8], 16)
|
||||
fcp_parsed = decode_select_response(r[-1])
|
||||
return fcp_parsed['file_descriptor']['record_len']
|
||||
else:
|
||||
return int(r[-1][28:30], 16)
|
||||
|
||||
@@ -228,8 +193,8 @@ class SimCardCommands:
|
||||
# above.
|
||||
def __len(self, r) -> int:
|
||||
if self.sel_ctrl == "0004":
|
||||
tlv_parsed = self.__parse_fcp(r[-1])
|
||||
return int(tlv_parsed['80'], 16)
|
||||
fcp_parsed = decode_select_response(r[-1])
|
||||
return fcp_parsed['file_size']
|
||||
else:
|
||||
return int(r[-1][4:8], 16)
|
||||
|
||||
@@ -735,7 +700,7 @@ class SimCardCommands:
|
||||
Args:
|
||||
payload : payload as hex string
|
||||
"""
|
||||
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload) + "00", apply_lchan = False)
|
||||
return self.send_apdu_checksw('80c20000%02x%s' % (len(payload)//2, payload), apply_lchan = False)
|
||||
|
||||
def terminal_profile(self, payload: Hexstr) -> ResTuple:
|
||||
"""Send TERMINAL PROFILE to card
|
||||
|
||||
@@ -61,8 +61,8 @@ def compile_asn1_subdir(subdir_name:str, codec='der'):
|
||||
return asn1tools.compile_string(asn_txt, codec=codec)
|
||||
|
||||
|
||||
# SGP.22 section 4.1 Activation Code
|
||||
class ActivationCode:
|
||||
"""SGP.22 section 4.1 Activation Code"""
|
||||
def __init__(self, hostname:str, token:str, oid: Optional[str] = None, cc_required: Optional[bool] = False):
|
||||
if '$' in hostname:
|
||||
raise ValueError('$ sign not permitted in hostname')
|
||||
@@ -78,7 +78,6 @@ class ActivationCode:
|
||||
|
||||
@staticmethod
|
||||
def decode_str(ac: str) -> dict:
|
||||
"""decode an activation code from its string representation."""
|
||||
if ac[0] != '1':
|
||||
raise ValueError("Unsupported AC_Format '%s'!" % ac[0])
|
||||
ac_elements = ac.split('$')
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning BSP (BPP Protection Protocol),
|
||||
where BPP is the Bound Profile Package. So the full expansion is the
|
||||
"GSMA eSIM Remote SIM Provisioning Bound Profile Packate Protection Protocol"
|
||||
|
||||
Originally (SGP.22 v2.x) this was called SCP03t, but it has since been renamed to BSP."""
|
||||
|
||||
# Early proof-of-concept implementation of
|
||||
# GSMA eSIM RSP (Remote SIM Provisioning BSP (BPP Protection Protocol),
|
||||
# where BPP is the Bound Profile Package. So the full expansion is the
|
||||
# "GSMA eSIM Remote SIM Provisioning Bound Profile Packate Protection Protocol"
|
||||
#
|
||||
# Originally (SGP.22 v2.x) this was called SCP03t, but it has since been
|
||||
# renamed to BSP.
|
||||
#
|
||||
# (C) 2023 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -43,7 +45,6 @@ logger.addHandler(logging.NullHandler())
|
||||
MAX_SEGMENT_SIZE = 1020
|
||||
|
||||
class BspAlgo(abc.ABC):
|
||||
"""Base class representing a cryptographic algorithm within the BSP (BPP Security Protocol)."""
|
||||
blocksize: int
|
||||
|
||||
def _get_padding(self, in_len: int, multiple: int, padding: int = 0) -> bytes:
|
||||
@@ -61,7 +62,6 @@ class BspAlgo(abc.ABC):
|
||||
return self.__class__.__name__
|
||||
|
||||
class BspAlgoCrypt(BspAlgo, abc.ABC):
|
||||
"""Base class representing an encryption/decryption algorithm within the BSP (BPP Security Protocol)."""
|
||||
|
||||
def __init__(self, s_enc: bytes):
|
||||
self.s_enc = s_enc
|
||||
@@ -73,7 +73,7 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
|
||||
block_nr = self.block_nr
|
||||
ciphertext = self._encrypt(padded_data)
|
||||
logger.debug("encrypt(block_nr=%u, s_enc=%s, plaintext=%s, padded=%s) -> %s",
|
||||
block_nr, b2h(self.s_enc)[:20], b2h(data)[:20], b2h(padded_data)[:20], b2h(ciphertext)[:20])
|
||||
block_nr, b2h(self.s_enc), b2h(data), b2h(padded_data), b2h(ciphertext))
|
||||
return ciphertext
|
||||
|
||||
def decrypt(self, data:bytes) -> bytes:
|
||||
@@ -93,7 +93,6 @@ class BspAlgoCrypt(BspAlgo, abc.ABC):
|
||||
"""Actual implementation, to be implemented by derived class."""
|
||||
|
||||
class BspAlgoCryptAES128(BspAlgoCrypt):
|
||||
"""AES-CBC-128 implementation of the BPP Security Protocol for GSMA SGP.22 eSIM."""
|
||||
name = 'AES-CBC-128'
|
||||
blocksize = 16
|
||||
|
||||
@@ -134,7 +133,6 @@ class BspAlgoCryptAES128(BspAlgoCrypt):
|
||||
|
||||
|
||||
class BspAlgoMac(BspAlgo, abc.ABC):
|
||||
"""Base class representing a message authentication code algorithm within the BSP (BPP Security Protocol)."""
|
||||
l_mac = 0 # must be overridden by derived class
|
||||
|
||||
def __init__(self, s_mac: bytes, initial_mac_chaining_value: bytes):
|
||||
@@ -149,20 +147,10 @@ class BspAlgoMac(BspAlgo, abc.ABC):
|
||||
temp_data = self.mac_chain + tag_and_length + data
|
||||
old_mcv = self.mac_chain
|
||||
c_mac = self._auth(temp_data)
|
||||
|
||||
# DEBUG: Show MAC computation details
|
||||
logger.debug(f"MAC_DEBUG: tag=0x{tag:02x}, lcc={lcc}")
|
||||
logger.debug(f"MAC_DEBUG: tag_and_length: {tag_and_length.hex()}")
|
||||
logger.debug(f"MAC_DEBUG: mac_chain[:20]: {old_mcv[:20].hex()}")
|
||||
logger.debug(f"MAC_DEBUG: temp_data[:20]: {temp_data[:20].hex()}")
|
||||
logger.debug(f"MAC_DEBUG: c_mac: {c_mac.hex()}")
|
||||
|
||||
# The output data is computed by concatenating the following data: the tag, the final length, the result of step 2 and the C-MAC value.
|
||||
ret = tag_and_length + data + c_mac
|
||||
logger.debug(f"MAC_DEBUG: final_output[:20]: {ret[:20].hex()}")
|
||||
|
||||
logger.debug("auth(tag=0x%x, mcv=%s, s_mac=%s, plaintext=%s, temp=%s) -> %s",
|
||||
tag, b2h(old_mcv)[:20], b2h(self.s_mac)[:20], b2h(data)[:20], b2h(temp_data)[:20], b2h(ret)[:20])
|
||||
tag, b2h(old_mcv), b2h(self.s_mac), b2h(data), b2h(temp_data), b2h(ret))
|
||||
return ret
|
||||
|
||||
def verify(self, ciphertext: bytes) -> bool:
|
||||
@@ -179,7 +167,6 @@ class BspAlgoMac(BspAlgo, abc.ABC):
|
||||
"""To be implemented by algorithm specific derived class."""
|
||||
|
||||
class BspAlgoMacAES128(BspAlgoMac):
|
||||
"""AES-CMAC-128 implementation of the BPP Security Protocol for GSMA SGP.22 eSIM."""
|
||||
name = 'AES-CMAC-128'
|
||||
l_mac = 8
|
||||
|
||||
@@ -214,11 +201,6 @@ def bsp_key_derivation(shared_secret: bytes, key_type: int, key_length: int, hos
|
||||
s_enc = out[l:2*l]
|
||||
s_mac = out[l*2:3*l]
|
||||
|
||||
logger.debug(f"BSP_KDF_DEBUG: kdf_out = {b2h(out)}")
|
||||
logger.debug(f"BSP_KDF_DEBUG: initial_mcv = {b2h(initial_mac_chaining_value)}")
|
||||
logger.debug(f"BSP_KDF_DEBUG: s_enc = {b2h(s_enc)}")
|
||||
logger.debug(f"BSP_KDF_DEBUG: s_mac = {b2h(s_mac)}")
|
||||
|
||||
return s_enc, s_mac, initial_mac_chaining_value
|
||||
|
||||
|
||||
@@ -243,24 +225,12 @@ class BspInstance:
|
||||
return cls(s_enc, s_mac, initial_mcv)
|
||||
|
||||
def encrypt_and_mac_one(self, tag: int, plaintext:bytes) -> bytes:
|
||||
"""Encrypt + MAC a single plaintext TLV. Returns the protected ciphertext."""
|
||||
"""Encrypt + MAC a single plaintext TLV. Returns the protected ciphertex."""
|
||||
assert tag <= 255
|
||||
assert len(plaintext) <= self.max_payload_size
|
||||
|
||||
# DEBUG: Show what we're processing
|
||||
logger.debug(f"BSP_DEBUG: encrypt_and_mac_one(tag=0x{tag:02x}, plaintext_len={len(plaintext)})")
|
||||
logger.debug(f"BSP_DEBUG: plaintext[:20]: {plaintext[:20].hex()}")
|
||||
logger.debug(f"BSP_DEBUG: s_enc[:20]: {self.c_algo.s_enc[:20].hex()}")
|
||||
logger.debug(f"BSP_DEBUG: s_mac[:20]: {self.m_algo.s_mac[:20].hex()}")
|
||||
|
||||
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext)[:20])
|
||||
logger.debug("encrypt_and_mac_one(tag=0x%x, plaintext=%s)", tag, b2h(plaintext))
|
||||
ciphered = self.c_algo.encrypt(plaintext)
|
||||
logger.debug(f"BSP_DEBUG: ciphered[:20]: {ciphered[:20].hex()}")
|
||||
|
||||
maced = self.m_algo.auth(tag, ciphered)
|
||||
logger.debug(f"BSP_DEBUG: final_result[:20]: {maced[:20].hex()}")
|
||||
logger.debug(f"BSP_DEBUG: final_result_len: {len(maced)}")
|
||||
|
||||
return maced
|
||||
|
||||
def encrypt_and_mac(self, tag: int, plaintext:bytes) -> List[bytes]:
|
||||
@@ -280,11 +250,11 @@ class BspInstance:
|
||||
return result
|
||||
|
||||
def mac_only_one(self, tag: int, plaintext: bytes) -> bytes:
|
||||
"""MAC a single plaintext TLV. Returns the protected ciphertext."""
|
||||
"""MAC a single plaintext TLV. Returns the protected ciphertex."""
|
||||
assert tag <= 255
|
||||
assert len(plaintext) < self.max_payload_size
|
||||
maced = self.m_algo.auth(tag, plaintext)
|
||||
# The data block counter for ICV calculation is incremented also for each segment with C-MAC only.
|
||||
# The data block counter for ICV caluclation is incremented also for each segment with C-MAC only.
|
||||
self.c_algo.block_nr += 1
|
||||
return maced
|
||||
|
||||
@@ -318,7 +288,7 @@ class BspInstance:
|
||||
def demac_only_one(self, ciphertext: bytes) -> bytes:
|
||||
payload = self.m_algo.verify(ciphertext)
|
||||
_tdict, _l, val, _remain = bertlv_parse_one(payload)
|
||||
# The data block counter for ICV calculation is incremented also for each segment with C-MAC only.
|
||||
# The data block counter for ICV caluclation is incremented also for each segment with C-MAC only.
|
||||
self.c_algo.block_nr += 1
|
||||
return val
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning) ES8+ as per SGP22 v3.0 Section 5.5"""
|
||||
|
||||
# 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
|
||||
@@ -25,9 +26,6 @@ import pySim.esim.rsp as rsp
|
||||
from pySim.esim.bsp import BspInstance
|
||||
from pySim.esim import PMO
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 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.
|
||||
@@ -99,7 +97,7 @@ class ProfileMetadata:
|
||||
self.notifications.append((event, address))
|
||||
|
||||
def gen_store_metadata_request(self) -> bytes:
|
||||
"""Generate encoded (but unsigned) StoreMetadataRequest DO (SGP.22 5.5.3)"""
|
||||
"""Generate encoded (but unsigned) StoreMetadataReqest DO (SGP.22 5.5.3)"""
|
||||
smr = {
|
||||
'iccid': self.iccid_bin,
|
||||
'serviceProviderName': self.spn,
|
||||
@@ -199,12 +197,8 @@ class BoundProfilePackage(ProfilePackage):
|
||||
# 'initialiseSecureChannelRequest'
|
||||
bpp_seq = rsp.asn1.encode('InitialiseSecureChannelRequest', iscr)
|
||||
# firstSequenceOf87
|
||||
logger.debug(f"BPP_ENCODE_DEBUG: Encrypting ConfigureISDP with BSP keys")
|
||||
logger.debug(f"BPP_ENCODE_DEBUG: BSP S-ENC: {bsp.c_algo.s_enc.hex()}")
|
||||
logger.debug(f"BPP_ENCODE_DEBUG: BSP S-MAC: {bsp.m_algo.s_mac.hex()}")
|
||||
bpp_seq += encode_seq(0xa0, bsp.encrypt_and_mac(0x87, conf_idsp_bin))
|
||||
# sequenceOF88
|
||||
logger.debug(f"BPP_ENCODE_DEBUG: MAC-only StoreMetadata with BSP keys")
|
||||
bpp_seq += encode_seq(0xa1, bsp.mac_only(0x88, smr_bin))
|
||||
|
||||
if self.ppp: # we have to use session keys
|
||||
|
||||
@@ -26,15 +26,15 @@ logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
class ApiParam(abc.ABC):
|
||||
"""A class representing a single parameter in the API."""
|
||||
"""A class reprsenting a single parameter in the API."""
|
||||
@classmethod
|
||||
def verify_decoded(cls, data):
|
||||
"""Verify the decoded representation of a value. Should raise an exception if something is odd."""
|
||||
"""Verify the decoded reprsentation of a value. Should raise an exception if somthing is odd."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def verify_encoded(cls, data):
|
||||
"""Verify the encoded representation of a value. Should raise an exception if something is odd."""
|
||||
"""Verify the encoded reprsentation of a value. Should raise an exception if somthing is odd."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Implementation of GSMA eSIM RSP (Remote SIM Provisioning) as per SGP22 v3.0"""
|
||||
|
||||
# Implementation of GSMA eSIM RSP (Remote SIM Provisioning)
|
||||
# as per SGP22 v3.0
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -94,57 +95,9 @@ class RspSessionState:
|
||||
self.__dict__.update(state)
|
||||
|
||||
|
||||
class RspSessionStore:
|
||||
"""A wrapper around the database-backed storage 'shelve' for storing RspSessionState objects.
|
||||
Can be configured to use either file-based storage or in-memory storage.
|
||||
We use it to store RspSessionState objects indexed by transactionId."""
|
||||
|
||||
def __init__(self, filename: Optional[str] = None, in_memory: bool = False):
|
||||
self._in_memory = in_memory
|
||||
|
||||
if in_memory:
|
||||
self._shelf = shelve.Shelf(dict())
|
||||
else:
|
||||
if filename is None:
|
||||
raise ValueError("filename is required for file-based session store")
|
||||
self._shelf = shelve.open(filename)
|
||||
|
||||
# dunder magic
|
||||
def __getitem__(self, key):
|
||||
return self._shelf[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._shelf[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self._shelf[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._shelf
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._shelf)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._shelf)
|
||||
|
||||
# everything else
|
||||
def __getattr__(self, name):
|
||||
"""Delegate attribute access to the underlying shelf object."""
|
||||
return getattr(self._shelf, name)
|
||||
|
||||
def close(self):
|
||||
"""Close the session store."""
|
||||
if hasattr(self._shelf, 'close'):
|
||||
self._shelf.close()
|
||||
if self._in_memory:
|
||||
# For in-memory store, clear the reference
|
||||
self._shelf = None
|
||||
|
||||
def sync(self):
|
||||
"""Synchronize the cache with the underlying storage."""
|
||||
if hasattr(self._shelf, 'sync'):
|
||||
self._shelf.sync()
|
||||
class RspSessionStore(shelve.DbfilenameShelf):
|
||||
"""A derived class as wrapper around the database-backed non-volatile storage 'shelve', in case we might
|
||||
need to extend it in the future. We use it to store RspSessionState objects indexed by transactionId."""
|
||||
|
||||
def extract_euiccSigned1(authenticateServerResponse: bytes) -> bytes:
|
||||
"""Extract the raw, DER-encoded binary euiccSigned1 field from the given AuthenticateServerResponse. This
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Implementation of SimAlliance/TCA Interoperable Profile handling"""
|
||||
|
||||
# Implementation of SimAlliance/TCA Interoperable Profile handling
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -18,17 +18,13 @@
|
||||
import logging
|
||||
import abc
|
||||
import io
|
||||
import os
|
||||
from typing import Tuple, List, Optional, Dict, Union
|
||||
from collections import OrderedDict
|
||||
import asn1tools
|
||||
import zipfile
|
||||
from pySim import javacard
|
||||
from osmocom.utils import b2h, h2b, Hexstr
|
||||
from osmocom.tlv import BER_TLV_IE, bertlv_parse_tag, bertlv_parse_len
|
||||
from osmocom.construct import build_construct, parse_construct, GreedyInteger, GreedyBytes, StripHeaderAdapter
|
||||
from osmocom.construct import build_construct, parse_construct, GreedyInteger
|
||||
|
||||
from pySim import ts_102_222
|
||||
from pySim.utils import dec_imsi
|
||||
from pySim.ts_102_221 import FileDescriptor
|
||||
from pySim.filesystem import CardADF, Path
|
||||
@@ -45,7 +41,7 @@ asn1 = compile_asn1_subdir('saip')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Naa:
|
||||
"""A class defining a Network Access Application (NAA)"""
|
||||
"""A class defining a Network Access Application (NAA)."""
|
||||
name = None
|
||||
# AID prefix, as used for ADF and EF.DIR
|
||||
aid = None
|
||||
@@ -61,7 +57,6 @@ class Naa:
|
||||
return 'adf-' + cls.mandatory_services[0]
|
||||
|
||||
class NaaCsim(Naa):
|
||||
"""A class representing the CSIM (CDMA) Network Access Application (NAA)"""
|
||||
name = "csim"
|
||||
aid = h2b("")
|
||||
mandatory_services = ["csim"]
|
||||
@@ -69,7 +64,6 @@ class NaaCsim(Naa):
|
||||
templates = [oid.ADF_CSIM_by_default, oid.ADF_CSIMopt_not_by_default]
|
||||
|
||||
class NaaUsim(Naa):
|
||||
"""A class representing the USIM Network Access Application (NAA)"""
|
||||
name = "usim"
|
||||
aid = h2b("a0000000871002")
|
||||
mandatory_services = ["usim"]
|
||||
@@ -82,7 +76,6 @@ class NaaUsim(Naa):
|
||||
adf = ADF_USIM()
|
||||
|
||||
class NaaIsim(Naa):
|
||||
"""A class representing the ISIM Network Access Application (NAA)"""
|
||||
name = "isim"
|
||||
aid = h2b("a0000000871004")
|
||||
mandatory_services = ["isim"]
|
||||
@@ -111,7 +104,7 @@ class File:
|
||||
self.pe_name = pename
|
||||
self._name = name
|
||||
self.template = template
|
||||
self._body: Optional[bytes] = None
|
||||
self.body: Optional[bytes] = None
|
||||
self.node: Optional['FsNode'] = None
|
||||
self.file_type = None
|
||||
self.fid: Optional[int] = None
|
||||
@@ -121,11 +114,8 @@ class File:
|
||||
self.nb_rec: Optional[int] = None
|
||||
self._file_size = 0
|
||||
self.high_update: bool = False
|
||||
self.read_and_update_when_deact: bool = False
|
||||
self.shareable: bool = True
|
||||
self.df_name = None
|
||||
self.fill_pattern = None
|
||||
self.fill_pattern_repeat = False
|
||||
# apply some defaults from profile
|
||||
if self.template:
|
||||
self.from_template(self.template)
|
||||
@@ -183,7 +173,9 @@ class File:
|
||||
self.file_type = template.file_type
|
||||
self.fid = template.fid
|
||||
self.sfi = template.sfi
|
||||
self.arr = template.arr.to_bytes(1)
|
||||
self.arr = template.arr
|
||||
#self.default_val = template.default_val
|
||||
#self.default_val_repeat = template.default_val_repeat
|
||||
if hasattr(template, 'rec_len'):
|
||||
self.rec_len = template.rec_len
|
||||
else:
|
||||
@@ -196,38 +188,19 @@ class File:
|
||||
# All the files defined in the templates shall have, by default, shareable/not-shareable bit in the file descriptor set to "shareable".
|
||||
self.shareable = True
|
||||
self._template_derived = True
|
||||
if hasattr(template, 'file_size'):
|
||||
self._file_size = template.file_size
|
||||
|
||||
def _recompute_size(self):
|
||||
"""recompute the file size, if needed (body larger than current size)"""
|
||||
body_size = len(self.body)
|
||||
if self.file_size == None or self.file_size < body_size:
|
||||
self._file_size = body_size
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value: bytes):
|
||||
self._body = value
|
||||
# we need to potentially update the file size after changing the body [size]
|
||||
self._recompute_size()
|
||||
|
||||
def to_fileDescriptor(self) -> dict:
|
||||
"""Convert from internal representation to 'fileDescriptor' as used by asn1tools for SAIP"""
|
||||
fileDescriptor = {}
|
||||
fdb_dec = {}
|
||||
pefi = {}
|
||||
spfi = 0
|
||||
if self.fid and self.fid != self.template.fid:
|
||||
if self.fid:
|
||||
fileDescriptor['fileID'] = self.fid.to_bytes(2, 'big')
|
||||
if self.sfi and self.sfi != self.template.sfi:
|
||||
if self.sfi:
|
||||
fileDescriptor['shortEFID'] = bytes([self.sfi])
|
||||
if self.df_name:
|
||||
fileDescriptor['dfName'] = self.df_name
|
||||
if self.arr and self.arr != self.template.arr.to_bytes(1):
|
||||
if self.arr:
|
||||
fileDescriptor['securityAttributesReferenced'] = self.arr
|
||||
if self.file_type in ['LF', 'CY']:
|
||||
fdb_dec['file_type'] = 'working_ef'
|
||||
@@ -260,16 +233,15 @@ class File:
|
||||
if len(fd_dict):
|
||||
fileDescriptor['fileDescriptor'] = build_construct(FileDescriptor._construct, fd_dict)
|
||||
if self.high_update:
|
||||
spfi |= 0x80 # TS 102 222 Table 5
|
||||
if self.read_and_update_when_deact:
|
||||
spfi |= 0x40 # TS 102 222 Table 5
|
||||
if spfi != 0x00:
|
||||
pefi['specialFileInformation'] = spfi.to_bytes(1)
|
||||
if self.fill_pattern:
|
||||
if not self.fill_pattern_repeat:
|
||||
pefi['fillPattern'] = self.fill_pattern
|
||||
else:
|
||||
pefi['repeatPattern'] = self.fill_pattern
|
||||
pefi['specialFileInformation'] = b'\x80' # TS 102 222 Table 5
|
||||
try:
|
||||
if self.template and self.template.default_val_repeat:
|
||||
pefi['repeatPattern'] = self.template.expand_default_value_pattern()
|
||||
elif self.template and self.template.default_val:
|
||||
pefi['fillPattern'] = self.template.expand_default_value_pattern()
|
||||
except ValueError:
|
||||
# ignore this here as without a file or record length we cannot do this
|
||||
pass
|
||||
if len(pefi.keys()):
|
||||
# TODO: When overwriting the default "proprietaryEFInfo" for a template EF for which a
|
||||
# default fill or repeat pattern is defined; it is hence recommended to provide the
|
||||
@@ -301,15 +273,12 @@ class File:
|
||||
self.shareable = fdb_dec['shareable']
|
||||
if fdb_dec['file_type'] == 'working_ef':
|
||||
efFileSize = fileDescriptor.get('efFileSize', None)
|
||||
if efFileSize:
|
||||
self._file_size = self._decode_file_size(efFileSize)
|
||||
if fd_dec['num_of_rec']:
|
||||
self.nb_rec = fd_dec['num_of_rec']
|
||||
if fd_dec['record_len']:
|
||||
self.rec_len = fd_dec['record_len']
|
||||
if efFileSize:
|
||||
self._file_size = self._decode_file_size(efFileSize)
|
||||
if self.rec_len and self.nb_rec == None:
|
||||
# compute the number of records from file size and record length
|
||||
self.nb_rec = self._file_size // self.rec_len
|
||||
if fdb_dec['structure'] == 'linear_fixed':
|
||||
self.file_type = 'LF'
|
||||
elif fdb_dec['structure'] == 'cyclic':
|
||||
@@ -322,17 +291,12 @@ class File:
|
||||
self._file_size = self._decode_file_size(pefi['maximumFileSize'])
|
||||
specialFileInformation = pefi.get('specialFileInformation', None)
|
||||
if specialFileInformation:
|
||||
# TS 102 222 Table 5
|
||||
if specialFileInformation[0] & 0x80:
|
||||
self.high_update = True
|
||||
if specialFileInformation[0] & 0x40:
|
||||
self.read_and_update_when_deact = True
|
||||
self.hihgi_update = True
|
||||
if 'repeatPattern' in pefi:
|
||||
self.fill_pattern = pefi['repeatPattern']
|
||||
self.fill_pattern_repeat = True
|
||||
if 'fillPattern' in pefi:
|
||||
self.fill_pattern = pefi['fillPattern']
|
||||
self.fill_pattern_repeat = False
|
||||
self.repeat_pattern = pefi['repeatPattern']
|
||||
if 'defaultPattern' in pefi:
|
||||
self.repeat_pattern = pefi['defaultPattern']
|
||||
elif fdb_dec['file_type'] == 'df':
|
||||
# only set it, if an earlier call to from_template() didn't alrady set it, as
|
||||
# the template can differentiate between MF, DF and ADF (unlike FDB)
|
||||
@@ -355,7 +319,7 @@ class File:
|
||||
if fd:
|
||||
self.from_fileDescriptor(dict(fd))
|
||||
# BODY
|
||||
self._body = self.file_content_from_tuples(l)
|
||||
self.body = self.file_content_from_tuples(l)
|
||||
|
||||
@staticmethod
|
||||
def path_from_gfm(bin_path: bytes):
|
||||
@@ -377,29 +341,18 @@ class File:
|
||||
ret += self.file_content_to_tuples()
|
||||
return ret
|
||||
|
||||
def expand_fill_pattern(self) -> bytes:
|
||||
"""Expand the fill/repeat pattern as per TS 102 222 Section 6.3.2.2.2"""
|
||||
return ts_102_222.expand_pattern(self.fill_pattern, self.fill_pattern_repeat, self.file_size)
|
||||
|
||||
def file_content_from_tuples(self, l: List[Tuple]) -> Optional[bytes]:
|
||||
@staticmethod
|
||||
def file_content_from_tuples(l: List[Tuple]) -> Optional[bytes]:
|
||||
"""linearize a list of fillFileContent / fillFileOffset tuples into a stream of bytes."""
|
||||
stream = io.BytesIO()
|
||||
# Providing file content within "fillFileContent" / "fillFileOffset" shall have the same effect as
|
||||
# creating a file with a fill/repeat pattern and thereafter updating the content via Update.
|
||||
# Step 1: Fill with pattern from Fcp or Template
|
||||
if self.fill_pattern:
|
||||
stream.write(self.expand_fill_pattern())
|
||||
elif self.template and self.template.default_val:
|
||||
stream.write(self.template.expand_default_value_pattern(self.file_size))
|
||||
stream.seek(0)
|
||||
# then process the fillFileContent/fillFileOffset
|
||||
for k, v in l:
|
||||
if k == 'doNotCreate':
|
||||
return None
|
||||
if k == 'fileDescriptor':
|
||||
pass
|
||||
elif k == 'fillFileOffset':
|
||||
stream.seek(v, os.SEEK_CUR)
|
||||
# FIXME: respect the fillPattern!
|
||||
stream.write(b'\xff' * v)
|
||||
elif k == 'fillFileContent':
|
||||
stream.write(v)
|
||||
else:
|
||||
@@ -473,7 +426,7 @@ class ProfileElement:
|
||||
@property
|
||||
def header_name(self) -> str:
|
||||
"""Return the name of the header field within the profile element."""
|
||||
# unnecessary complication by inconsistent naming :(
|
||||
# unneccessarry compliaction by inconsistent naming :(
|
||||
if self.type.startswith('opt-'):
|
||||
return self.type.replace('-','') + '-header'
|
||||
if self.type in self.header_name_translation_dict:
|
||||
@@ -511,7 +464,7 @@ class ProfileElement:
|
||||
# TODO: cdmaParameter
|
||||
'securityDomain': ProfileElementSD,
|
||||
'rfm': ProfileElementRFM,
|
||||
'application': ProfileElementApplication,
|
||||
# TODO: application
|
||||
# TODO: nonStandard
|
||||
'end': ProfileElementEnd,
|
||||
'mf': ProfileElementMF,
|
||||
@@ -648,14 +601,6 @@ class FsProfileElement(ProfileElement):
|
||||
file = File(k, v, template.files_by_pename.get(k, None))
|
||||
self.add_file(file)
|
||||
|
||||
def create_file(self, pename: str) -> File:
|
||||
"""Programatically create a file by its PE-Name."""
|
||||
template = templates.ProfileTemplateRegistry.get_by_oid(self.templateID)
|
||||
file = File(pename, None, template.files_by_pename.get(pename, None))
|
||||
self.add_file(file)
|
||||
self.decoded[pename] = []
|
||||
return file
|
||||
|
||||
def _post_decode(self):
|
||||
# not entirely sure about doing this this automatism
|
||||
self.pe2files()
|
||||
@@ -753,7 +698,6 @@ class ProfileElementGFM(ProfileElement):
|
||||
|
||||
|
||||
class ProfileElementMF(FsProfileElement):
|
||||
"""Class representing the ProfileElement for the MF (Master File)"""
|
||||
type = 'mf'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -767,7 +711,6 @@ class ProfileElementMF(FsProfileElement):
|
||||
# TODO: resize EF.DIR?
|
||||
|
||||
class ProfileElementPuk(ProfileElement):
|
||||
"""Class representing the ProfileElement for a PUK (PIN Unblocking Code)"""
|
||||
type = 'pukCodes'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -798,7 +741,6 @@ class ProfileElementPuk(ProfileElement):
|
||||
|
||||
|
||||
class ProfileElementPin(ProfileElement):
|
||||
"""Class representing the ProfileElement for a PIN (Personal Identification Number)"""
|
||||
type = 'pinCodes'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -835,7 +777,6 @@ class ProfileElementPin(ProfileElement):
|
||||
|
||||
|
||||
class ProfileElementTelecom(FsProfileElement):
|
||||
"""Class representing the ProfileElement for DF.TELECOM"""
|
||||
type = 'telecom'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -848,7 +789,6 @@ class ProfileElementTelecom(FsProfileElement):
|
||||
self.decoded[fname] = []
|
||||
|
||||
class ProfileElementCD(FsProfileElement):
|
||||
"""Class representing the ProfileElement for DF.CD"""
|
||||
type = 'cd'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -861,7 +801,6 @@ class ProfileElementCD(FsProfileElement):
|
||||
self.decoded[fname] = []
|
||||
|
||||
class ProfileElementPhonebook(FsProfileElement):
|
||||
"""Class representing the ProfileElement for DF.PHONEBOOK"""
|
||||
type = 'phonebook'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -874,7 +813,6 @@ class ProfileElementPhonebook(FsProfileElement):
|
||||
self.decoded[fname] = []
|
||||
|
||||
class ProfileElementGsmAccess(FsProfileElement):
|
||||
"""Class representing the ProfileElement for ADF.USIM/DF.GSM-ACCESS"""
|
||||
type = 'gsm-access'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -887,7 +825,6 @@ class ProfileElementGsmAccess(FsProfileElement):
|
||||
self.decoded[fname] = []
|
||||
|
||||
class ProfileElementDf5GS(FsProfileElement):
|
||||
"""Class representing the ProfileElement for ADF.USIM/DF.5GS"""
|
||||
type = 'df-5gs'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -900,7 +837,6 @@ class ProfileElementDf5GS(FsProfileElement):
|
||||
self.decoded[fname] = []
|
||||
|
||||
class ProfileElementEAP(FsProfileElement):
|
||||
"""Class representing the ProfileElement for DF.EAP"""
|
||||
type = 'eap'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -913,7 +849,6 @@ class ProfileElementEAP(FsProfileElement):
|
||||
self.decoded[fname] = []
|
||||
|
||||
class ProfileElementDfSAIP(FsProfileElement):
|
||||
"""Class representing the ProfileElement for DF.SAIP"""
|
||||
type = 'df-saip'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -926,7 +861,6 @@ class ProfileElementDfSAIP(FsProfileElement):
|
||||
self.decoded[fname] = []
|
||||
|
||||
class ProfileElementDfSNPN(FsProfileElement):
|
||||
"""Class representing the ProfileElement for DF.SNPN"""
|
||||
type = 'df-snpn'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -939,7 +873,6 @@ class ProfileElementDfSNPN(FsProfileElement):
|
||||
self.decoded[fname] = []
|
||||
|
||||
class ProfileElementDf5GProSe(FsProfileElement):
|
||||
"""Class representing the ProfileElement for DF.5GProSe"""
|
||||
type = 'df-5gprose'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -976,7 +909,7 @@ class SecurityDomainKeyComponent:
|
||||
'macLength': self.mac_length}
|
||||
|
||||
class SecurityDomainKey:
|
||||
"""Representation of a key used for SCP access to a security domain."""
|
||||
"""Represenation of a key used for SCP access to a security domain."""
|
||||
def __init__(self, key_version_number: int, key_id: int, key_usage_qualifier: dict,
|
||||
key_components: List[SecurityDomainKeyComponent]):
|
||||
self.key_usage_qualifier = key_usage_qualifier
|
||||
@@ -1104,136 +1037,7 @@ class ProfileElementSSD(ProfileElementSD):
|
||||
'uiccToolkitApplicationSpecificParametersField': h2b('01000001000000020112036C756500'),
|
||||
}
|
||||
|
||||
class ProfileElementApplication(ProfileElement):
|
||||
"""Class representing an application ProfileElement."""
|
||||
type = 'application'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
super().__init__(decoded, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_file(cls,
|
||||
filename:str,
|
||||
aid:Hexstr,
|
||||
sd_aid:Hexstr = None,
|
||||
non_volatile_code_limit:int = None,
|
||||
volatile_data_limit:int = None,
|
||||
non_volatile_data_limit:int = None,
|
||||
hash_value:Hexstr = None) -> 'ProfileElementApplication':
|
||||
"""Fill contents of application ProfileElement from a .cap file."""
|
||||
|
||||
inst = cls()
|
||||
Construct_data_limit = StripHeaderAdapter(GreedyBytes, 4, steps = [2,4])
|
||||
|
||||
if filename.lower().endswith('.cap'):
|
||||
cap = javacard.CapFile(filename)
|
||||
load_block_object = cap.get_loadfile()
|
||||
elif filename.lower().endswith('.ijc'):
|
||||
fd = open(filename, 'rb')
|
||||
load_block_object = fd.read()
|
||||
else:
|
||||
raise ValueError('Invalid file type, file must either .cap or .ijc')
|
||||
|
||||
# Mandatory
|
||||
inst.decoded['loadBlock'] = {
|
||||
'loadPackageAID': h2b(aid),
|
||||
'loadBlockObject': load_block_object
|
||||
}
|
||||
|
||||
# Optional
|
||||
if sd_aid:
|
||||
inst.decoded['loadBlock']['securityDomainAID'] = h2b(sd_aid)
|
||||
if non_volatile_code_limit:
|
||||
inst.decoded['loadBlock']['nonVolatileCodeLimitC6'] = Construct_data_limit.build(non_volatile_code_limit)
|
||||
if volatile_data_limit:
|
||||
inst.decoded['loadBlock']['volatileDataLimitC7'] = Construct_data_limit.build(volatile_data_limit)
|
||||
if non_volatile_data_limit:
|
||||
inst.decoded['loadBlock']['nonVolatileDataLimitC8'] = Construct_data_limit.build(non_volatile_data_limit)
|
||||
if hash_value:
|
||||
inst.decoded['loadBlock']['hashValue'] = h2b(hash_value)
|
||||
|
||||
return inst
|
||||
|
||||
def to_file(self, filename:str):
|
||||
"""Write loadBlockObject contents of application ProfileElement to a .cap or .ijc file."""
|
||||
|
||||
load_package_aid = b2h(self.decoded['loadBlock']['loadPackageAID'])
|
||||
load_block_object = self.decoded['loadBlock']['loadBlockObject']
|
||||
|
||||
if filename.lower().endswith('.cap'):
|
||||
with io.BytesIO(load_block_object) as f, zipfile.ZipFile(filename, 'w') as z:
|
||||
javacard.ijc_to_cap(f, z, load_package_aid)
|
||||
elif filename.lower().endswith('.ijc'):
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(load_block_object)
|
||||
else:
|
||||
raise ValueError('Invalid file type, file must either .cap or .ijc')
|
||||
|
||||
def add_instance(self,
|
||||
aid:Hexstr,
|
||||
class_aid:Hexstr,
|
||||
inst_aid:Hexstr,
|
||||
app_privileges:Hexstr,
|
||||
app_spec_pars:Hexstr,
|
||||
uicc_toolkit_app_spec_pars:Hexstr = None,
|
||||
uicc_access_app_spec_pars:Hexstr = None,
|
||||
uicc_adm_access_app_spec_pars:Hexstr = None,
|
||||
volatile_memory_quota:Hexstr = None,
|
||||
non_volatile_memory_quota:Hexstr = None,
|
||||
process_data:list[Hexstr] = None):
|
||||
"""Create a new instance and add it to the instanceList"""
|
||||
|
||||
# Mandatory
|
||||
inst = {'applicationLoadPackageAID': h2b(aid),
|
||||
'classAID': h2b(class_aid),
|
||||
'instanceAID': h2b(inst_aid),
|
||||
'applicationPrivileges': h2b(app_privileges),
|
||||
'applicationSpecificParametersC9': h2b(app_spec_pars)}
|
||||
|
||||
# Optional
|
||||
if uicc_toolkit_app_spec_pars or uicc_access_app_spec_pars or uicc_adm_access_app_spec_pars:
|
||||
inst['applicationParameters'] = {}
|
||||
if uicc_toolkit_app_spec_pars:
|
||||
inst['applicationParameters']['uiccToolkitApplicationSpecificParametersField'] = \
|
||||
h2b(uicc_toolkit_app_spec_pars)
|
||||
if uicc_access_app_spec_pars:
|
||||
inst['applicationParameters']['uiccAccessApplicationSpecificParametersField'] = \
|
||||
h2b(uicc_access_app_spec_pars)
|
||||
if uicc_adm_access_app_spec_pars:
|
||||
inst['applicationParameters']['uiccAdministrativeAccessApplicationSpecificParametersField'] = \
|
||||
h2b(uicc_adm_access_app_spec_pars)
|
||||
if volatile_memory_quota is not None or non_volatile_memory_quota is not None:
|
||||
inst['systemSpecificParameters'] = {}
|
||||
Construct_data_limit = StripHeaderAdapter(GreedyBytes, 4, steps = [2,4])
|
||||
if volatile_memory_quota is not None:
|
||||
inst['systemSpecificParameters']['volatileMemoryQuotaC7'] = \
|
||||
Construct_data_limit.build(volatile_memory_quota)
|
||||
if non_volatile_memory_quota is not None:
|
||||
inst['systemSpecificParameters']['nonVolatileMemoryQuotaC8'] = \
|
||||
Construct_data_limit.build(non_volatile_memory_quota)
|
||||
if len(process_data) > 0:
|
||||
inst['processData'] = []
|
||||
for proc in process_data:
|
||||
inst['processData'].append(h2b(proc))
|
||||
|
||||
# Append created instance to instance list
|
||||
if 'instanceList' not in self.decoded.keys():
|
||||
self.decoded['instanceList'] = []
|
||||
self.decoded['instanceList'].append(inst)
|
||||
|
||||
def remove_instance(self, inst_aid:Hexstr):
|
||||
"""Remove an instance from the instanceList"""
|
||||
inst_list = self.decoded.get('instanceList', [])
|
||||
for inst in enumerate(inst_list):
|
||||
if b2h(inst[1].get('instanceAID', None)) == inst_aid:
|
||||
inst_list.pop(inst[0])
|
||||
return
|
||||
raise ValueError("instance AID: '%s' not present in instanceList, cannot remove instance" % inst[1])
|
||||
|
||||
|
||||
|
||||
class ProfileElementRFM(ProfileElement):
|
||||
"""Class representing the ProfileElement for RFM (Remote File Management)."""
|
||||
type = 'rfm'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None,
|
||||
@@ -1259,7 +1063,6 @@ class ProfileElementRFM(ProfileElement):
|
||||
}
|
||||
|
||||
class ProfileElementUSIM(FsProfileElement):
|
||||
"""Class representing the ProfileElement for ADF.USIM Mandatory Files"""
|
||||
type = 'usim'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -1277,12 +1080,10 @@ class ProfileElementUSIM(FsProfileElement):
|
||||
|
||||
@property
|
||||
def imsi(self) -> Optional[str]:
|
||||
template = templates.ProfileTemplateRegistry.get_by_oid(self.templateID)
|
||||
f = File('ef-imsi', self.decoded['ef-imsi'], template.files_by_pename.get('ef-imsi', None))
|
||||
f = File('ef-imsi', self.decoded['ef-imsi'])
|
||||
return dec_imsi(b2h(f.body))
|
||||
|
||||
class ProfileElementOptUSIM(FsProfileElement):
|
||||
"""Class representing the ProfileElement for ADF.USIM Optional Files"""
|
||||
type = 'opt-usim'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -1293,7 +1094,6 @@ class ProfileElementOptUSIM(FsProfileElement):
|
||||
self.decoded['templateID'] = str(oid.ADF_USIMopt_not_by_default_v2)
|
||||
|
||||
class ProfileElementISIM(FsProfileElement):
|
||||
"""Class representing the ProfileElement for ADF.ISIM Mandatory Files"""
|
||||
type = 'isim'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -1310,7 +1110,6 @@ class ProfileElementISIM(FsProfileElement):
|
||||
return b2h(self.decoded['adf-isim'][0][1]['dfName'])
|
||||
|
||||
class ProfileElementOptISIM(FsProfileElement):
|
||||
"""Class representing the ProfileElement for ADF.ISIM Optional Files"""
|
||||
type = 'opt-isim'
|
||||
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
@@ -1322,7 +1121,6 @@ class ProfileElementOptISIM(FsProfileElement):
|
||||
|
||||
|
||||
class ProfileElementAKA(ProfileElement):
|
||||
"""Class representing the ProfileElement for Authentication and Key Agreement (AKA)."""
|
||||
type = 'akaParameter'
|
||||
# TODO: RES size for USIM test algorithm can be set to 32, 64 or 128 bits. This value was
|
||||
# previously limited to 128 bits. Recommendation: Avoid using RES size 32 or 64 in Profiles
|
||||
@@ -1402,7 +1200,6 @@ class ProfileElementAKA(ProfileElement):
|
||||
})
|
||||
|
||||
class ProfileElementHeader(ProfileElement):
|
||||
"""Class representing the ProfileElement for the Header of the PE-Sequence."""
|
||||
type = 'header'
|
||||
def __init__(self, decoded: Optional[dict] = None,
|
||||
ver_major: Optional[int] = 2, ver_minor: Optional[int] = 3,
|
||||
@@ -1433,17 +1230,7 @@ class ProfileElementHeader(ProfileElement):
|
||||
if profile_type:
|
||||
self.decoded['profileType'] = profile_type
|
||||
|
||||
def mandatory_service_add(self, service_name):
|
||||
self.decoded['eUICC-Mandatory-services'][service_name] = None
|
||||
|
||||
def mandatory_service_remove(self, service_name):
|
||||
if service_name in self.decoded['eUICC-Mandatory-services'].keys():
|
||||
del self.decoded['eUICC-Mandatory-services'][service_name]
|
||||
else:
|
||||
raise ValueError("service not in eUICC-Mandatory-services list, cannot remove")
|
||||
|
||||
class ProfileElementEnd(ProfileElement):
|
||||
"""Class representing the ProfileElement for the End of the PE-Sequence."""
|
||||
type = 'end'
|
||||
def __init__(self, decoded: Optional[dict] = None, **kwargs):
|
||||
super().__init__(decoded, **kwargs)
|
||||
@@ -1465,7 +1252,7 @@ class ProfileElementSequence:
|
||||
sequence."""
|
||||
def __init__(self):
|
||||
"""After calling the constructor, you have to further initialize the instance by either
|
||||
calling the parse_der() method, or by manually adding individual PEs, including the header and
|
||||
calling the parse_der() method, or by manually adding individual PEs, including the hedaer and
|
||||
end PEs."""
|
||||
self.pe_list: List[ProfileElement] = []
|
||||
self.pe_by_type: Dict = {}
|
||||
@@ -1487,7 +1274,7 @@ class ProfileElementSequence:
|
||||
def add_hdr_and_end(self):
|
||||
"""Initialize the PE Sequence with a header and end PE."""
|
||||
if len(self.pe_list):
|
||||
raise ValueError("Cannot add header + end PE to a non-empty PE-Sequence")
|
||||
raise ValueError("Cannot add header + end PE to a non-enmpty PE-Sequence")
|
||||
# start with a minimal/empty sequence of header + end
|
||||
self.append(ProfileElementHeader())
|
||||
self.append(ProfileElementEnd())
|
||||
@@ -1504,7 +1291,7 @@ class ProfileElementSequence:
|
||||
|
||||
def get_pe_for_type(self, tname: str) -> Optional[ProfileElement]:
|
||||
"""Return a single profile element for given profile element type. Works only for
|
||||
types of which there is only a single instance in the PE Sequence!"""
|
||||
types of which there is only a signle instance in the PE Sequence!"""
|
||||
l = self.get_pes_for_type(tname)
|
||||
if len(l) == 0:
|
||||
return None
|
||||
@@ -1512,7 +1299,7 @@ class ProfileElementSequence:
|
||||
return l[0]
|
||||
|
||||
def get_pes_for_templateID(self, tid: oid.OID) -> List[ProfileElement]:
|
||||
"""Return list of profile elements present for given profile element type."""
|
||||
"""Return list of profile elements present for given profile eleemnt type."""
|
||||
res = []
|
||||
for pe in self.pe_list:
|
||||
if not pe.templateID:
|
||||
@@ -1689,27 +1476,6 @@ class ProfileElementSequence:
|
||||
pe.header['identification'] = i
|
||||
i += 1
|
||||
|
||||
def get_index_by_pe(self, pe: ProfileElement) -> int:
|
||||
"""Return a list with the indicies of all instances of PEs of petype."""
|
||||
ret = []
|
||||
i = 0
|
||||
for cur in self.pe_list:
|
||||
if cur == pe:
|
||||
return i
|
||||
i += 1
|
||||
raise ValueError('PE %s is not part of PE Sequence' % (pe))
|
||||
|
||||
def insert_at_index(self, idx: int, pe: ProfileElement) -> None:
|
||||
"""Insert a given [new] ProfileElement at given index into the PE Sequence."""
|
||||
self.pe_list.insert(idx, pe)
|
||||
self._process_pelist()
|
||||
self.renumber_identification()
|
||||
|
||||
def insert_after_pe(self, pe_before: ProfileElement, pe_new: ProfileElement) -> None:
|
||||
"""Insert a given [new] ProfileElement after a given [existing] PE in the PE Sequence."""
|
||||
idx = self.get_index_by_pe(pe_before)
|
||||
self.insert_at_index(idx+1, pe_new)
|
||||
|
||||
def get_index_by_type(self, petype: str) -> List[int]:
|
||||
"""Return a list with the indicies of all instances of PEs of petype."""
|
||||
ret = []
|
||||
@@ -1725,7 +1491,9 @@ class ProfileElementSequence:
|
||||
# find MNO-SD index
|
||||
idx = self.get_index_by_type('securityDomain')[0]
|
||||
# insert _after_ MNO-SD
|
||||
self.insert_at_index(idx+1, ssd)
|
||||
self.pe_list.insert(idx+1, ssd)
|
||||
self._process_pelist()
|
||||
self.renumber_identification()
|
||||
|
||||
def remove_naas_of_type(self, naa: Naa) -> None:
|
||||
"""Remove all instances of NAAs of given type. This can be used, for example,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Implementation of SimAlliance/TCA Interoperable Profile OIDs"""
|
||||
|
||||
# Implementation of SimAlliance/TCA Interoperable Profile OIDs
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Implementation of Personalization of eSIM profiles in SimAlliance/TCA Interoperable Profile."""
|
||||
|
||||
# Implementation of SimAlliance/TCA Interoperable Profile handling
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -67,7 +67,7 @@ class Iccid(ConfigurableParameter):
|
||||
If the string of digits is only 18 digits long, a Luhn check digit will be added."""
|
||||
|
||||
def validate(self):
|
||||
# convert to string as it might be an integer
|
||||
# convert to string as it migt be an integer
|
||||
iccid_str = str(self.input_value)
|
||||
if len(iccid_str) < 18 or len(iccid_str) > 20:
|
||||
raise ValueError('ICCID must be 18, 19 or 20 digits long')
|
||||
@@ -86,7 +86,7 @@ class Imsi(ConfigurableParameter):
|
||||
the last digit of the IMSI."""
|
||||
|
||||
def validate(self):
|
||||
# convert to string as it might be an integer
|
||||
# convert to string as it migt be an integer
|
||||
imsi_str = str(self.input_value)
|
||||
if len(imsi_str) < 6 or len(imsi_str) > 15:
|
||||
raise ValueError('IMSI must be 6..15 digits long')
|
||||
@@ -112,7 +112,7 @@ class SdKey(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
key_id = None
|
||||
kvn = None
|
||||
key_usage_qual = None
|
||||
permitted_len = []
|
||||
permitted_len = None
|
||||
|
||||
def validate(self):
|
||||
if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
|
||||
@@ -300,7 +300,7 @@ class Adm2(Pin, keyReference=0x0B):
|
||||
|
||||
|
||||
class AlgoConfig(ConfigurableParameter, metaclass=ClassVarMeta):
|
||||
"""Configurable Algorithm parameter."""
|
||||
"""Configurable Algorithm parameter. bytes."""
|
||||
key = None
|
||||
def validate(self):
|
||||
if not isinstance(self.input_value, (io.BytesIO, bytes, bytearray)):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Implementation of SimAlliance/TCA Interoperable Profile Templates."""
|
||||
|
||||
# Implementation of SimAlliance/TCA Interoperable Profile Template handling
|
||||
#
|
||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -224,8 +224,8 @@ class ProfileTemplateRegistry:
|
||||
# below are transcribed template definitions from "ANNEX A (Normative): File Structure Templates Definition"
|
||||
# of "Profile interoperability specification V3.3.1 Final" (unless other version explicitly specified).
|
||||
|
||||
# Section 9.2
|
||||
class FilesAtMF(ProfileTemplate):
|
||||
"""Files at MF as per Section 9.2"""
|
||||
created_by_default = True
|
||||
oid = OID.MF
|
||||
files = [
|
||||
@@ -238,8 +238,8 @@ class FilesAtMF(ProfileTemplate):
|
||||
]
|
||||
|
||||
|
||||
# Section 9.3
|
||||
class FilesCD(ProfileTemplate):
|
||||
"""Files at DF.CD as per Section 9.3"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_CD
|
||||
files = [
|
||||
@@ -287,8 +287,8 @@ for i in range(0x90, 0x98):
|
||||
for i in range(0x98, 0xa0):
|
||||
df_pb_files.append(FileTemplate(0x4f00+i, 'EF.CCP1', 'LF', None, None, 5, None, 'FF...FF', False, ['nb_rec','size','sfi'], ppath=[0x5f3a]))
|
||||
|
||||
# Section 9.4 v2.3.1
|
||||
class FilesTelecom(ProfileTemplate):
|
||||
"""Files at DF.TELECOM as per Section 9.4 v2.3.1"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_TELECOM
|
||||
base_path = Path('MF')
|
||||
@@ -328,8 +328,8 @@ class FilesTelecom(ProfileTemplate):
|
||||
]
|
||||
|
||||
|
||||
# Section 9.4
|
||||
class FilesTelecomV2(ProfileTemplate):
|
||||
"""Files at DF.TELECOM as per Section 9.4"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_TELECOM_v2
|
||||
base_path = Path('MF')
|
||||
@@ -379,8 +379,8 @@ class FilesTelecomV2(ProfileTemplate):
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.1 v2.3.1
|
||||
class FilesUsimMandatory(ProfileTemplate):
|
||||
"""Mandatory Files at ADF.USIM as per Section 9.5.1 v2.3.1"""
|
||||
created_by_default = True
|
||||
oid = OID.ADF_USIM_by_default
|
||||
files = [
|
||||
@@ -410,8 +410,8 @@ class FilesUsimMandatory(ProfileTemplate):
|
||||
FileTemplate(0x6fe4, 'EF.EPSNSC', 'LF', 1, 80, 5, 0x18, 'FF...FF', False, ass_serv=[85], high_update=True),
|
||||
]
|
||||
|
||||
# Section 9.5.1
|
||||
class FilesUsimMandatoryV2(ProfileTemplate):
|
||||
"""Mandatory Files at ADF.USIM as per Section 9.5.1"""
|
||||
created_by_default = True
|
||||
oid = OID.ADF_USIM_by_default_v2
|
||||
files = [
|
||||
@@ -442,8 +442,8 @@ class FilesUsimMandatoryV2(ProfileTemplate):
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.2 v2.3.1
|
||||
class FilesUsimOptional(ProfileTemplate):
|
||||
"""Optional Files at ADF.USIM as per Section 9.5.2 v2.3.1"""
|
||||
created_by_default = False
|
||||
optional = True
|
||||
oid = OID.ADF_USIMopt_not_by_default
|
||||
@@ -529,7 +529,6 @@ class FilesUsimOptional(ProfileTemplate):
|
||||
|
||||
# Section 9.5.2
|
||||
class FilesUsimOptionalV2(ProfileTemplate):
|
||||
"""Optional Files at ADF.USIM as per Section 9.5.2"""
|
||||
created_by_default = False
|
||||
optional = True
|
||||
oid = OID.ADF_USIMopt_not_by_default_v2
|
||||
@@ -623,8 +622,8 @@ class FilesUsimOptionalV2(ProfileTemplate):
|
||||
FileTemplate(0x6ffd, 'EF.MudMidCfgdata','BT', None, None,2, None, None, True, ['size'], ass_serv=[134]),
|
||||
]
|
||||
|
||||
# Section 9.5.2.3 v3.3.1
|
||||
class FilesUsimOptionalV3(ProfileTemplate):
|
||||
"""Optional Files at ADF.USIM as per Section 9.5.2.3 v3.3.1"""
|
||||
created_by_default = False
|
||||
optional = True
|
||||
oid = OID.ADF_USIMopt_not_by_default_v3
|
||||
@@ -634,16 +633,16 @@ class FilesUsimOptionalV3(ProfileTemplate):
|
||||
FileTemplate(0x6f01, 'EF.eAKA', 'TR', None, 1, 3, None, None, True, ['size'], ass_serv=[134]),
|
||||
]
|
||||
|
||||
# Section 9.5.3
|
||||
class FilesUsimDfPhonebook(ProfileTemplate):
|
||||
"""DF.PHONEBOOK Files at ADF.USIM as per Section 9.5.3"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_PHONEBOOK_ADF_USIM
|
||||
base_path = Path('ADF.USIM')
|
||||
files = df_pb_files
|
||||
|
||||
|
||||
# Section 9.5.4
|
||||
class FilesUsimDfGsmAccess(ProfileTemplate):
|
||||
"""DF.GSM-ACCESS Files at ADF.USIM as per Section 9.5.4"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_GSM_ACCESS_ADF_USIM
|
||||
base_path = Path('ADF.USIM')
|
||||
@@ -657,8 +656,8 @@ class FilesUsimDfGsmAccess(ProfileTemplate):
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.11 v2.3.1
|
||||
class FilesUsimDf5GS(ProfileTemplate):
|
||||
"""DF.5GS Files at ADF.USIM as per Section 9.5.11 v2.3.1"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_5GS
|
||||
base_path = Path('ADF.USIM')
|
||||
@@ -678,8 +677,8 @@ class FilesUsimDf5GS(ProfileTemplate):
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.11.2
|
||||
class FilesUsimDf5GSv2(ProfileTemplate):
|
||||
"""DF.5GS Files at ADF.USIM as per Section 9.5.11.2"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_5GS_v2
|
||||
base_path = Path('ADF.USIM')
|
||||
@@ -701,8 +700,8 @@ class FilesUsimDf5GSv2(ProfileTemplate):
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.11.3
|
||||
class FilesUsimDf5GSv3(ProfileTemplate):
|
||||
"""DF.5GS Files at ADF.USIM as per Section 9.5.11.3"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_5GS_v3
|
||||
base_path = Path('ADF.USIM')
|
||||
@@ -725,8 +724,8 @@ class FilesUsimDf5GSv3(ProfileTemplate):
|
||||
FileTemplate(0x4f0c, 'EF.TN3GPPSNN', 'TR', None, 1, 2, 0x0c, '00', False, ass_serv=[135]),
|
||||
]
|
||||
|
||||
# Section 9.5.11.4
|
||||
class FilesUsimDf5GSv4(ProfileTemplate):
|
||||
"""DF.5GS Files at ADF.USIM as per Section 9.5.11.4"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_5GS_v4
|
||||
base_path = Path('ADF.USIM')
|
||||
@@ -757,8 +756,8 @@ class FilesUsimDf5GSv4(ProfileTemplate):
|
||||
]
|
||||
|
||||
|
||||
# Section 9.5.12
|
||||
class FilesUsimDfSaip(ProfileTemplate):
|
||||
"""DF.SAIP Files at ADF.USIM as per Section 9.5.12"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_SAIP
|
||||
base_path = Path('ADF.USIM')
|
||||
@@ -768,8 +767,8 @@ class FilesUsimDfSaip(ProfileTemplate):
|
||||
FileTemplate(0x4f01, 'EF.SUCICalcInfo','TR', None, None, 3, None, 'FF...FF', False, ['size'], ass_serv=[125], pe_name='ef-suci-calc-info-usim'),
|
||||
]
|
||||
|
||||
# Section 9.5.13
|
||||
class FilesDfSnpn(ProfileTemplate):
|
||||
"""DF.SNPN Files at ADF.USIM as per Section 9.5.13"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_SNPN
|
||||
base_path = Path('ADF.USIM')
|
||||
@@ -779,8 +778,8 @@ class FilesDfSnpn(ProfileTemplate):
|
||||
FileTemplate(0x4f01, 'EF.PWS_SNPN', 'TR', None, 1, 10, None, None, True, ass_serv=[143]),
|
||||
]
|
||||
|
||||
# Section 9.5.14
|
||||
class FilesDf5GProSe(ProfileTemplate):
|
||||
"""DF.ProSe Files at ADF.USIM as per Section 9.5.14"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_5GProSe
|
||||
base_path = Path('ADF.USIM')
|
||||
@@ -795,8 +794,8 @@ class FilesDf5GProSe(ProfileTemplate):
|
||||
FileTemplate(0x4f06, 'EF.5G_PROSE_UIR', 'TR', None, 32, 2, 0x06, None, True, ass_serv=[139,1005]),
|
||||
]
|
||||
|
||||
# Section 9.6.1
|
||||
class FilesIsimMandatory(ProfileTemplate):
|
||||
"""Mandatory Files at ADF.ISIM as per Section 9.6.1"""
|
||||
created_by_default = True
|
||||
oid = OID.ADF_ISIM_by_default
|
||||
files = [
|
||||
@@ -810,8 +809,8 @@ class FilesIsimMandatory(ProfileTemplate):
|
||||
]
|
||||
|
||||
|
||||
# Section 9.6.2 v2.3.1
|
||||
class FilesIsimOptional(ProfileTemplate):
|
||||
"""Optional Files at ADF.ISIM as per Section 9.6.2 of v2.3.1"""
|
||||
created_by_default = False
|
||||
optional = True
|
||||
oid = OID.ADF_ISIMopt_not_by_default
|
||||
@@ -830,8 +829,8 @@ class FilesIsimOptional(ProfileTemplate):
|
||||
]
|
||||
|
||||
|
||||
# Section 9.6.2
|
||||
class FilesIsimOptionalv2(ProfileTemplate):
|
||||
"""Optional Files at ADF.ISIM as per Section 9.6.2"""
|
||||
created_by_default = False
|
||||
optional = True
|
||||
oid = OID.ADF_ISIMopt_not_by_default_v2
|
||||
@@ -858,8 +857,8 @@ class FilesIsimOptionalv2(ProfileTemplate):
|
||||
# TODO: CSIM
|
||||
|
||||
|
||||
# Section 9.8
|
||||
class FilesEap(ProfileTemplate):
|
||||
"""Files at DF.EAP as per Section 9.8"""
|
||||
created_by_default = False
|
||||
oid = OID.DF_EAP
|
||||
files = [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""Implementation of SimAlliance/TCA Interoperable Profile validation."""
|
||||
|
||||
# Implementation of SimAlliance/TCA Interoperable Profile handling
|
||||
#
|
||||
# (C) 2023-2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -19,21 +19,16 @@
|
||||
from pySim.esim.saip import *
|
||||
|
||||
class ProfileError(Exception):
|
||||
"""Raised when a ProfileConstraintChecker finds an error in a file [structure]."""
|
||||
pass
|
||||
|
||||
class ProfileConstraintChecker:
|
||||
"""Base class of a constraint checker for a ProfileElementSequence."""
|
||||
def check(self, pes: ProfileElementSequence):
|
||||
"""Execute all the check_* methods of the ProfileConstraintChecker against the given
|
||||
ProfileElementSequence"""
|
||||
for name in dir(self):
|
||||
if name.startswith('check_'):
|
||||
method = getattr(self, name)
|
||||
method(pes)
|
||||
|
||||
class CheckBasicStructure(ProfileConstraintChecker):
|
||||
"""ProfileConstraintChecker for the basic profile structure constraints."""
|
||||
def _is_after_if_exists(self, pes: ProfileElementSequence, opt:str, after:str):
|
||||
opt_pe = pes.get_pe_for_type(opt)
|
||||
if opt_pe:
|
||||
@@ -43,7 +38,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
||||
# FIXME: check order
|
||||
|
||||
def check_start_and_end(self, pes: ProfileElementSequence):
|
||||
"""Check for mandatory header and end ProfileElements at the right position."""
|
||||
if pes.pe_list[0].type != 'header':
|
||||
raise ProfileError('first element is not header')
|
||||
if pes.pe_list[1].type != 'mf':
|
||||
@@ -53,7 +47,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
||||
raise ProfileError('last element is not end')
|
||||
|
||||
def check_number_of_occurrence(self, pes: ProfileElementSequence):
|
||||
"""Check The number of occurrence of various ProfileElements."""
|
||||
# check for invalid number of occurrences
|
||||
if len(pes.get_pes_for_type('header')) != 1:
|
||||
raise ProfileError('multiple ProfileHeader')
|
||||
@@ -67,7 +60,6 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
||||
raise ProfileError('multiple PE-%s' % tn.upper())
|
||||
|
||||
def check_optional_ordering(self, pes: ProfileElementSequence):
|
||||
"""Check the ordering of optional PEs following the respective mandatory ones."""
|
||||
# ordering and required depenencies
|
||||
self._is_after_if_exists(pes,'opt-usim', 'usim')
|
||||
self._is_after_if_exists(pes,'opt-isim', 'isim')
|
||||
@@ -112,21 +104,17 @@ class CheckBasicStructure(ProfileConstraintChecker):
|
||||
FileChoiceList = List[Tuple]
|
||||
|
||||
class FileError(ProfileError):
|
||||
"""Raised when a FileConstraintChecker finds an error in a file [structure]."""
|
||||
pass
|
||||
|
||||
class FileConstraintChecker:
|
||||
def check(self, l: FileChoiceList):
|
||||
"""Execute all the check_* methods of the FileConstraintChecker against the given FileChoiceList"""
|
||||
for name in dir(self):
|
||||
if name.startswith('check_'):
|
||||
method = getattr(self, name)
|
||||
method(l)
|
||||
|
||||
class FileCheckBasicStructure(FileConstraintChecker):
|
||||
"""Validator for the basic structure of a decoded file."""
|
||||
def check_seqence(self, l: FileChoiceList):
|
||||
"""Check the sequence/ordering."""
|
||||
by_type = {}
|
||||
for k, v in l:
|
||||
if k in by_type:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Implementation of X.509 certificate handling in GSMA eSIM as per SGP22 v3.0"""
|
||||
|
||||
# Implementation of X.509 certificate handling in GSMA eSIM
|
||||
# as per SGP22 v3.0
|
||||
#
|
||||
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@@ -25,13 +26,12 @@ from cryptography.hazmat.primitives.serialization import load_pem_private_key, E
|
||||
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
|
||||
|
||||
from pySim.utils import b2h
|
||||
from . import x509_err
|
||||
|
||||
def check_signed(signed: x509.Certificate, signer: x509.Certificate) -> bool:
|
||||
"""Verify if 'signed' certificate was signed using 'signer'."""
|
||||
# this code only works for ECDSA, but this is all we need for GSMA eSIM
|
||||
pkey = signer.public_key()
|
||||
# this 'signed.signature_algorithm_parameters' below requires cryptography 41.0.0 :(
|
||||
# this 'signed.signature_algorithm_parameters' below requires cryptopgraphy 41.0.0 :(
|
||||
pkey.verify(signed.signature, signed.tbs_certificate_bytes, signed.signature_algorithm_parameters)
|
||||
|
||||
def cert_get_subject_key_id(cert: x509.Certificate) -> bytes:
|
||||
@@ -65,6 +65,9 @@ class oid:
|
||||
id_rspRole_ds_tls_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.6')
|
||||
id_rspRole_ds_auth_v2 = x509.ObjectIdentifier(ID_RSP_ROLE + '.7')
|
||||
|
||||
class VerifyError(Exception):
|
||||
"""An error during certificate verification,"""
|
||||
|
||||
class CertificateSet:
|
||||
"""A set of certificates consisting of a trusted [self-signed] CA root certificate,
|
||||
and an optional number of intermediate certificates. Can be used to verify the certificate chain
|
||||
@@ -133,7 +136,7 @@ class CertificateSet:
|
||||
# we cannot check if there's no CRL
|
||||
return
|
||||
if self.crl.get_revoked_certificate_by_serial_number(cert.serial_nr):
|
||||
raise x509_err.CertificateRevoked()
|
||||
raise VerifyError('Certificate is present in CRL, verification failed')
|
||||
|
||||
def verify_cert_chain(self, cert: x509.Certificate, max_depth: int = 100):
|
||||
"""Verify if a given certificate's signature chain can be traced back to the root CA of this
|
||||
@@ -147,14 +150,14 @@ class CertificateSet:
|
||||
check_signed(c, self.root_cert)
|
||||
return
|
||||
parent_cert = self.intermediate_certs.get(aki, None)
|
||||
if not parent_cert:
|
||||
raise x509_err.MissingIntermediateCert(b2h(aki))
|
||||
if not aki:
|
||||
raise VerifyError('Could not find intermediate certificate for AuthKeyId %s' % b2h(aki))
|
||||
check_signed(c, parent_cert)
|
||||
# if we reach here, we passed (no exception raised)
|
||||
c = parent_cert
|
||||
depth += 1
|
||||
if depth > max_depth:
|
||||
raise x509_err.MaxDepthExceeded(max_depth, depth)
|
||||
raise VerifyError('Maximum depth %u exceeded while verifying certificate chain' % max_depth)
|
||||
|
||||
|
||||
def ecdsa_dss_to_tr03111(sig: bytes) -> bytes:
|
||||
@@ -186,7 +189,7 @@ class CertAndPrivkey:
|
||||
def ecdsa_sign(self, plaintext: bytes) -> bytes:
|
||||
"""Sign some input-data using an ECDSA signature compliant with SGP.22,
|
||||
which internally refers to Global Platform 2.2 Annex E, which in turn points
|
||||
to BSI TS-03111 which states "concatenated raw R + S values". """
|
||||
to BSI TS-03111 which states "concatengated raw R + S values". """
|
||||
sig = self.priv_key.sign(plaintext, ec.ECDSA(hashes.SHA256()))
|
||||
# convert from DER format to BSI TR-03111; first get long integers; then convert those to bytes
|
||||
return ecdsa_dss_to_tr03111(sig)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
"""X.509 certificate verification exceptions for GSMA eSIM."""
|
||||
|
||||
class VerifyError(Exception):
|
||||
"""Base class for certificate verification errors."""
|
||||
pass
|
||||
|
||||
|
||||
class MissingIntermediateCert(VerifyError):
|
||||
"""Raised when an intermediate certificate in the chain cannot be found."""
|
||||
def __init__(self, auth_key_id: str):
|
||||
self.auth_key_id = auth_key_id
|
||||
super().__init__(f'Could not find intermediate certificate for AuthKeyId {auth_key_id}')
|
||||
|
||||
|
||||
class CertificateRevoked(VerifyError):
|
||||
"""Raised when a certificate is found in the CRL."""
|
||||
def __init__(self, cert_serial: str = None):
|
||||
self.cert_serial = cert_serial
|
||||
msg = 'Certificate is present in CRL, verification failed'
|
||||
if cert_serial:
|
||||
msg += f' (serial: {cert_serial})'
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class MaxDepthExceeded(VerifyError):
|
||||
"""Raised when certificate chain depth exceeds the maximum allowed."""
|
||||
def __init__(self, max_depth: int, actual_depth: int):
|
||||
self.max_depth = max_depth
|
||||
self.actual_depth = actual_depth
|
||||
super().__init__(f'Maximum depth {max_depth} exceeded while verifying certificate chain (actual: {actual_depth})')
|
||||
|
||||
|
||||
class SignatureVerification(VerifyError):
|
||||
"""Raised when certificate signature verification fails."""
|
||||
def __init__(self, cert_subject: str = None, signer_subject: str = None):
|
||||
self.cert_subject = cert_subject
|
||||
self.signer_subject = signer_subject
|
||||
msg = 'Certificate signature verification failed'
|
||||
if cert_subject and signer_subject:
|
||||
msg += f': {cert_subject} not signed by {signer_subject}'
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
class InvalidCertificate(VerifyError):
|
||||
"""Raised when a certificate is invalid (missing required fields, wrong type, etc)."""
|
||||
def __init__(self, reason: str):
|
||||
self.reason = reason
|
||||
super().__init__(f'Invalid certificate: {reason}')
|
||||
|
||||
|
||||
class CertificateExpired(VerifyError):
|
||||
"""Raised when a certificate has expired."""
|
||||
def __init__(self, cert_subject: str = None):
|
||||
self.cert_subject = cert_subject
|
||||
msg = 'Certificate has expired'
|
||||
if cert_subject:
|
||||
msg += f': {cert_subject}'
|
||||
super().__init__(msg)
|
||||
@@ -34,7 +34,6 @@ from osmocom.construct import *
|
||||
from pySim.exceptions import SwMatchError
|
||||
from pySim.utils import Hexstr, SwHexstr, SwMatchstr
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.ts_102_221 import CardProfileUICC
|
||||
import pySim.global_platform
|
||||
|
||||
# SGP.02 Section 2.2.2
|
||||
@@ -120,7 +119,7 @@ class SetDefaultDpAddress(BER_TLV_IE, tag=0xbf3f, nested=[DefaultDpAddress, SetD
|
||||
|
||||
# SGP.22 Section 5.7.7: GetEUICCChallenge
|
||||
class EuiccChallenge(BER_TLV_IE, tag=0x80):
|
||||
_construct = Bytes(16)
|
||||
_construct = HexAdapter(Bytes(16))
|
||||
class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
|
||||
pass
|
||||
|
||||
@@ -128,7 +127,7 @@ class GetEuiccChallenge(BER_TLV_IE, tag=0xbf2e, nested=[EuiccChallenge]):
|
||||
class SVN(BER_TLV_IE, tag=0x82):
|
||||
_construct = VersionType
|
||||
class SubjectKeyIdentifier(BER_TLV_IE, tag=0x04):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class EuiccCiPkiListForVerification(BER_TLV_IE, tag=0xa9, nested=[SubjectKeyIdentifier]):
|
||||
pass
|
||||
class EuiccCiPkiListForSigning(BER_TLV_IE, tag=0xaa, nested=[SubjectKeyIdentifier]):
|
||||
@@ -140,15 +139,15 @@ class ProfileVersion(BER_TLV_IE, tag=0x81):
|
||||
class EuiccFirmwareVer(BER_TLV_IE, tag=0x83):
|
||||
_construct = VersionType
|
||||
class ExtCardResource(BER_TLV_IE, tag=0x84):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class UiccCapability(BER_TLV_IE, tag=0x85):
|
||||
_construct = GreedyBytes # FIXME
|
||||
_construct = HexAdapter(GreedyBytes) # FIXME
|
||||
class TS102241Version(BER_TLV_IE, tag=0x86):
|
||||
_construct = VersionType
|
||||
class GlobalPlatformVersion(BER_TLV_IE, tag=0x87):
|
||||
_construct = VersionType
|
||||
class RspCapability(BER_TLV_IE, tag=0x88):
|
||||
_construct = GreedyBytes # FIXME
|
||||
_construct = HexAdapter(GreedyBytes) # FIXME
|
||||
class EuiccCategory(BER_TLV_IE, tag=0x8b):
|
||||
_construct = Enum(Int8ub, other=0, basicEuicc=1, mediumEuicc=2, contactlessEuicc=3)
|
||||
class PpVersion(BER_TLV_IE, tag=0x04):
|
||||
@@ -211,7 +210,7 @@ class TagList(BER_TLV_IE, tag=0x5c):
|
||||
class ProfileInfoListReq(BER_TLV_IE, tag=0xbf2d, nested=[TagList]): # FIXME: SearchCriteria
|
||||
pass
|
||||
class IsdpAid(BER_TLV_IE, tag=0x4f):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class ProfileState(BER_TLV_IE, tag=0x9f70):
|
||||
_construct = Enum(Int8ub, disabled=0, enabled=1)
|
||||
class ProfileNickname(BER_TLV_IE, tag=0x90):
|
||||
@@ -268,20 +267,9 @@ class DeleteProfileReq(BER_TLV_IE, tag=0xbf33, nested=[IsdpAid, Iccid]):
|
||||
class DeleteProfileResp(BER_TLV_IE, tag=0xbf33, nested=[DeleteResult]):
|
||||
pass
|
||||
|
||||
# SGP.22 Section 5.7.19: EuiccMemoryReset
|
||||
class ResetOptions(BER_TLV_IE, tag=0x82):
|
||||
_construct = FlagsEnum(Byte, deleteOperationalProfiles=0x80, deleteFieldLoadedTestProfiles=0x40,
|
||||
resetDefaultSmdpAddress=0x20)
|
||||
class ResetResult(BER_TLV_IE, tag=0x80):
|
||||
_construct = Enum(Int8ub, ok=0, nothingToDelete=1, undefinedError=127)
|
||||
class EuiccMemoryResetReq(BER_TLV_IE, tag=0xbf34, nested=[ResetOptions]):
|
||||
pass
|
||||
class EuiccMemoryResetResp(BER_TLV_IE, tag=0xbf34, nested=[ResetResult]):
|
||||
pass
|
||||
|
||||
# SGP.22 Section 5.7.20 GetEID
|
||||
class EidValue(BER_TLV_IE, tag=0x5a):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class GetEuiccData(BER_TLV_IE, tag=0xbf3e, nested=[TagList, EidValue]):
|
||||
pass
|
||||
|
||||
@@ -373,7 +361,7 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
ged_cmd = GetEuiccData(children=[TagList(decoded=[0x5A])])
|
||||
ged = CardApplicationISDR.store_data_tlv(scc, ged_cmd, GetEuiccData)
|
||||
d = ged.to_dict()
|
||||
return b2h(flatten_dict_lists(d['get_euicc_data'])['eid_value'])
|
||||
return flatten_dict_lists(d['get_euicc_data'])['eid_value']
|
||||
|
||||
def decode_select_response(self, data_hex: Hexstr) -> object:
|
||||
t = FciTemplate()
|
||||
@@ -515,30 +503,6 @@ class CardApplicationISDR(pySim.global_platform.CardApplicationSD):
|
||||
d = dp.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['delete_profile_resp']))
|
||||
|
||||
mem_res_parser = argparse.ArgumentParser()
|
||||
mem_res_parser.add_argument('--delete-operational', action='store_true',
|
||||
help='Delete all operational profiles')
|
||||
mem_res_parser.add_argument('--delete-test-field-installed', action='store_true',
|
||||
help='Delete all test profiles, except pre-installed ones')
|
||||
mem_res_parser.add_argument('--reset-smdp-address', action='store_true',
|
||||
help='Reset the SM-DP+ address')
|
||||
|
||||
@cmd2.with_argparser(mem_res_parser)
|
||||
def do_euicc_memory_reset(self, opts):
|
||||
"""Perform an ES10c eUICCMemoryReset function. This will permanently delete the selected subset of
|
||||
profiles from the eUICC."""
|
||||
flags = {}
|
||||
if opts.delete_operational:
|
||||
flags['deleteOperationalProfiles'] = True
|
||||
if opts.delete_test_field_installed:
|
||||
flags['deleteFieldLoadedTestProfiles'] = True
|
||||
if opts.reset_smdp_address:
|
||||
flags['resetDefaultSmdpAddress'] = True
|
||||
|
||||
mr_cmd = EuiccMemoryResetReq(children=[ResetOptions(decoded=flags)])
|
||||
mr = CardApplicationISDR.store_data_tlv(self._cmd.lchan.scc, mr_cmd, EuiccMemoryResetResp)
|
||||
d = mr.to_dict()
|
||||
self._cmd.poutput_json(flatten_dict_lists(d['euicc_memory_reset_resp']))
|
||||
|
||||
def do_get_eid(self, _opts):
|
||||
"""Perform an ES10c GetEID function."""
|
||||
@@ -592,43 +556,3 @@ class CardApplicationECASD(pySim.global_platform.CardApplicationSD):
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
pass
|
||||
|
||||
class CardProfileEuiccSGP32(CardProfileUICC):
|
||||
ORDER = 5
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='IoT eUICC (SGP.32)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
# try a command only supported by SGP.32
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ISD_R)
|
||||
CardApplicationISDR.store_data_tlv(scc, GetCertsReq(), GetCertsResp)
|
||||
|
||||
class CardProfileEuiccSGP22(CardProfileUICC):
|
||||
ORDER = 6
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='Consumer eUICC (SGP.22)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
# try to read EID from ISD-R
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ISD_R)
|
||||
eid = CardApplicationISDR.get_eid(scc)
|
||||
# TODO: Store EID identity?
|
||||
|
||||
class CardProfileEuiccSGP02(CardProfileUICC):
|
||||
ORDER = 7
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='M2M eUICC (SGP.02)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ECASD)
|
||||
scc.get_data(0x5a)
|
||||
# TODO: Store EID identity?
|
||||
|
||||
@@ -301,7 +301,7 @@ class CardFile:
|
||||
|
||||
@staticmethod
|
||||
def export(as_json: bool, lchan):
|
||||
r"""
|
||||
"""
|
||||
Export file contents in the form of commandline script. This method is meant to be overloaded by a subclass in
|
||||
case any exportable contents are present. The generated script may contain multiple command lines separated by
|
||||
line breaks ("\n"), where the last commandline shall have no line break at the end
|
||||
@@ -661,7 +661,7 @@ class TransparentEF(CardEF):
|
||||
filename = '%s/file' % dirname
|
||||
# write existing data as JSON to file
|
||||
with open(filename, 'w') as text_file:
|
||||
json.dump(orig_json, text_file, indent=4, cls=JsonEncoder)
|
||||
json.dump(orig_json, text_file, indent=4)
|
||||
# run a text editor
|
||||
self._cmd.run_editor(filename)
|
||||
with open(filename, 'r') as text_file:
|
||||
@@ -963,7 +963,7 @@ class LinFixedEF(CardEF):
|
||||
filename = '%s/file' % dirname
|
||||
# write existing data as JSON to file
|
||||
with open(filename, 'w') as text_file:
|
||||
json.dump(orig_json, text_file, indent=4, cls=JsonEncoder)
|
||||
json.dump(orig_json, text_file, indent=4)
|
||||
# run a text editor
|
||||
self._cmd.run_editor(filename)
|
||||
with open(filename, 'r') as text_file:
|
||||
@@ -1224,13 +1224,6 @@ class TransRecEF(TransparentEF):
|
||||
Returns:
|
||||
abstract_data; dict representing the decoded data
|
||||
"""
|
||||
|
||||
# The record data length should always be equal or at least greater than the record length defined for the
|
||||
# TransRecEF. Short records may be occur when the length of the underlying TransparentEF is not a multiple
|
||||
# of the TransRecEF record length.
|
||||
if len(raw_hex_data) // 2 < self.__get_rec_len():
|
||||
return {'raw': raw_hex_data}
|
||||
|
||||
method = getattr(self, '_decode_record_hex', None)
|
||||
if callable(method):
|
||||
return method(raw_hex_data)
|
||||
@@ -1258,11 +1251,6 @@ class TransRecEF(TransparentEF):
|
||||
Returns:
|
||||
abstract_data; dict representing the decoded data
|
||||
"""
|
||||
|
||||
# See comment in decode_record_hex (above)
|
||||
if len(raw_bin_data) < self.__get_rec_len():
|
||||
return {'raw': b2h(raw_bin_data)}
|
||||
|
||||
method = getattr(self, '_decode_record_bin', None)
|
||||
if callable(method):
|
||||
return method(raw_bin_data)
|
||||
@@ -1542,7 +1530,8 @@ class CardModel(abc.ABC):
|
||||
"""Test if given card matches this model."""
|
||||
card_atr = scc.get_atr()
|
||||
for atr in cls._atrs:
|
||||
if atr == card_atr:
|
||||
atr_bin = toBytes(atr)
|
||||
if atr_bin == card_atr:
|
||||
print("Detected CardModel:", cls.__name__)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -17,7 +17,6 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import io
|
||||
from copy import deepcopy
|
||||
from typing import Optional, List, Dict, Tuple
|
||||
from construct import Optional as COptional
|
||||
@@ -30,11 +29,9 @@ from osmocom.construct import *
|
||||
from pySim.utils import ResTuple
|
||||
from pySim.card_key_provider import card_key_provider_get_field
|
||||
from pySim.global_platform.scp import SCP02, SCP03
|
||||
from pySim.global_platform.install_param import gen_install_parameters
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile import CardProfile
|
||||
from pySim.ota import SimFileAccessAndToolkitAppSpecParams
|
||||
from pySim.javacard import CapFile
|
||||
|
||||
# GPCS Table 11-48 Load Parameter Tags
|
||||
class NonVolatileCodeMinMemoryReq(BER_TLV_IE, tag=0xC6):
|
||||
@@ -318,7 +315,7 @@ class CurrentSecurityLevel(BER_TLV_IE, tag=0xd3):
|
||||
|
||||
# GlobalPlatform v2.3.1 Section 11.3.3.1.3
|
||||
class ApplicationAID(BER_TLV_IE, tag=0x4f):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class ApplicationTemplate(BER_TLV_IE, tag=0x61, ntested=[ApplicationAID]):
|
||||
pass
|
||||
class ListOfApplications(BER_TLV_IE, tag=0x2f00, nested=[ApplicationTemplate]):
|
||||
@@ -425,10 +422,10 @@ class FciTemplate(BER_TLV_IE, tag=0x6f, nested=FciTemplateNestedList):
|
||||
pass
|
||||
|
||||
class IssuerIdentificationNumber(BER_TLV_IE, tag=0x42):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class CardImageNumber(BER_TLV_IE, tag=0x45):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class SequenceCounterOfDefaultKvn(BER_TLV_IE, tag=0xc1):
|
||||
_construct = GreedyInteger()
|
||||
@@ -467,7 +464,8 @@ class LifeCycleState(BER_TLV_IE, tag=0x9f70):
|
||||
|
||||
# Section 11.4.3.1 Table 11-36 + Section 11.1.2
|
||||
class Privileges(BER_TLV_IE, tag=0xc5):
|
||||
_construct = FlagsEnum(StripTrailerAdapter(GreedyBytes, 3, steps = [1, 3]),
|
||||
# we only support 3-byte encoding. Can't use StripTrailerAdapter as length==2 is not permitted. sigh.
|
||||
_construct = FlagsEnum(Int24ub,
|
||||
security_domain=0x800000, dap_verification=0x400000,
|
||||
delegated_management=0x200000, card_lock=0x100000, card_terminate=0x080000,
|
||||
card_reset=0x040000, cvm_management=0x020000,
|
||||
@@ -487,7 +485,7 @@ class ImplicitSelectionParameter(BER_TLV_IE, tag=0xcf):
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class ExecutableLoadFileAID(BER_TLV_IE, tag=0xc4):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class ExecutableLoadFileVersionNumber(BER_TLV_IE, tag=0xce):
|
||||
@@ -495,15 +493,15 @@ class ExecutableLoadFileVersionNumber(BER_TLV_IE, tag=0xce):
|
||||
# specification. It shall consist of the version information contained in the original Load File: on a
|
||||
# Java Card based card, this version number represents the major and minor version attributes of the
|
||||
# original Load File Data Block.
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class ExecutableModuleAID(BER_TLV_IE, tag=0x84):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class AssociatedSecurityDomainAID(BER_TLV_IE, tag=0xcc):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# Section 11.4.3.1 Table 11-36
|
||||
class GpRegistryRelatedData(BER_TLV_IE, tag=0xe3, nested=[ApplicationAID, LifeCycleState, Privileges,
|
||||
@@ -640,8 +638,8 @@ class ADF_SD(CardADF):
|
||||
|
||||
# Table 11-68: Key Data Field - Format 1 (Basic Format)
|
||||
KeyDataBasic = GreedyRange(Struct('key_type'/KeyType,
|
||||
'kcb'/Prefixed(Int8ub, GreedyBytes),
|
||||
'kcv'/Prefixed(Int8ub, GreedyBytes)))
|
||||
'kcb'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'kcv'/HexAdapter(Prefixed(Int8ub, GreedyBytes))))
|
||||
|
||||
def put_key(self, old_kvn:int, kvn: int, kid: int, key_dict: dict) -> bytes:
|
||||
"""Perform the GlobalPlatform PUT KEY command in order to store a new key on the card.
|
||||
@@ -704,7 +702,7 @@ class ADF_SD(CardADF):
|
||||
def set_status(self, scope:str, status:str, aid:Hexstr = ''):
|
||||
SetStatus = Struct(Const(0x80, Byte), Const(0xF0, Byte),
|
||||
'scope'/SetStatusScope, 'status'/CLifeCycleState,
|
||||
'aid'/Prefixed(Int8ub, COptional(GreedyBytes)))
|
||||
'aid'/HexAdapter(Prefixed(Int8ub, COptional(GreedyBytes))))
|
||||
apdu = build_construct(SetStatus, {'scope':scope, 'status':status, 'aid':aid})
|
||||
_data, _sw = self._cmd.lchan.scc.send_apdu_checksw(b2h(apdu))
|
||||
|
||||
@@ -726,7 +724,7 @@ class ADF_SD(CardADF):
|
||||
inst_inst_parser.add_argument('--application-aid', type=is_hexstr, required=True,
|
||||
help='Application AID')
|
||||
inst_inst_parser.add_argument('--install-parameters', type=is_hexstr, default='',
|
||||
help='Install Parameters (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
|
||||
help='Install Parameters')
|
||||
inst_inst_parser.add_argument('--privilege', action='append', dest='privileges', default=[],
|
||||
choices=list(Privileges._construct.flags.keys()),
|
||||
help='Privilege granted to newly installed Application')
|
||||
@@ -738,12 +736,12 @@ class ADF_SD(CardADF):
|
||||
@cmd2.with_argparser(inst_inst_parser)
|
||||
def do_install_for_install(self, opts):
|
||||
"""Perform GlobalPlatform INSTALL [for install] command in order to install an application."""
|
||||
InstallForInstallCD = Struct('load_file_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
'module_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
'application_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
InstallForInstallCD = Struct('load_file_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'module_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'application_aid'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'privileges'/Prefixed(Int8ub, Privileges._construct),
|
||||
'install_parameters'/Prefixed(Int8ub, GreedyBytes),
|
||||
'install_token'/Prefixed(Int8ub, GreedyBytes))
|
||||
'install_parameters'/HexAdapter(Prefixed(Int8ub, GreedyBytes)),
|
||||
'install_token'/HexAdapter(Prefixed(Int8ub, GreedyBytes)))
|
||||
p1 = 0x04
|
||||
if opts.make_selectable:
|
||||
p1 |= 0x08
|
||||
@@ -753,31 +751,6 @@ class ADF_SD(CardADF):
|
||||
ifi_bytes = build_construct(InstallForInstallCD, decoded)
|
||||
self.install(p1, 0x00, b2h(ifi_bytes))
|
||||
|
||||
inst_load_parser = argparse.ArgumentParser()
|
||||
inst_load_parser.add_argument('--load-file-aid', type=is_hexstr, required=True,
|
||||
help='AID of the loded file')
|
||||
inst_load_parser.add_argument('--security-domain-aid', type=is_hexstr, default='',
|
||||
help='AID of the Security Domain into which the file shalle be added')
|
||||
inst_load_parser.add_argument('--load-file-hash', type=is_hexstr, default='',
|
||||
help='Load File Data Block Hash (GPC_SPE_034, section C.2)')
|
||||
inst_load_parser.add_argument('--load-parameters', type=is_hexstr, default='',
|
||||
help='Load Parameters (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
|
||||
inst_load_parser.add_argument('--load-token', type=is_hexstr, default='',
|
||||
help='Load Token (GPC_SPE_034, section C.4.1)')
|
||||
|
||||
@cmd2.with_argparser(inst_load_parser)
|
||||
def do_install_for_load(self, opts):
|
||||
"""Perform GlobalPlatform INSTALL [for load] command in order to prepare to load an application."""
|
||||
if opts.load_token != '' and opts.load_file_hash == '':
|
||||
raise ValueError('Load File Data Block Hash is mandatory if a Load Token is present')
|
||||
InstallForLoadCD = Struct('load_file_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
'security_domain_aid'/Prefixed(Int8ub, GreedyBytes),
|
||||
'load_file_hash'/Prefixed(Int8ub, GreedyBytes),
|
||||
'load_parameters'/Prefixed(Int8ub, GreedyBytes),
|
||||
'load_token'/Prefixed(Int8ub, GreedyBytes))
|
||||
ifl_bytes = build_construct(InstallForLoadCD, vars(opts))
|
||||
self.install(0x02, 0x00, b2h(ifl_bytes))
|
||||
|
||||
def install(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
|
||||
cmd_hex = "80E6%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
|
||||
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
@@ -818,101 +791,11 @@ class ADF_SD(CardADF):
|
||||
self.delete(0x00, p2, cmd)
|
||||
|
||||
def delete(self, p1:int, p2:int, data:Hexstr) -> ResTuple:
|
||||
cmd_hex = "80E4%02x%02x%02x%s00" % (p1, p2, len(data)//2, data)
|
||||
cmd_hex = "80E4%02x%02x%02x%s" % (p1, p2, len(data)//2, data)
|
||||
return self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
|
||||
load_parser = argparse.ArgumentParser()
|
||||
load_parser_from_grp = load_parser.add_mutually_exclusive_group(required=True)
|
||||
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')
|
||||
|
||||
@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))
|
||||
elif opts.from_file is not None:
|
||||
self.load(opts.from_file.read())
|
||||
elif opts.from_cap_file is not None:
|
||||
cap = CapFile(opts.from_cap_file)
|
||||
self.load(cap.get_loadfile())
|
||||
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?
|
||||
# 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 vaious chunks to the card
|
||||
total_size = len(remainder)
|
||||
block_nr = 0
|
||||
while len(remainder):
|
||||
block = remainder[:chunk_len]
|
||||
remainder = remainder[chunk_len:]
|
||||
# build LOAD command APDU according to GPC_SPE_034 section 11.6.2 / Table 11-56
|
||||
p1 = 0x00 if len(remainder) else 0x80
|
||||
p2 = block_nr % 256
|
||||
block_nr += 1
|
||||
cmd_hex = "80E8%02x%02x%02x%s00" % (p1, p2, len(block), b2h(block))
|
||||
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
self._cmd.poutput("Loaded a total of %u bytes in %u blocks. Don't forget install_for_install (and make selectable) now!" % (total_size, block_nr))
|
||||
|
||||
install_cap_parser = argparse.ArgumentParser()
|
||||
install_cap_parser.add_argument('cap_file', type=str, metavar='FILE',
|
||||
help='JAVA-CARD CAP file to install')
|
||||
install_cap_parser_inst_prm_g = install_cap_parser.add_mutually_exclusive_group()
|
||||
install_cap_parser_inst_prm_g.add_argument('--install-parameters', type=is_hexstr, default=None,
|
||||
help='install Parameters (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
|
||||
install_cap_parser_inst_prm_g_grp = install_cap_parser_inst_prm_g.add_argument_group()
|
||||
install_cap_parser_inst_prm_g_grp.add_argument('--install-parameters-volatile-memory-quota',
|
||||
type=int, default=None,
|
||||
help='volatile memory quota (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
|
||||
install_cap_parser_inst_prm_g_grp.add_argument('--install-parameters-non-volatile-memory-quota',
|
||||
type=int, default=None,
|
||||
help='non volatile memory quota (GPC_SPE_034, section 11.5.2.3.7, table 11-49)')
|
||||
install_cap_parser_inst_prm_g_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)')
|
||||
|
||||
@cmd2.with_argparser(install_cap_parser)
|
||||
def do_install_cap(self, opts):
|
||||
"""Perform a .cap file installation using GlobalPlatform LOAD and INSTALL commands."""
|
||||
|
||||
self._cmd.poutput("loading cap file: %s ..." % opts.cap_file)
|
||||
cap = CapFile(opts.cap_file)
|
||||
|
||||
security_domain_aid = self._cmd.lchan.selected_file.aid
|
||||
load_file = cap.get_loadfile()
|
||||
load_file_aid = cap.get_loadfile_aid()
|
||||
module_aid = cap.get_applet_aid()
|
||||
application_aid = module_aid
|
||||
if opts.install_parameters:
|
||||
install_parameters = opts.install_parameters;
|
||||
else:
|
||||
install_parameters = gen_install_parameters(opts.install_parameters_non_volatile_memory_quota,
|
||||
opts.install_parameters_volatile_memory_quota,
|
||||
opts.install_parameters_stk)
|
||||
self._cmd.poutput("parameters:")
|
||||
self._cmd.poutput(" security-domain-aid: %s" % security_domain_aid)
|
||||
self._cmd.poutput(" load-file: %u bytes" % len(load_file))
|
||||
self._cmd.poutput(" load-file-aid: %s" % load_file_aid)
|
||||
self._cmd.poutput(" module-aid: %s" % module_aid)
|
||||
self._cmd.poutput(" application-aid: %s" % application_aid)
|
||||
self._cmd.poutput(" install-parameters: %s" % install_parameters)
|
||||
|
||||
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._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))
|
||||
self._cmd.poutput("done.")
|
||||
|
||||
est_scp02_parser = argparse.ArgumentParser()
|
||||
est_scp02_parser.add_argument('--key-ver', type=auto_uint8, default=0, help='Key Version Number (KVN)')
|
||||
est_scp02_parser.add_argument('--key-ver', type=auto_uint8, required=True, help='Key Version Number (KVN)')
|
||||
est_scp02_parser.add_argument('--host-challenge', type=is_hexstr,
|
||||
help='Hard-code the host challenge; default: random')
|
||||
est_scp02_parser.add_argument('--security-level', type=auto_uint8, default=0x01,
|
||||
@@ -1014,12 +897,17 @@ class CardApplicationISD(CardApplicationSD):
|
||||
super().__init__(aid=aid, name='ADF.ISD', desc='Issuer Security Domain')
|
||||
self.adf.scp_key_identity = 'ICCID'
|
||||
|
||||
#class CardProfileGlobalPlatform(CardProfile):
|
||||
# ORDER = 23
|
||||
#
|
||||
# def __init__(self, name='GlobalPlatform'):
|
||||
# super().__init__(name, desc='GlobalPlatfomr 2.1.1', cla=['00','80','84'], sw=sw_table)
|
||||
|
||||
|
||||
class GpCardKeyset:
|
||||
"""A single set of GlobalPlatform card keys and the associated KVN."""
|
||||
def __init__(self, kvn: int, enc: bytes, mac: bytes, dek: bytes):
|
||||
# The Key Version Number is an 8 bit integer number, where 0 refers to the first available key,
|
||||
# see also: GPC_SPE_034, section E.5.1.3
|
||||
assert 0 <= kvn < 256
|
||||
assert 0 < kvn < 256
|
||||
assert len(enc) == len(mac) == len(dek)
|
||||
self.kvn = kvn
|
||||
self.enc = enc
|
||||
|
||||
@@ -17,10 +17,9 @@ Also known as SCP81 for SIM/USIM/UICC/eUICC/eSIM OTA.
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from construct import Struct, Int8ub, Int16ub, GreedyString, BytesInteger
|
||||
from construct import Struct, Int8ub, Int16ub, Bytes, GreedyBytes, GreedyString, BytesInteger
|
||||
from construct import this, len_, Rebuild, Const
|
||||
from construct import Optional as COptional
|
||||
from osmocom.construct import Bytes, GreedyBytes
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
from pySim import cat
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
# GlobalPlatform install parameter generator
|
||||
#
|
||||
# (C) 2024 by Sysmocom s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import *
|
||||
from osmocom.tlv import *
|
||||
|
||||
class AppSpecificParams(BER_TLV_IE, tag=0xC9):
|
||||
# GPD_SPE_013, table 11-49
|
||||
_construct = GreedyBytes
|
||||
|
||||
class VolatileMemoryQuota(BER_TLV_IE, tag=0xC7):
|
||||
# GPD_SPE_013, table 11-49
|
||||
_construct = StripHeaderAdapter(GreedyBytes, 4, steps = [2,4])
|
||||
|
||||
class NonVolatileMemoryQuota(BER_TLV_IE, tag=0xC8):
|
||||
# GPD_SPE_013, table 11-49
|
||||
_construct = StripHeaderAdapter(GreedyBytes, 4, steps = [2,4])
|
||||
|
||||
class StkParameter(BER_TLV_IE, tag=0xCA):
|
||||
# GPD_SPE_013, table 11-49
|
||||
# ETSI TS 102 226, section 8.2.1.3.2.1
|
||||
_construct = GreedyBytes
|
||||
|
||||
class SystemSpecificParams(BER_TLV_IE, tag=0xEF, nested=[VolatileMemoryQuota, NonVolatileMemoryQuota, StkParameter]):
|
||||
# GPD_SPE_013 v1.1 Table 6-5
|
||||
pass
|
||||
|
||||
class InstallParams(TLV_IE_Collection, nested=[AppSpecificParams, SystemSpecificParams]):
|
||||
# GPD_SPE_013, table 11-49
|
||||
pass
|
||||
|
||||
def gen_install_parameters(non_volatile_memory_quota:int, volatile_memory_quota:int, stk_parameter:str):
|
||||
|
||||
# GPD_SPE_013, table 11-49
|
||||
|
||||
#Mandatory
|
||||
install_params = InstallParams()
|
||||
install_params_dict = [{'app_specific_params': None}]
|
||||
|
||||
#Conditional
|
||||
if non_volatile_memory_quota and volatile_memory_quota and stk_parameter:
|
||||
system_specific_params = []
|
||||
#Optional
|
||||
if non_volatile_memory_quota:
|
||||
system_specific_params += [{'non_volatile_memory_quota': non_volatile_memory_quota}]
|
||||
#Optional
|
||||
if volatile_memory_quota:
|
||||
system_specific_params += [{'volatile_memory_quota': volatile_memory_quota}]
|
||||
#Optional
|
||||
if stk_parameter:
|
||||
system_specific_params += [{'stk_parameter': stk_parameter}]
|
||||
install_params_dict += [{'system_specific_params': system_specific_params}]
|
||||
|
||||
install_params.from_dict(install_params_dict)
|
||||
return b2h(install_params.to_bytes())
|
||||
@@ -20,9 +20,8 @@ import logging
|
||||
from typing import Optional
|
||||
from Cryptodome.Cipher import DES3, DES
|
||||
from Cryptodome.Util.strxor import strxor
|
||||
from construct import Struct, Int8ub, Int16ub, Const
|
||||
from construct import Struct, Bytes, Int8ub, Int16ub, Const
|
||||
from construct import Optional as COptional
|
||||
from osmocom.construct import Bytes
|
||||
from osmocom.utils import b2h
|
||||
from osmocom.tlv import bertlv_parse_len, bertlv_encode_len
|
||||
from pySim.utils import parse_command_apdu
|
||||
@@ -114,39 +113,7 @@ CLA_SM = 0x04
|
||||
class SCP(SecureChannel, abc.ABC):
|
||||
"""Abstract base class containing some common interface + functionality for SCP protocols."""
|
||||
def __init__(self, card_keys: 'GpCardKeyset', lchan_nr: int = 0):
|
||||
|
||||
# Spec references that explain KVN ranges:
|
||||
# TS 102 225 Annex A.1 states KVN 0x01..0x0F shall be used for SCP80
|
||||
# GPC_GUI_003 states
|
||||
# * For the Issuer Security Domain, this is initially Key Version Number 'FF' which has been deliberately
|
||||
# chosen to be outside of the allowable range ('01' to '7F') for a Key Version Number.
|
||||
# * It is logical that the initial keys in the Issuer Security Domain be replaced by an initial issuer Key
|
||||
# Version Number in the range '01' to '6F'.
|
||||
# * Key Version Numbers '70' to '72' and '74' to '7F' are reserved for future use.
|
||||
# * On an implementation supporting Supplementary Security Domains, the RSA public key with a Key Version
|
||||
# Number '73' and a Key Identifier of '01' has the following functionality in a Supplementary Security
|
||||
# Domain with the DAP Verification privilege [...]
|
||||
# GPC_GUI_010 V1.0.1 Section 6 states
|
||||
# * Key Version number range ('20' to '2F') is reserved for SCP02
|
||||
# * Key Version 'FF' is reserved for use by an Issuer Security Domain supporting SCP02, and cannot be used
|
||||
# for SCP80. This initial key set shall be replaced by a key set with a Key Version Number in the
|
||||
# ('20' to '2F') range.
|
||||
# * Key Version number range ('01' to '0F') is reserved for SCP80
|
||||
# * Key Version number '70' with Key Identifier '01' is reserved for the Token Key, which is either a RSA
|
||||
# public key or a DES key
|
||||
# * Key Version number '71' with Key Identifier '01' is reserved for the Receipt Key, which is a DES key
|
||||
# * Key Version Number '11' is reserved for DAP as specified in ETSI TS 102 226 [2]
|
||||
# * Key Version Number '73' with Key Identifier '01' is reserved for the DAP verification key as specified
|
||||
# in sections 3.3.3 and 4 of [4], which is either an RSA public key or DES key
|
||||
# * Key Version Number '74' is reserved for the CASD Keys (cf. section 9.2)
|
||||
# * Key Version Number '75' with Key Identifier '01' is reserved for the key used to decipher the Ciphered
|
||||
# Load File Data Block described in section 4.8 of [5].
|
||||
|
||||
if card_keys.kvn == 0:
|
||||
# Key Version Number 0x00 refers to the first available key, so we won't carry out
|
||||
# a range check in this case. See also: GPC_SPE_034, section E.5.1.3
|
||||
pass
|
||||
elif hasattr(self, 'kvn_range'):
|
||||
if hasattr(self, 'kvn_range'):
|
||||
if not card_keys.kvn in range(self.kvn_range[0], self.kvn_range[1]+1):
|
||||
raise ValueError('%s cannot be used with KVN outside range 0x%02x..0x%02x' %
|
||||
(self.__class__.__name__, self.kvn_range[0], self.kvn_range[1]))
|
||||
@@ -257,9 +224,8 @@ class SCP02(SCP):
|
||||
|
||||
constr_iur = Struct('key_div_data'/Bytes(10), 'key_ver'/Int8ub, Const(b'\x02'),
|
||||
'seq_counter'/Int16ub, 'card_challenge'/Bytes(6), 'card_cryptogram'/Bytes(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]]
|
||||
# The 0x70 is a non-spec special-case of sysmoISIM-SJA2/SJA5 and possibly more sysmocom products
|
||||
kvn_ranges = [[0x20, 0x2f], [0x70, 0x70]]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.overhead = 8
|
||||
|
||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from construct import GreedyString
|
||||
from construct import GreedyBytes, GreedyString
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
|
||||
|
||||
@@ -1,142 +1,19 @@
|
||||
# JavaCard related utilities
|
||||
#
|
||||
# (C) 2024 by Sysmocom s.f.m.c. GmbH
|
||||
# All Rights Reserved
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import zipfile
|
||||
import struct
|
||||
import sys
|
||||
import io
|
||||
from osmocom.utils import b2h, Hexstr
|
||||
from construct import Struct, Array, this, Int32ub, Int16ub, Int8ub
|
||||
from osmocom.construct import *
|
||||
from osmocom.tlv import *
|
||||
from construct import Optional as COptional
|
||||
|
||||
def ijc_to_cap(in_file: io.IOBase, out_zip: zipfile.ZipFile, p : str = "foo"):
|
||||
"""Convert an ICJ (Interoperable Java Card) file [back] to a CAP file.
|
||||
example usage:
|
||||
with io.open(sys.argv[1],"rb") as f, zipfile.ZipFile(sys.argv[2], "wb") as z:
|
||||
ijc_to_cap(f, z)
|
||||
"""
|
||||
TAGS = ["Header", "Directory", "Applet", "Import", "ConstantPool", "Class", "Method", "StaticField", "RefLocation",
|
||||
"Export", "Descriptor", "Debug"]
|
||||
"""Convert an ICJ (Interoperable Java Card) file [back] to a CAP file."""
|
||||
TAGS = ["Header", "Directory", "Applet", "Import", "ConstantPool", "Class", "Method", "StaticField", "RefLocation", "Export", "Descriptor", "Debug"]
|
||||
b = in_file.read()
|
||||
while len(b):
|
||||
tag, size = struct.unpack('!BH', b[0:3])
|
||||
out_zip.writestr(p+"/javacard/"+TAGS[tag-1]+".cap", b[0:3+size])
|
||||
b = b[3+size:]
|
||||
|
||||
class CapFile():
|
||||
|
||||
# Java Card Platform Virtual Machine Specification, v3.2, section 6.4
|
||||
__header_component_compact = Struct('tag'/Int8ub,
|
||||
'size'/Int16ub,
|
||||
'magic'/Int32ub,
|
||||
'minor_version'/Int8ub,
|
||||
'major_version'/Int8ub,
|
||||
'flags'/Int8ub,
|
||||
'package'/Struct('minor_version'/Int8ub,
|
||||
'major_version'/Int8ub,
|
||||
'AID'/LV),
|
||||
'package_name'/COptional(LV)) #since CAP format 2.2
|
||||
|
||||
# Java Card Platform Virtual Machine Specification, v3.2, section 6.6
|
||||
__applet_component_compact = Struct('tag'/Int8ub,
|
||||
'size'/Int16ub,
|
||||
'count'/Int8ub,
|
||||
'applets'/Array(this.count, Struct('AID'/LV,
|
||||
'install_method_offset'/Int16ub)),
|
||||
)
|
||||
|
||||
def __init__(self, filename:str):
|
||||
|
||||
# In this dictionary we will keep all nested .cap file components by their file names (without .cap suffix)
|
||||
# See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2.1
|
||||
self.__component = {}
|
||||
|
||||
# Extract the nested .cap components from the .cap file
|
||||
# See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2.1
|
||||
cap = zipfile.ZipFile(filename)
|
||||
cap_namelist = cap.namelist()
|
||||
for i, filename in enumerate(cap_namelist):
|
||||
if filename.lower().endswith('.capx') and not filename.lower().endswith('.capx'):
|
||||
#TODO: At the moment we only support the compact .cap format, add support for the extended .cap format.
|
||||
raise ValueError("incompatible .cap file, extended .cap format not supported!")
|
||||
|
||||
if filename.lower().endswith('.cap'):
|
||||
key = filename.split('/')[-1].removesuffix('.cap')
|
||||
self.__component[key] = cap.read(filename)
|
||||
|
||||
# Make sure that all mandatory components are present
|
||||
# See also: Java Card Platform Virtual Machine Specification, v3.2, section 6.2
|
||||
required_components = {'Header' : 'COMPONENT_Header',
|
||||
'Directory' : 'COMPONENT_Directory',
|
||||
'Import' : 'COMPONENT_Import',
|
||||
'ConstantPool' : 'COMPONENT_ConstantPool',
|
||||
'Class' : 'COMPONENT_Class',
|
||||
'Method' : 'COMPONENT_Method',
|
||||
'StaticField' : 'COMPONENT_StaticField',
|
||||
'RefLocation' : 'COMPONENT_ReferenceLocation',
|
||||
'Descriptor' : 'COMPONENT_Descriptor'}
|
||||
for component in required_components:
|
||||
if component not in self.__component.keys():
|
||||
raise ValueError("invalid cap file, %s missing!" % required_components[component])
|
||||
|
||||
def get_loadfile(self) -> bytes:
|
||||
"""Get the executeable loadfile as hexstring"""
|
||||
# Concatenate all cap file components in the specified order
|
||||
# see also: Java Card Platform Virtual Machine Specification, v3.2, section 6.3
|
||||
loadfile = self.__component['Header']
|
||||
loadfile += self.__component['Directory']
|
||||
loadfile += self.__component['Import']
|
||||
if 'Applet' in self.__component.keys():
|
||||
loadfile += self.__component['Applet']
|
||||
loadfile += self.__component['Class']
|
||||
loadfile += self.__component['Method']
|
||||
loadfile += self.__component['StaticField']
|
||||
if 'Export' in self.__component.keys():
|
||||
loadfile += self.__component['Export']
|
||||
loadfile += self.__component['ConstantPool']
|
||||
loadfile += self.__component['RefLocation']
|
||||
if 'Descriptor' in self.__component.keys():
|
||||
loadfile += self.__component['Descriptor']
|
||||
return loadfile
|
||||
|
||||
def get_loadfile_aid(self) -> Hexstr:
|
||||
"""Get the loadfile AID as hexstring"""
|
||||
header = self.__header_component_compact.parse(self.__component['Header'])
|
||||
magic = header['magic'] or 0
|
||||
if magic != 0xDECAFFED:
|
||||
raise ValueError("invalid cap file, COMPONENT_Header lacks magic number (0x%08X!=0xDECAFFED)!" % magic)
|
||||
#TODO: check cap version and make sure we are compatible with it
|
||||
return header['package']['AID']
|
||||
|
||||
def get_applet_aid(self, index:int = 0) -> Hexstr:
|
||||
"""Get the applet AID as hexstring"""
|
||||
#To get the module AID, we must look into COMPONENT_Applet. Unfortunately, even though this component should
|
||||
#be present in any .cap file, it is defined as an optional component.
|
||||
if 'Applet' not in self.__component.keys():
|
||||
raise ValueError("can't get the AID, this cap file lacks the optional COMPONENT_Applet component!")
|
||||
|
||||
applet = self.__applet_component_compact.parse(self.__component['Applet'])
|
||||
|
||||
if index > applet['count']:
|
||||
raise ValueError("can't get the AID for applet with index=%u, this .cap file only has %u applets!" %
|
||||
(index, applet['count']))
|
||||
|
||||
return applet['applets'][index]['AID']
|
||||
|
||||
# example usage:
|
||||
# with io.open(sys.argv[1],"rb") as f, zipfile.ZipFile(sys.argv[2], "wb") as z:
|
||||
# ijc_to_cap(f, z)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
################################################################################
|
||||
|
||||
import abc
|
||||
from smartcard.util import toBytes
|
||||
from pytlv.TLV import *
|
||||
|
||||
from pySim.cards import SimCardBase, UiccCardBase
|
||||
@@ -15,7 +16,7 @@ from pySim.legacy.ts_51_011 import EF, DF
|
||||
from pySim.legacy.ts_31_102 import EF_USIM_ADF_map
|
||||
from pySim.legacy.ts_31_103 import EF_ISIM_ADF_map
|
||||
|
||||
from pySim.ts_51_011 import EF_AD, EF_SPN
|
||||
from pySim.profile.ts_51_011 import EF_AD, EF_SPN
|
||||
|
||||
def format_addr(addr: str, addr_type: str) -> str:
|
||||
"""
|
||||
@@ -780,7 +781,7 @@ class SysmoSIMgr1(GrcardSim):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b991800118822334455667760":
|
||||
if scc.get_atr() == toBytes("3B 99 18 00 11 88 22 33 44 55 66 77 60"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -825,7 +826,7 @@ class SysmoSIMgr2(SimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b7d9400005555530a7486930b247c4d5468":
|
||||
if scc.get_atr() == toBytes("3B 7D 94 00 00 55 55 53 0A 74 86 93 0B 24 7C 4D 54 68"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -903,7 +904,7 @@ class SysmoUSIMSJS1(UsimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b9f96801fc78031a073be21136743200718000001a5":
|
||||
if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 43 20 07 18 00 00 01 A5"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1031,7 +1032,7 @@ class FairwavesSIM(UsimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b9f96801fc78031a073be21136744220610000001a9":
|
||||
if scc.get_atr() == toBytes("3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 44 22 06 10 00 00 01 A9"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1165,7 +1166,7 @@ class OpenCellsSim(SimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b9f95801fc38031e073fe21135786810286984418a8":
|
||||
if scc.get_atr() == toBytes("3B 9F 95 80 1F C3 80 31 E0 73 FE 21 13 57 86 81 02 86 98 44 18 A8"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1214,7 +1215,7 @@ class WavemobileSim(UsimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == "3b9f95801fc78031e073f62113674d4516004301008f":
|
||||
if scc.get_atr() == toBytes("3B 9F 95 80 1F C7 80 31 E0 73 F6 21 13 67 4D 45 16 00 43 01 00 8F"):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1304,18 +1305,18 @@ class SysmoISIMSJA2(UsimCard, IsimCard):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Try card model #1
|
||||
atr = "3b9f96801f878031e073fe211b674a4c753034054ba9"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 30 34 05 4B A9"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
|
||||
# Try card model #2
|
||||
atr = "3b9f96801f878031e073fe211b674a4c7531330251b2"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 31 33 02 51 B2"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
|
||||
# Try card model #3
|
||||
atr = "3b9f96801f878031e073fe211b674a4c5275310451d5"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 52 75 31 04 51 D5"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1553,16 +1554,16 @@ class SysmoISIMSJA5(SysmoISIMSJA2):
|
||||
def autodetect(kls, scc):
|
||||
try:
|
||||
# Try card model #1 (9FJ)
|
||||
atr = "3b9f96801f878031e073fe211b674a357530350251cc"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
# Try card model #2 (SLM17)
|
||||
atr = "3b9f96801f878031e073fe211b674a357530350265f8"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
# Try card model #3 (9FV)
|
||||
atr = "3b9f96801f878031e073fe211b674a357530350259c4"
|
||||
if scc.get_atr() == atr:
|
||||
atr = "3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"
|
||||
if scc.get_atr() == toBytes(atr):
|
||||
return kls(scc)
|
||||
except:
|
||||
return None
|
||||
@@ -1591,7 +1592,7 @@ class GialerSim(UsimCard):
|
||||
def autodetect(cls, scc):
|
||||
try:
|
||||
# Look for ATR
|
||||
if scc.get_atr() == '3b9f95801fc78031a073b6a10067cf3215ca9cd70920':
|
||||
if scc.get_atr() == toBytes('3B 9F 95 80 1F C7 80 31 A0 73 B6 A1 00 67 CF 32 15 CA 9C D7 09 20'):
|
||||
return cls(scc)
|
||||
except:
|
||||
return None
|
||||
|
||||
@@ -148,7 +148,7 @@ def dec_st(st, table="sim") -> str:
|
||||
from pySim.ts_31_102 import EF_UST_map
|
||||
lookup_map = EF_UST_map
|
||||
else:
|
||||
from pySim.ts_51_011 import EF_SST_map
|
||||
from pySim.profile.ts_51_011 import EF_SST_map
|
||||
lookup_map = EF_SST_map
|
||||
|
||||
st_bytes = [st[i:i+2] for i in range(0, len(st), 2)]
|
||||
|
||||
@@ -19,7 +19,7 @@ import zlib
|
||||
import abc
|
||||
import struct
|
||||
from typing import Optional, Tuple
|
||||
from construct import Enum, Int8ub, Int16ub, Struct, BitsInteger, BitStruct
|
||||
from construct import Enum, Int8ub, Int16ub, Struct, Bytes, GreedyBytes, BitsInteger, BitStruct
|
||||
from construct import Flag, Padding, Switch, this, PrefixedArray, GreedyRange
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import b2h
|
||||
@@ -410,7 +410,6 @@ class OtaDialectSms(OtaDialect):
|
||||
ciph = encoded[2+8:]
|
||||
envelope_data = otak.crypt.decrypt(ciph)
|
||||
else:
|
||||
cpl = None # FIXME this line was just added to silence pylint possibly-used-before-assignment
|
||||
part_head = encoded[:8]
|
||||
envelope_data = encoded[8:]
|
||||
|
||||
|
||||
@@ -25,9 +25,9 @@ from osmocom.construct import *
|
||||
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile import CardProfile, CardProfileAddon
|
||||
from pySim.ts_51_011 import CardProfileSIM
|
||||
from pySim.ts_51_011 import DF_TELECOM, DF_GSM
|
||||
from pySim.ts_51_011 import EF_ServiceTable
|
||||
from pySim.profile.ts_51_011 import CardProfileSIM
|
||||
from pySim.profile.ts_51_011 import DF_TELECOM, DF_GSM
|
||||
from pySim.profile.ts_51_011 import EF_ServiceTable
|
||||
|
||||
|
||||
# Mapping between CDMA Service Number and its description
|
||||
@@ -115,7 +115,7 @@ class EF_AD(TransparentEF):
|
||||
'''3.4.33 Administrative Data'''
|
||||
|
||||
_test_de_encode = [
|
||||
( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : b'\x00\x00', 'rfu' : b'' } ),
|
||||
( "000000", { 'ms_operation_mode' : 'normal', 'additional_info' : '0000', 'rfu' : '' } ),
|
||||
]
|
||||
_test_no_pad = True
|
||||
|
||||
@@ -134,9 +134,9 @@ class EF_AD(TransparentEF):
|
||||
# Byte 1: Display Condition
|
||||
'ms_operation_mode'/Enum(Byte, self.OP_MODE),
|
||||
# Bytes 2-3: Additional information
|
||||
'additional_info'/Bytes(2),
|
||||
'additional_info'/HexAdapter(Bytes(2)),
|
||||
# Bytes 4..: RFU
|
||||
'rfu'/GreedyBytesRFU,
|
||||
'rfu'/HexAdapter(GreedyBytesRFU),
|
||||
)
|
||||
|
||||
|
||||
58
pySim/profile/euicc.py
Normal file
58
pySim/profile/euicc.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# Copyright (C) 2023 Harald Welte <laforge@osmocom.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from pySim.profile.ts_102_221 import CardProfileUICC
|
||||
from pySim.commands import SimCardCommands
|
||||
from pySim.euicc import CardApplicationISDR, AID_ISD_R, AID_ECASD, GetCertsReq, GetCertsResp
|
||||
|
||||
class CardProfileEuiccSGP32(CardProfileUICC):
|
||||
ORDER = 5
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='IoT eUICC (SGP.32)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
# try a command only supported by SGP.32
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ISD_R)
|
||||
CardApplicationISDR.store_data_tlv(scc, GetCertsReq(), GetCertsResp)
|
||||
|
||||
class CardProfileEuiccSGP22(CardProfileUICC):
|
||||
ORDER = 6
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='Consumer eUICC (SGP.22)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
# try to read EID from ISD-R
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ISD_R)
|
||||
eid = CardApplicationISDR.get_eid(scc)
|
||||
# TODO: Store EID identity?
|
||||
|
||||
class CardProfileEuiccSGP02(CardProfileUICC):
|
||||
ORDER = 7
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(name='M2M eUICC (SGP.02)')
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
scc.cla_byte = "00"
|
||||
scc.select_adf(AID_ECASD)
|
||||
scc.get_data(0x5a)
|
||||
# TODO: Store EID identity?
|
||||
@@ -26,7 +26,7 @@ order to describe the files specified in UIC Reference P38 T 9001 5.0 "FFFIS for
|
||||
|
||||
from pySim.utils import *
|
||||
from struct import pack, unpack
|
||||
from construct import Struct, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum
|
||||
from construct import Struct, Bytes, Int8ub, Int16ub, Int24ub, Int32ub, FlagsEnum
|
||||
from construct import Optional as COptional
|
||||
from osmocom.construct import *
|
||||
|
||||
@@ -184,13 +184,13 @@ class EF_CallconfI(LinFixedEF):
|
||||
class EF_Shunting(TransparentEF):
|
||||
"""Section 7.6"""
|
||||
_test_de_encode = [
|
||||
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": h2b("f8ffffff000000") } ),
|
||||
( "03f8ffffff000000", { "common_gid": 3, "shunting_gid": "f8ffffff000000" } ),
|
||||
]
|
||||
def __init__(self):
|
||||
super().__init__(fid='6ff4', sfid=None,
|
||||
name='EF.Shunting', desc='Shunting', size=(8, 8))
|
||||
self._construct = Struct('common_gid'/Int8ub,
|
||||
'shunting_gid'/Bytes(7))
|
||||
'shunting_gid'/HexAdapter(Bytes(7)))
|
||||
|
||||
|
||||
class EF_GsmrPLMN(LinFixedEF):
|
||||
@@ -199,13 +199,13 @@ class EF_GsmrPLMN(LinFixedEF):
|
||||
( "22f860f86f8d6f8e01", { "plmn": "228-06", "class_of_network": {
|
||||
"supported": { "vbs": True, "vgcs": True, "emlpp": True,
|
||||
"fn": True, "eirene": True }, "preference": 0 },
|
||||
"ic_incoming_ref_tbl": h2b("6f8d"), "outgoing_ref_tbl": h2b("6f8e"),
|
||||
"ic_table_ref": h2b("01") } ),
|
||||
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
|
||||
"ic_table_ref": "01" } ),
|
||||
( "22f810416f8d6f8e02", { "plmn": "228-01", "class_of_network": {
|
||||
"supported": { "vbs": False, "vgcs": False, "emlpp": False,
|
||||
"fn": True, "eirene": False }, "preference": 1 },
|
||||
"ic_incoming_ref_tbl": h2b("6f8d"), "outgoing_ref_tbl": h2b("6f8e"),
|
||||
"ic_table_ref": h2b("02") } ),
|
||||
"ic_incoming_ref_tbl": "6f8d", "outgoing_ref_tbl": "6f8e",
|
||||
"ic_table_ref": "02" } ),
|
||||
]
|
||||
def __init__(self):
|
||||
super().__init__(fid='6ff5', sfid=None, name='EF.GsmrPLMN',
|
||||
@@ -213,24 +213,24 @@ class EF_GsmrPLMN(LinFixedEF):
|
||||
self._construct = Struct('plmn'/PlmnAdapter(Bytes(3)),
|
||||
'class_of_network'/BitStruct('supported'/FlagsEnum(BitsInteger(5), vbs=1, vgcs=2, emlpp=4, fn=8, eirene=16),
|
||||
'preference'/BitsInteger(3)),
|
||||
'ic_incoming_ref_tbl'/Bytes(2),
|
||||
'outgoing_ref_tbl'/Bytes(2),
|
||||
'ic_table_ref'/Bytes(1))
|
||||
'ic_incoming_ref_tbl'/HexAdapter(Bytes(2)),
|
||||
'outgoing_ref_tbl'/HexAdapter(Bytes(2)),
|
||||
'ic_table_ref'/HexAdapter(Bytes(1)))
|
||||
|
||||
|
||||
class EF_IC(LinFixedEF):
|
||||
"""Section 7.8"""
|
||||
_test_de_encode = [
|
||||
( "f06f8e40f10001", { "next_table_type": "decision", "id_of_next_table": h2b("6f8e"),
|
||||
( "f06f8e40f10001", { "next_table_type": "decision", "id_of_next_table": "6f8e",
|
||||
"ic_decision_value": "041f", "network_string_table_index": 1 } ),
|
||||
( "ffffffffffffff", { "next_table_type": "empty", "id_of_next_table": h2b("ffff"),
|
||||
( "ffffffffffffff", { "next_table_type": "empty", "id_of_next_table": "ffff",
|
||||
"ic_decision_value": "ffff", "network_string_table_index": 65535 } ),
|
||||
]
|
||||
def __init__(self):
|
||||
super().__init__(fid='6f8d', sfid=None, name='EF.IC',
|
||||
desc='International Code', rec_len=(7, 7))
|
||||
self._construct = Struct('next_table_type'/NextTableType,
|
||||
'id_of_next_table'/Bytes(2),
|
||||
'id_of_next_table'/HexAdapter(Bytes(2)),
|
||||
'ic_decision_value'/BcdAdapter(Bytes(2)),
|
||||
'network_string_table_index'/Int16ub)
|
||||
|
||||
@@ -252,18 +252,18 @@ class EF_NW(LinFixedEF):
|
||||
class EF_Switching(LinFixedEF):
|
||||
"""Section 8.4"""
|
||||
_test_de_encode = [
|
||||
( "f26f87f0ff00", { "next_table_type": "num_dial_digits", "id_of_next_table": h2b("6f87"),
|
||||
( "f26f87f0ff00", { "next_table_type": "num_dial_digits", "id_of_next_table": "6f87",
|
||||
"decision_value": "0fff", "string_table_index": 0 } ),
|
||||
( "f06f8ff1ff01", { "next_table_type": "decision", "id_of_next_table": h2b("6f8f"),
|
||||
( "f06f8ff1ff01", { "next_table_type": "decision", "id_of_next_table": "6f8f",
|
||||
"decision_value": "1fff", "string_table_index": 1 } ),
|
||||
( "f16f89f5ff05", { "next_table_type": "predefined", "id_of_next_table": h2b("6f89"),
|
||||
( "f16f89f5ff05", { "next_table_type": "predefined", "id_of_next_table": "6f89",
|
||||
"decision_value": "5fff", "string_table_index": 5 } ),
|
||||
]
|
||||
def __init__(self, fid='1234', name='Switching', desc=None):
|
||||
super().__init__(fid=fid, sfid=None,
|
||||
name=name, desc=desc, rec_len=(6, 6))
|
||||
self._construct = Struct('next_table_type'/NextTableType,
|
||||
'id_of_next_table'/Bytes(2),
|
||||
'id_of_next_table'/HexAdapter(Bytes(2)),
|
||||
'decision_value'/BcdAdapter(Bytes(2)),
|
||||
'string_table_index'/Int8ub)
|
||||
|
||||
@@ -271,12 +271,12 @@ class EF_Switching(LinFixedEF):
|
||||
class EF_Predefined(LinFixedEF):
|
||||
"""Section 8.5"""
|
||||
_test_de_encode = [
|
||||
( "f26f85", 1, { "next_table_type": "num_dial_digits", "id_of_next_table": h2b("6f85") } ),
|
||||
( "f26f85", 1, { "next_table_type": "num_dial_digits", "id_of_next_table": "6f85" } ),
|
||||
( "f0ffc8", 2, { "predefined_value1": "0fff", "string_table_index1": 200 } ),
|
||||
]
|
||||
# header and other records have different structure. WTF !?!
|
||||
construct_first = Struct('next_table_type'/NextTableType,
|
||||
'id_of_next_table'/Bytes(2))
|
||||
'id_of_next_table'/HexAdapter(Bytes(2)))
|
||||
construct_others = Struct('predefined_value1'/BcdAdapter(Bytes(2)),
|
||||
'string_table_index1'/Int8ub)
|
||||
|
||||
@@ -301,13 +301,13 @@ class EF_Predefined(LinFixedEF):
|
||||
class EF_DialledVals(TransparentEF):
|
||||
"""Section 8.6"""
|
||||
_test_de_encode = [
|
||||
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": h2b("ffff"), "dialed_digits": "22" } ),
|
||||
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": h2b("6f88"), "dialed_digits": "58" }),
|
||||
( "ffffff22", { "next_table_type": "empty", "id_of_next_table": "ffff", "dialed_digits": "22" } ),
|
||||
( "f16f8885", { "next_table_type": "predefined", "id_of_next_table": "6f88", "dialed_digits": "58" }),
|
||||
]
|
||||
def __init__(self, fid='1234', name='DialledVals', desc=None):
|
||||
super().__init__(fid=fid, sfid=None, name=name, desc=desc, size=(4, 4))
|
||||
self._construct = Struct('next_table_type'/NextTableType,
|
||||
'id_of_next_table'/Bytes(2),
|
||||
'id_of_next_table'/HexAdapter(Bytes(2)),
|
||||
'dialed_digits'/BcdAdapter(Bytes(1)))
|
||||
|
||||
|
||||
394
pySim/profile/ts_102_221.py
Normal file
394
pySim/profile/ts_102_221.py
Normal file
@@ -0,0 +1,394 @@
|
||||
# coding=utf-8
|
||||
"""Card Profile of ETSI TS 102 221, the core UICC spec.
|
||||
|
||||
(C) 2021-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 General Public License as published by
|
||||
the Free Software Foundation, either version 2 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 General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from construct import Struct, FlagsEnum, GreedyString
|
||||
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import *
|
||||
from osmocom.tlv import BER_TLV_IE
|
||||
|
||||
from pySim.utils import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile import CardProfile
|
||||
from pySim import iso7816_4
|
||||
from pySim.ts_102_221 import decode_select_response, ts_102_22x_cmdset
|
||||
from pySim.ts_102_221 import AM_DO_EF, SC_DO, AdditionalInterfacesSupport, AdditionalTermCapEuicc
|
||||
from pySim.ts_102_221 import TerminalPowerSupply, ExtendedLchanTerminalSupport, TerminalCapability
|
||||
|
||||
# A UICC will usually also support 2G functionality. If this is the case, we
|
||||
# need to add DF_GSM and DF_TELECOM along with the UICC related files
|
||||
from pySim.profile.ts_51_011 import AddonSIM, EF_ICCID, EF_PL
|
||||
from pySim.profile.gsm_r import AddonGSMR
|
||||
from pySim.profile.cdma_ruim import AddonRUIM
|
||||
|
||||
|
||||
# TS 102 221 Section 13.1
|
||||
class EF_DIR(LinFixedEF):
|
||||
_test_de_encode = [
|
||||
( '61294f10a0000000871002ffffffff890709000050055553696d31730ea00c80011781025f608203454150',
|
||||
{ "application_template": [ { "application_id": h2b("a0000000871002ffffffff8907090000") },
|
||||
{ "application_label": "USim1" },
|
||||
{ "discretionary_template": h2b("a00c80011781025f608203454150") } ] }
|
||||
),
|
||||
( '61194f10a0000000871004ffffffff890709000050054953696d31',
|
||||
{ "application_template": [ { "application_id": h2b("a0000000871004ffffffff8907090000") },
|
||||
{ "application_label": "ISim1" } ] }
|
||||
),
|
||||
]
|
||||
class ApplicationLabel(BER_TLV_IE, tag=0x50):
|
||||
# TODO: UCS-2 coding option as per Annex A of TS 102 221
|
||||
_construct = GreedyString('ascii')
|
||||
|
||||
# see https://github.com/PyCQA/pylint/issues/5794
|
||||
#pylint: disable=undefined-variable
|
||||
class ApplicationTemplate(BER_TLV_IE, tag=0x61,
|
||||
nested=[iso7816_4.ApplicationId, ApplicationLabel, iso7816_4.FileReference,
|
||||
iso7816_4.CommandApdu, iso7816_4.DiscretionaryData,
|
||||
iso7816_4.DiscretionaryTemplate, iso7816_4.URL,
|
||||
iso7816_4.ApplicationRelatedDOSet]):
|
||||
pass
|
||||
|
||||
def __init__(self, fid='2f00', sfid=0x1e, name='EF.DIR', desc='Application Directory'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(5, 54))
|
||||
self._tlv = EF_DIR.ApplicationTemplate
|
||||
|
||||
|
||||
# TS 102 221 Section 13.4
|
||||
class EF_ARR(LinFixedEF):
|
||||
_test_de_encode = [
|
||||
( '800101a40683010a950108800106900080016097008401d4a40683010a950108',
|
||||
[ [ { "access_mode": [ "read_search_compare" ] },
|
||||
{ "control_reference_template": "ADM1" } ],
|
||||
[ { "access_mode": [ "write_append", "update_erase" ] },
|
||||
{ "always": None } ],
|
||||
[ { "access_mode": [ "delete_file", "terminate_ef" ] },
|
||||
{ "never": None } ],
|
||||
[ { "command_header": { "INS": 212 } },
|
||||
{ "control_reference_template": "ADM1" } ]
|
||||
] ),
|
||||
( '80010190008001029700800118a40683010a9501088401d4a40683010a950108',
|
||||
[ [ { "access_mode": [ "read_search_compare" ] },
|
||||
{ "always": None } ],
|
||||
[ { "access_mode": [ "update_erase" ] },
|
||||
{ "never": None } ],
|
||||
[ { "access_mode": [ "activate_file_or_record", "deactivate_file_or_record" ] },
|
||||
{ "control_reference_template": "ADM1" } ],
|
||||
[ { "command_header": { "INS": 212 } },
|
||||
{ "control_reference_template": "ADM1" } ]
|
||||
] ),
|
||||
]
|
||||
def __init__(self, fid='2f06', sfid=0x06, name='EF.ARR', desc='Access Rule Reference'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc)
|
||||
# add those commands to the general commands of a TransparentEF
|
||||
self.shell_commands += [self.AddlShellCommands()]
|
||||
|
||||
@staticmethod
|
||||
def flatten(inp: list):
|
||||
"""Flatten the somewhat deep/complex/nested data returned from decoder."""
|
||||
def sc_abbreviate(sc):
|
||||
if 'always' in sc:
|
||||
return 'always'
|
||||
elif 'never' in sc:
|
||||
return 'never'
|
||||
elif 'control_reference_template' in sc:
|
||||
return sc['control_reference_template']
|
||||
else:
|
||||
return sc
|
||||
|
||||
by_mode = {}
|
||||
for t in inp:
|
||||
am = t[0]
|
||||
sc = t[1]
|
||||
sc_abbr = sc_abbreviate(sc)
|
||||
if 'access_mode' in am:
|
||||
for m in am['access_mode']:
|
||||
by_mode[m] = sc_abbr
|
||||
elif 'command_header' in am:
|
||||
ins = am['command_header']['INS']
|
||||
if 'CLA' in am['command_header']:
|
||||
cla = am['command_header']['CLA']
|
||||
else:
|
||||
cla = None
|
||||
cmd = ts_102_22x_cmdset.lookup(ins, cla)
|
||||
if cmd:
|
||||
name = cmd.name.lower().replace(' ', '_')
|
||||
by_mode[name] = sc_abbr
|
||||
else:
|
||||
raise ValueError
|
||||
else:
|
||||
raise ValueError
|
||||
return by_mode
|
||||
|
||||
def _decode_record_bin(self, raw_bin_data, **kwargs):
|
||||
# we can only guess if we should decode for EF or DF here :(
|
||||
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
||||
dec = arr_seq.decode_multi(raw_bin_data)
|
||||
# we cannot pass the result through flatten() here, as we don't have a related
|
||||
# 'un-flattening' decoder, and hence would be unable to encode :(
|
||||
return dec[0]
|
||||
|
||||
def _encode_record_bin(self, in_json, **kwargs):
|
||||
# we can only guess if we should decode for EF or DF here :(
|
||||
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
||||
return arr_seq.encode_multi(in_json)
|
||||
|
||||
@with_default_category('File-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
|
||||
def do_read_arr_record(self, opts):
|
||||
"""Read one EF.ARR record in flattened, human-friendly form."""
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
|
||||
data = self._cmd.lchan.selected_file.flatten(data)
|
||||
self._cmd.poutput_json(data, opts.oneline)
|
||||
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
|
||||
def do_read_arr_records(self, opts):
|
||||
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
# collect all results in list so they are rendered as JSON list when printing
|
||||
data_list = []
|
||||
for recnr in range(1, 1 + num_of_rec):
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(recnr)
|
||||
data = self._cmd.lchan.selected_file.flatten(data)
|
||||
data_list.append(data)
|
||||
self._cmd.poutput_json(data_list, opts.oneline)
|
||||
|
||||
|
||||
# TS 102 221 Section 13.6
|
||||
class EF_UMPC(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '3cff02', { "max_current_mA": 60, "t_op_s": 255,
|
||||
"addl_info": { "req_inc_idle_current": False, "support_uicc_suspend": True } } ),
|
||||
( '320500', { "max_current_mA": 50, "t_op_s": 5, "addl_info": {"req_inc_idle_current": False,
|
||||
"support_uicc_suspend": False } } ),
|
||||
]
|
||||
def __init__(self, fid='2f08', sfid=0x08, name='EF.UMPC', desc='UICC Maximum Power Consumption'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=(5, 5))
|
||||
addl_info = FlagsEnum(Byte, req_inc_idle_current=1,
|
||||
support_uicc_suspend=2)
|
||||
self._construct = Struct(
|
||||
'max_current_mA'/Int8ub, 't_op_s'/Int8ub, 'addl_info'/addl_info)
|
||||
|
||||
|
||||
class CardProfileUICC(CardProfile):
|
||||
|
||||
ORDER = 10
|
||||
|
||||
def __init__(self, name='UICC'):
|
||||
files = [
|
||||
EF_DIR(),
|
||||
EF_ICCID(),
|
||||
EF_PL(),
|
||||
EF_ARR(),
|
||||
# FIXME: DF.CD
|
||||
EF_UMPC(),
|
||||
]
|
||||
addons = [
|
||||
AddonSIM,
|
||||
AddonGSMR,
|
||||
AddonRUIM,
|
||||
]
|
||||
sw = {
|
||||
'Normal': {
|
||||
'9000': 'Normal ending of the command',
|
||||
'91xx': 'Normal ending of the command, with extra information from the proactive UICC containing a command for the terminal',
|
||||
'92xx': 'Normal ending of the command, with extra information concerning an ongoing data transfer session',
|
||||
},
|
||||
'Postponed processing': {
|
||||
'9300': 'SIM Application Toolkit is busy. Command cannot be executed at present, further normal commands are allowed',
|
||||
},
|
||||
'Warnings': {
|
||||
'6200': 'No information given, state of non-volatile memory unchanged',
|
||||
'6281': 'Part of returned data may be corrupted',
|
||||
'6282': 'End of file/record reached before reading Le bytes or unsuccessful search',
|
||||
'6283': 'Selected file invalidated/disabled; needs to be activated before use',
|
||||
'6284': 'Selected file in termination state',
|
||||
'62f1': 'More data available',
|
||||
'62f2': 'More data available and proactive command pending',
|
||||
'62f3': 'Response data available',
|
||||
'63f1': 'More data expected',
|
||||
'63f2': 'More data expected and proactive command pending',
|
||||
'63cx': 'Command successful but after using an internal update retry routine X times',
|
||||
},
|
||||
'Execution errors': {
|
||||
'6400': 'No information given, state of non-volatile memory unchanged',
|
||||
'6500': 'No information given, state of non-volatile memory changed',
|
||||
'6581': 'Memory problem',
|
||||
},
|
||||
'Checking errors': {
|
||||
'6700': 'Wrong length',
|
||||
'67xx': 'The interpretation of this status word is command dependent',
|
||||
'6b00': 'Wrong parameter(s) P1-P2',
|
||||
'6d00': 'Instruction code not supported or invalid',
|
||||
'6e00': 'Class not supported',
|
||||
'6f00': 'Technical problem, no precise diagnosis',
|
||||
'6fxx': 'The interpretation of this status word is command dependent',
|
||||
},
|
||||
'Functions in CLA not supported': {
|
||||
'6800': 'No information given',
|
||||
'6881': 'Logical channel not supported',
|
||||
'6882': 'Secure messaging not supported',
|
||||
},
|
||||
'Command not allowed': {
|
||||
'6900': 'No information given',
|
||||
'6981': 'Command incompatible with file structure',
|
||||
'6982': 'Security status not satisfied',
|
||||
'6983': 'Authentication/PIN method blocked',
|
||||
'6984': 'Referenced data invalidated',
|
||||
'6985': 'Conditions of use not satisfied',
|
||||
'6986': 'Command not allowed (no EF selected)',
|
||||
'6989': 'Command not allowed - secure channel - security not satisfied',
|
||||
},
|
||||
'Wrong parameters': {
|
||||
'6a80': 'Incorrect parameters in the data field',
|
||||
'6a81': 'Function not supported',
|
||||
'6a82': 'File not found',
|
||||
'6a83': 'Record not found',
|
||||
'6a84': 'Not enough memory space',
|
||||
'6a86': 'Incorrect parameters P1 to P2',
|
||||
'6a87': 'Lc inconsistent with P1 to P2',
|
||||
'6a88': 'Referenced data not found',
|
||||
},
|
||||
'Application errors': {
|
||||
'9850': 'INCREASE cannot be performed, max value reached',
|
||||
'9862': 'Authentication error, application specific',
|
||||
'9863': 'Security session or association expired',
|
||||
'9864': 'Minimum UICC suspension time is too long',
|
||||
},
|
||||
}
|
||||
|
||||
super().__init__(name, desc='ETSI TS 102 221', cla="00",
|
||||
sel_ctrl="0004", files_in_mf=files, sw=sw,
|
||||
shell_cmdsets = [self.AddlShellCommands()], addons = addons)
|
||||
|
||||
@staticmethod
|
||||
def decode_select_response(data_hex: str) -> object:
|
||||
"""ETSI TS 102 221 Section 11.1.1.3"""
|
||||
return decode_select_response(data_hex)
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
""" Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the
|
||||
card is considered a UICC card."""
|
||||
cls._mf_select_test(scc, "00", "0004", ["3f00"])
|
||||
|
||||
@with_default_category('TS 102 221 Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
suspend_uicc_parser = argparse.ArgumentParser()
|
||||
suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
|
||||
help='Proposed minimum duration of suspension')
|
||||
suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
|
||||
help='Proposed maximum duration of suspension')
|
||||
|
||||
# not ISO7816-4 but TS 102 221
|
||||
@cmd2.with_argparser(suspend_uicc_parser)
|
||||
def do_suspend_uicc(self, opts):
|
||||
"""Perform the SUSPEND UICC command. Only supported on some UICC (check EF.UMPC)."""
|
||||
(duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
|
||||
max_len_secs=opts.max_duration_secs)
|
||||
self._cmd.poutput(
|
||||
'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
|
||||
|
||||
resume_uicc_parser = argparse.ArgumentParser()
|
||||
resume_uicc_parser.add_argument('TOKEN', type=str, help='Token provided during SUSPEND')
|
||||
|
||||
@cmd2.with_argparser(resume_uicc_parser)
|
||||
def do_resume_uicc(self, opts):
|
||||
"""Perform the REUSME UICC operation. Only supported on some UICC. Also: A power-cycle
|
||||
of the card is required between SUSPEND and RESUME, and only very few non-RESUME
|
||||
commands are permitted between SUSPEND and RESUME. See TS 102 221 Section 11.1.22."""
|
||||
self._cmd.card._scc.resume_uicc(opts.TOKEN)
|
||||
|
||||
term_cap_parser = argparse.ArgumentParser()
|
||||
# power group
|
||||
tc_power_grp = term_cap_parser.add_argument_group('Terminal Power Supply')
|
||||
tc_power_grp.add_argument('--used-supply-voltage-class', type=str, choices=['a','b','c','d','e'],
|
||||
help='Actual used Supply voltage class')
|
||||
tc_power_grp.add_argument('--maximum-available-power-supply', type=auto_uint8,
|
||||
help='Maximum available power supply of the terminal')
|
||||
tc_power_grp.add_argument('--actual-used-freq-100k', type=auto_uint8,
|
||||
help='Actual used clock frequency (in units of 100kHz)')
|
||||
# no separate groups for those two
|
||||
tc_elc_grp = term_cap_parser.add_argument_group('Extended logical channels terminal support')
|
||||
tc_elc_grp.add_argument('--extended-logical-channel', action='store_true',
|
||||
help='Extended Logical Channel supported')
|
||||
tc_aif_grp = term_cap_parser.add_argument_group('Additional interfaces support')
|
||||
tc_aif_grp.add_argument('--uicc-clf', action='store_true',
|
||||
help='Local User Interface in the Device (LUId) supported')
|
||||
# eUICC group
|
||||
tc_euicc_grp = term_cap_parser.add_argument_group('Additional Terminal capability indications related to eUICC')
|
||||
tc_euicc_grp.add_argument('--lui-d', action='store_true',
|
||||
help='Local User Interface in the Device (LUId) supported')
|
||||
tc_euicc_grp.add_argument('--lpd-d', action='store_true',
|
||||
help='Local Profile Download in the Device (LPDd) supported')
|
||||
tc_euicc_grp.add_argument('--lds-d', action='store_true',
|
||||
help='Local Discovery Service in the Device (LPDd) supported')
|
||||
tc_euicc_grp.add_argument('--lui-e-scws', action='store_true',
|
||||
help='LUIe based on SCWS supported')
|
||||
tc_euicc_grp.add_argument('--metadata-update-alerting', action='store_true',
|
||||
help='Metadata update alerting supported')
|
||||
tc_euicc_grp.add_argument('--enterprise-capable-device', action='store_true',
|
||||
help='Enterprise Capable Device')
|
||||
tc_euicc_grp.add_argument('--lui-e-e4e', action='store_true',
|
||||
help='LUIe using E4E (ENVELOPE tag E4) supported')
|
||||
tc_euicc_grp.add_argument('--lpr', action='store_true',
|
||||
help='LPR (LPA Proxy) supported')
|
||||
|
||||
@cmd2.with_argparser(term_cap_parser)
|
||||
def do_terminal_capability(self, opts):
|
||||
"""Perform the TERMINAL CAPABILITY function. Used to inform the UICC about terminal capability."""
|
||||
ps_flags = {}
|
||||
addl_if_flags = {}
|
||||
euicc_flags = {}
|
||||
|
||||
opts_dict = vars(opts)
|
||||
|
||||
power_items = ['used_supply_voltage_class', 'maximum_available_power_supply', 'actual_used_freq_100k']
|
||||
if any(opts_dict[x] for x in power_items):
|
||||
if not all(opts_dict[x] for x in power_items):
|
||||
raise argparse.ArgumentTypeError('If any of the Terminal Power Supply group options are used, all must be specified')
|
||||
|
||||
for k, v in opts_dict.items():
|
||||
if k in AdditionalInterfacesSupport._construct.flags.keys():
|
||||
addl_if_flags[k] = v
|
||||
elif k in AdditionalTermCapEuicc._construct.flags.keys():
|
||||
euicc_flags[k] = v
|
||||
elif k in [f.name for f in TerminalPowerSupply._construct.subcons]:
|
||||
if k == 'used_supply_voltage_class' and v:
|
||||
v = {v: True}
|
||||
ps_flags[k] = v
|
||||
|
||||
child_list = []
|
||||
if any(x for x in ps_flags.values()):
|
||||
child_list.append(TerminalPowerSupply(decoded=ps_flags))
|
||||
|
||||
if opts.extended_logical_channel:
|
||||
child_list.append(ExtendedLchanTerminalSupport())
|
||||
if any(x for x in addl_if_flags.values()):
|
||||
child_list.append(AdditionalInterfacesSupport(decoded=addl_if_flags))
|
||||
if any(x for x in euicc_flags.values()):
|
||||
child_list.append(AdditionalTermCapEuicc(decoded=euicc_flags))
|
||||
|
||||
print(child_list)
|
||||
tc = TerminalCapability(children=child_list)
|
||||
self.terminal_capability(b2h(tc.to_tlv()))
|
||||
|
||||
def terminal_capability(self, data:Hexstr):
|
||||
cmd_hex = "80AA0000%02x%s" % (len(data)//2, data)
|
||||
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
@@ -43,7 +43,7 @@ from pySim.utils import dec_iccid, enc_iccid, dec_imsi, enc_imsi, dec_plmn, enc_
|
||||
from pySim.profile import CardProfile, CardProfileAddon
|
||||
from pySim.filesystem import *
|
||||
from pySim.ts_31_102_telecom import DF_PHONEBOOK, DF_MULTIMEDIA, DF_MCS, DF_V2X
|
||||
from pySim.gsm_r import AddonGSMR
|
||||
from pySim.profile.gsm_r import AddonGSMR
|
||||
|
||||
# Mapping between SIM Service Number and its description
|
||||
EF_SST_map = {
|
||||
@@ -250,7 +250,7 @@ class EF_SMSP(LinFixedEF):
|
||||
"tp_sc_addr": { "length": 255, "ton_npi": { "ext": True, "type_of_number": "reserved_for_extension",
|
||||
"numbering_plan_id": "reserved_for_extension" },
|
||||
"call_number": "" },
|
||||
"tp_pid": b"\x00", "tp_dcs": b"\x00", "tp_vp_minutes": 1440 } ),
|
||||
"tp_pid": "00", "tp_dcs": "00", "tp_vp_minutes": 1440 } ),
|
||||
]
|
||||
_test_no_pad = True
|
||||
class ValidityPeriodAdapter(Adapter):
|
||||
@@ -286,8 +286,8 @@ class EF_SMSP(LinFixedEF):
|
||||
'tp_dest_addr'/ScAddr,
|
||||
'tp_sc_addr'/ScAddr,
|
||||
|
||||
'tp_pid'/Bytes(1),
|
||||
'tp_dcs'/Bytes(1),
|
||||
'tp_pid'/HexAdapter(Bytes(1)),
|
||||
'tp_dcs'/HexAdapter(Bytes(1)),
|
||||
'tp_vp_minutes'/EF_SMSP.ValidityPeriodAdapter(Byte))
|
||||
|
||||
# TS 51.011 Section 10.5.7
|
||||
@@ -309,14 +309,14 @@ class EF_SMSR(LinFixedEF):
|
||||
def __init__(self, fid='6f47', sfid=None, name='EF.SMSR', desc='SMS status reports', rec_len=(30, 30), **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
|
||||
self._construct = Struct(
|
||||
'sms_record_id'/Int8ub, 'sms_status_report'/Bytes(29))
|
||||
'sms_record_id'/Int8ub, 'sms_status_report'/HexAdapter(Bytes(29)))
|
||||
|
||||
|
||||
class EF_EXT(LinFixedEF):
|
||||
def __init__(self, fid, sfid=None, name='EF.EXT', desc='Extension', rec_len=(13, 13), **kwargs):
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
|
||||
self._construct = Struct(
|
||||
'record_type'/Int8ub, 'extension_data'/Bytes(11), 'identifier'/Int8ub)
|
||||
'record_type'/Int8ub, 'extension_data'/HexAdapter(Bytes(11)), 'identifier'/Int8ub)
|
||||
|
||||
# TS 51.011 Section 10.5.16
|
||||
class EF_CMI(LinFixedEF):
|
||||
@@ -589,11 +589,11 @@ class EF_ACC(TransparentEF):
|
||||
class EF_LOCI(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( "7802570222f81009780000",
|
||||
{ "tmsi": h2b("78025702"), "lai": h2b("22f8100978"), "tmsi_time": 0, "lu_status": "updated" } ),
|
||||
{ "tmsi": "78025702", "lai": "22f8100978", "tmsi_time": 0, "lu_status": "updated" } ),
|
||||
]
|
||||
def __init__(self, fid='6f7e', sfid=None, name='EF.LOCI', desc='Location Information', size=(11, 11)):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size)
|
||||
self._construct = Struct('tmsi'/Bytes(4), 'lai'/Bytes(5), 'tmsi_time'/Int8ub,
|
||||
self._construct = Struct('tmsi'/HexAdapter(Bytes(4)), 'lai'/HexAdapter(Bytes(5)), 'tmsi_time'/Int8ub,
|
||||
'lu_status'/Enum(Byte, updated=0, not_updated=1, plmn_not_allowed=2,
|
||||
location_area_not_allowed=3))
|
||||
|
||||
@@ -751,22 +751,22 @@ class EF_NIA(LinFixedEF):
|
||||
# TS 51.011 Section 10.3.32
|
||||
class EF_Kc(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( "837d783609a3858f05", { "kc": h2b("837d783609a3858f"), "cksn": 5 } ),
|
||||
( "837d783609a3858f05", { "kc": "837d783609a3858f", "cksn": 5 } ),
|
||||
]
|
||||
def __init__(self, fid='6f20', sfid=None, name='EF.Kc', desc='Ciphering key Kc', size=(9, 9), **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||
self._construct = Struct('kc'/Bytes(8), 'cksn'/Int8ub)
|
||||
self._construct = Struct('kc'/HexAdapter(Bytes(8)), 'cksn'/Int8ub)
|
||||
|
||||
# TS 51.011 Section 10.3.33
|
||||
class EF_LOCIGPRS(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( "ffffffffffffff22f8990000ff01",
|
||||
{ "ptmsi": h2b("ffffffff"), "ptmsi_sig": h2b("ffffff"), "rai": h2b("22f8990000ff"), "rau_status": "not_updated" } ),
|
||||
{ "ptmsi": "ffffffff", "ptmsi_sig": "ffffff", "rai": "22f8990000ff", "rau_status": "not_updated" } ),
|
||||
]
|
||||
def __init__(self, fid='6f53', sfid=None, name='EF.LOCIGPRS', desc='GPRS Location Information', size=(14, 14)):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size)
|
||||
self._construct = Struct('ptmsi'/Bytes(4), 'ptmsi_sig'/Bytes(3),
|
||||
'rai'/Bytes(6),
|
||||
self._construct = Struct('ptmsi'/HexAdapter(Bytes(4)), 'ptmsi_sig'/HexAdapter(Bytes(3)),
|
||||
'rai'/HexAdapter(Bytes(6)),
|
||||
'rau_status'/Enum(Byte, updated=0, not_updated=1, plmn_not_allowed=2,
|
||||
routing_area_not_allowed=3))
|
||||
|
||||
@@ -867,12 +867,12 @@ class EF_PNN(LinFixedEF):
|
||||
class FullNameForNetwork(BER_TLV_IE, tag=0x43):
|
||||
# TS 24.008 10.5.3.5a
|
||||
# TODO: proper decode
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class ShortNameForNetwork(BER_TLV_IE, tag=0x45):
|
||||
# TS 24.008 10.5.3.5a
|
||||
# TODO: proper decode
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class NetworkNameCollection(TLV_IE_Collection, nested=[FullNameForNetwork, ShortNameForNetwork]):
|
||||
pass
|
||||
@@ -885,14 +885,12 @@ class EF_PNN(LinFixedEF):
|
||||
class EF_OPL(LinFixedEF):
|
||||
_test_de_encode = [
|
||||
( '62f2100000fffe01',
|
||||
{ "lai": { "mcc_mnc": "262-01", "lac_min": h2b("0000"), "lac_max": h2b("fffe") }, "pnn_record_id": 1 } ),
|
||||
( '216354789abcde12',
|
||||
{ "lai": { "mcc_mnc": "123-456", "lac_min": h2b("789a"), "lac_max": h2b("bcde") }, "pnn_record_id": 18 } ),
|
||||
{ "lai": { "mcc_mnc": "262-01", "lac_min": "0000", "lac_max": "fffe" }, "pnn_record_id": 1 } ),
|
||||
]
|
||||
def __init__(self, fid='6fc6', sfid=None, name='EF.OPL', rec_len=(8, 8), desc='Operator PLMN List', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
|
||||
self._construct = Struct('lai'/Struct('mcc_mnc'/PlmnAdapter(Bytes(3)),
|
||||
'lac_min'/Bytes(2), 'lac_max'/Bytes(2)), 'pnn_record_id'/Int8ub)
|
||||
'lac_min'/HexAdapter(Bytes(2)), 'lac_max'/HexAdapter(Bytes(2))), 'pnn_record_id'/Int8ub)
|
||||
|
||||
# TS 51.011 Section 10.3.44 + TS 31.102 4.2.62
|
||||
class EF_MBI(LinFixedEF):
|
||||
@@ -941,8 +939,8 @@ class EF_SPDI(TransparentEF):
|
||||
class EF_MMSN(LinFixedEF):
|
||||
def __init__(self, fid='6fce', sfid=None, name='EF.MMSN', rec_len=(4, 20), desc='MMS Notification', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
|
||||
self._construct = Struct('mms_status'/Bytes(2), 'mms_implementation'/Bytes(1),
|
||||
'mms_notification'/Bytes(this._.total_len-4), 'ext_record_nr'/Byte)
|
||||
self._construct = Struct('mms_status'/HexAdapter(Bytes(2)), 'mms_implementation'/HexAdapter(Bytes(1)),
|
||||
'mms_notification'/HexAdapter(Bytes(this._.total_len-4)), 'ext_record_nr'/Byte)
|
||||
|
||||
# TS 51.011 Annex K.1
|
||||
class MMS_Implementation(BER_TLV_IE, tag=0x80):
|
||||
@@ -53,7 +53,6 @@ class RuntimeState:
|
||||
# this is a dict of card identities which different parts of the code might populate,
|
||||
# typically with something like ICCID, EID, ATR, ...
|
||||
self.identity = {}
|
||||
self.adm_verified = False
|
||||
|
||||
# make sure the class and selection control bytes, which are specified
|
||||
# by the card profile are used
|
||||
@@ -140,8 +139,7 @@ class RuntimeState:
|
||||
if lchan_nr == 0:
|
||||
continue
|
||||
del self.lchan[lchan_nr]
|
||||
self.adm_verified = False
|
||||
atr = self.card.reset()
|
||||
atr = i2h(self.card.reset())
|
||||
if cmd_app:
|
||||
cmd_app.lchan = self.lchan[0]
|
||||
# select MF to reset internal state and to verify card really works
|
||||
|
||||
49
pySim/sms.py
49
pySim/sms.py
@@ -20,10 +20,10 @@
|
||||
import typing
|
||||
import abc
|
||||
from bidict import bidict
|
||||
from construct import Int8ub, Byte, Bit, Flag, BitsInteger
|
||||
from construct import Int8ub, Byte, Bytes, Bit, Flag, BitsInteger
|
||||
from construct import Struct, Enum, Tell, BitStruct, this, Padding
|
||||
from construct import Prefixed, GreedyRange
|
||||
from osmocom.construct import BcdAdapter, TonNpi, Bytes, GreedyBytes
|
||||
from construct import Prefixed, GreedyRange, GreedyBytes
|
||||
from osmocom.construct import HexAdapter, BcdAdapter, TonNpi
|
||||
from osmocom.utils import Hexstr, h2b, b2h
|
||||
|
||||
from smpp.pdu import pdu_types, operations
|
||||
@@ -253,49 +253,6 @@ class SMS_DELIVER(SMS_TPDU):
|
||||
}
|
||||
return cls(**d)
|
||||
|
||||
@classmethod
|
||||
def from_submit(cls, submit: 'SMS_SUBMIT') -> 'SMS_DELIVER':
|
||||
"""Construct a SMS_DELIVER instance from a SMS_SUBMIT instance."""
|
||||
d = {
|
||||
# common fields (SMS_TPDU base class) which exist in submit, so we can copy them
|
||||
'tp_mti': submit.tp_mti,
|
||||
'tp_rp': submit.tp_rp,
|
||||
'tp_udhi': submit.tp_udhi,
|
||||
'tp_pid': submit.tp_pid,
|
||||
'tp_dcs': submit.tp_dcs,
|
||||
'tp_udl': submit.tp_udl,
|
||||
'tp_ud': submit.tp_ud,
|
||||
# SMS_DELIVER specific fields
|
||||
'tp_lp': False,
|
||||
'tp_mms': False,
|
||||
'tp_oa': None,
|
||||
'tp_scts': h2b('22705200000000'), # FIXME
|
||||
'tp_sri': False,
|
||||
}
|
||||
return cls(**d)
|
||||
|
||||
def to_smpp(self) -> pdu_types.PDU:
|
||||
"""Translate a SMS_DELIVER instance to a smpp.pdu.operations.DeliverSM instance."""
|
||||
# we only deal with binary SMS here:
|
||||
if self.tp_dcs != 0xF6:
|
||||
raise ValueError('Unsupported DCS: We only support DCS=0xF6 for now')
|
||||
dcs = pdu_types.DataCoding(pdu_types.DataCodingScheme.DEFAULT,
|
||||
pdu_types.DataCodingDefault.OCTET_UNSPECIFIED)
|
||||
esm_class = pdu_types.EsmClass(pdu_types.EsmClassMode.DEFAULT, pdu_types.EsmClassType.DEFAULT,
|
||||
gsmFeatures=[pdu_types.EsmClassGsmFeatures.UDHI_INDICATOR_SET])
|
||||
if self.tp_oa:
|
||||
oa_digits, oa_ton, oa_npi = self.tp_oa.to_smpp()
|
||||
else:
|
||||
oa_digits, oa_ton, oa_npi = None, None, None
|
||||
return operations.DeliverSM(source_addr=oa_digits,
|
||||
source_addr_ton=oa_ton,
|
||||
source_addr_npi=oa_npi,
|
||||
#destination_addr=ESME_MSISDN,
|
||||
esm_class=esm_class,
|
||||
protocol_id=self.tp_pid,
|
||||
data_coding=dcs,
|
||||
short_message=self.tp_ud)
|
||||
|
||||
|
||||
|
||||
class SMS_SUBMIT(SMS_TPDU):
|
||||
|
||||
@@ -18,13 +18,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
from struct import unpack
|
||||
from construct import FlagsEnum, Byte, Struct, Int8ub, Mapping, Enum, Padding, BitsInteger
|
||||
from construct import FlagsEnum, Byte, Struct, Int8ub, Bytes, Mapping, Enum, Padding, BitsInteger
|
||||
from construct import Bit, this, Int32ub, Int16ub, Nibble, BytesInteger, GreedyRange, Const
|
||||
from construct import Optional as COptional
|
||||
from osmocom.utils import *
|
||||
from osmocom.construct import *
|
||||
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile.ts_102_221 import CardProfileUICC
|
||||
from pySim.runtime import RuntimeState
|
||||
import pySim
|
||||
|
||||
@@ -51,13 +52,13 @@ class EF_PIN(TransparentEF):
|
||||
( 'f1030331323334ffffffff0a0a3132333435363738',
|
||||
{ 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
|
||||
'not_initialized': False, 'disabled': True },
|
||||
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': b'1234',
|
||||
'puk': { 'attempts_remaining': 10, 'maximum_attempts': 10, 'puk': b'12345678' }
|
||||
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': '31323334',
|
||||
'puk': { 'attempts_remaining': 10, 'maximum_attempts': 10, 'puk': '3132333435363738' }
|
||||
} ),
|
||||
( 'f003039999999999999999',
|
||||
{ 'state': { 'valid': True, 'change_able': True, 'unblock_able': True, 'disable_able': True,
|
||||
'not_initialized': False, 'disabled': False },
|
||||
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': h2b('9999999999999999'),
|
||||
'attempts_remaining': 3, 'maximum_attempts': 3, 'pin': '9999999999999999',
|
||||
'puk': None } ),
|
||||
]
|
||||
def __init__(self, fid='6f01', name='EF.CHV1'):
|
||||
@@ -66,32 +67,29 @@ class EF_PIN(TransparentEF):
|
||||
change_able=0x40, valid=0x80)
|
||||
PukStruct = Struct('attempts_remaining'/Int8ub,
|
||||
'maximum_attempts'/Int8ub,
|
||||
'puk'/Rpad(Bytes(8)))
|
||||
'puk'/HexAdapter(Rpad(Bytes(8))))
|
||||
self._construct = Struct('state'/StateByte,
|
||||
'attempts_remaining'/Int8ub,
|
||||
'maximum_attempts'/Int8ub,
|
||||
'pin'/Rpad(Bytes(8)),
|
||||
'pin'/HexAdapter(Rpad(Bytes(8))),
|
||||
'puk'/COptional(PukStruct))
|
||||
|
||||
|
||||
class EF_MILENAGE_CFG(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '40002040600000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000020000000000000000000000000000000400000000000000000000000000000008',
|
||||
{"r1": 64, "r2": 0, "r3": 32, "r4": 64, "r5": 96,
|
||||
"c1": h2b("00000000000000000000000000000000"),
|
||||
"c2": h2b("00000000000000000000000000000001"),
|
||||
"c3": h2b("00000000000000000000000000000002"),
|
||||
"c4": h2b("00000000000000000000000000000004"),
|
||||
"c5": h2b("00000000000000000000000000000008")} ),
|
||||
{"r1": 64, "r2": 0, "r3": 32, "r4": 64, "r5": 96, "c1": "00000000000000000000000000000000", "c2":
|
||||
"00000000000000000000000000000001", "c3": "00000000000000000000000000000002", "c4":
|
||||
"00000000000000000000000000000004", "c5": "00000000000000000000000000000008"} ),
|
||||
]
|
||||
def __init__(self, fid='6f21', name='EF.MILENAGE_CFG', desc='Milenage connfiguration'):
|
||||
super().__init__(fid, name=name, desc=desc)
|
||||
self._construct = Struct('r1'/Int8ub, 'r2'/Int8ub, 'r3'/Int8ub, 'r4'/Int8ub, 'r5'/Int8ub,
|
||||
'c1'/Bytes(16),
|
||||
'c2'/Bytes(16),
|
||||
'c3'/Bytes(16),
|
||||
'c4'/Bytes(16),
|
||||
'c5'/Bytes(16))
|
||||
'c1'/HexAdapter(Bytes(16)),
|
||||
'c2'/HexAdapter(Bytes(16)),
|
||||
'c3'/HexAdapter(Bytes(16)),
|
||||
'c4'/HexAdapter(Bytes(16)),
|
||||
'c5'/HexAdapter(Bytes(16)))
|
||||
|
||||
|
||||
class EF_0348_KEY(LinFixedEF):
|
||||
@@ -105,18 +103,18 @@ class EF_0348_KEY(LinFixedEF):
|
||||
self._construct = Struct('security_domain'/Int8ub,
|
||||
'key_set_version'/Int8ub,
|
||||
'key_len_and_type'/KeyLenAndType,
|
||||
'key'/Bytes(this.key_len_and_type.key_length))
|
||||
'key'/HexAdapter(Bytes(this.key_len_and_type.key_length)))
|
||||
|
||||
|
||||
class EF_0348_COUNT(LinFixedEF):
|
||||
_test_de_encode = [
|
||||
( 'fe010000000000', {"sec_domain": 254, "key_set_version": 1, "counter": h2b("0000000000")} ),
|
||||
( 'fe010000000000', {"sec_domain": 254, "key_set_version": 1, "counter": "0000000000"} ),
|
||||
]
|
||||
def __init__(self, fid='6f23', name='EF.0348_COUNT', desc='TS 03.48 OTA Counters'):
|
||||
super().__init__(fid, name=name, desc=desc, rec_len=(7, 7))
|
||||
self._construct = Struct('sec_domain'/Int8ub,
|
||||
'key_set_version'/Int8ub,
|
||||
'counter'/Bytes(5))
|
||||
'counter'/HexAdapter(Bytes(5)))
|
||||
|
||||
|
||||
class EF_SIM_AUTH_COUNTER(TransparentEF):
|
||||
@@ -148,9 +146,8 @@ class EF_GP_DIV_DATA(LinFixedEF):
|
||||
class EF_SIM_AUTH_KEY(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
||||
{"cfg": {"sres_deriv_func": 1, "use_opc_instead_of_op": True, "algorithm": "milenage"},
|
||||
"key": h2b("000102030405060708090a0b0c0d0e0f"),
|
||||
"op_opc": h2b("101112131415161718191a1b1c1d1e1f")} ),
|
||||
{"cfg": {"sres_deriv_func": 1, "use_opc_instead_of_op": True, "algorithm": "milenage"}, "key":
|
||||
"000102030405060708090a0b0c0d0e0f", "op_opc": "101112131415161718191a1b1c1d1e1f"} ),
|
||||
]
|
||||
def __init__(self, fid='6f20', name='EF.SIM_AUTH_KEY'):
|
||||
super().__init__(fid, name=name, desc='USIM authentication key')
|
||||
@@ -159,8 +156,8 @@ class EF_SIM_AUTH_KEY(TransparentEF):
|
||||
'use_opc_instead_of_op'/Flag,
|
||||
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3))
|
||||
self._construct = Struct('cfg'/CfgByte,
|
||||
'key'/Bytes(16),
|
||||
'op_opc' /Bytes(16))
|
||||
'key'/HexAdapter(Bytes(16)),
|
||||
'op_opc' /HexAdapter(Bytes(16)))
|
||||
|
||||
|
||||
class DF_SYSTEM(CardDF):
|
||||
@@ -184,7 +181,7 @@ class DF_SYSTEM(CardDF):
|
||||
self.add_files(files)
|
||||
|
||||
def decode_select_response(self, resp_hex):
|
||||
return pySim.ts_102_221.CardProfileUICC.decode_select_response(resp_hex)
|
||||
return CardProfileUICC.decode_select_response(resp_hex)
|
||||
|
||||
|
||||
class EF_USIM_SQN(TransparentEF):
|
||||
@@ -213,13 +210,13 @@ class EF_USIM_AUTH_KEY(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '141898d827f70120d33b3e7462ee5fd6fe6ca53d7a0a804561646816d7b0c702fb',
|
||||
{ "cfg": { "only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True, "algorithm": "milenage" },
|
||||
"key": h2b("1898d827f70120d33b3e7462ee5fd6fe"), "op_opc": h2b("6ca53d7a0a804561646816d7b0c702fb") } ),
|
||||
"key": "1898d827f70120d33b3e7462ee5fd6fe", "op_opc": "6ca53d7a0a804561646816d7b0c702fb" } ),
|
||||
( '160a04101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f000102030405060708090a0b0c0d0e0f',
|
||||
{ "cfg" : { "algorithm" : "tuak", "key_length" : 128, "sres_deriv_func_in_2g" : 1, "use_opc_instead_of_op" : True },
|
||||
"tuak_cfg" : { "ck_and_ik_size" : 128, "mac_size" : 128, "res_size" : 128 },
|
||||
"num_of_keccak_iterations" : 4,
|
||||
"k" : h2b("000102030405060708090a0b0c0d0e0f"),
|
||||
"op_opc" : h2b("101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f")
|
||||
"k" : "000102030405060708090a0b0c0d0e0f",
|
||||
"op_opc" : "101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"
|
||||
} ),
|
||||
]
|
||||
def __init__(self, fid='af20', name='EF.USIM_AUTH_KEY'):
|
||||
@@ -230,8 +227,8 @@ class EF_USIM_AUTH_KEY(TransparentEF):
|
||||
'use_opc_instead_of_op'/Mapping(Bit, {False:0, True:1}),
|
||||
'algorithm'/Algorithm)
|
||||
self._construct = Struct('cfg'/CfgByte,
|
||||
'key'/Bytes(16),
|
||||
'op_opc'/Bytes(16))
|
||||
'key'/HexAdapter(Bytes(16)),
|
||||
'op_opc' /HexAdapter(Bytes(16)))
|
||||
# TUAK has a rather different layout for the data, so we define a different
|
||||
# construct below and use explicit _{decode,encode}_bin() methods for separating
|
||||
# the TUAK and non-TUAK situation
|
||||
@@ -247,8 +244,8 @@ class EF_USIM_AUTH_KEY(TransparentEF):
|
||||
self._constr_tuak = Struct('cfg'/CfgByteTuak,
|
||||
'tuak_cfg'/TuakCfgByte,
|
||||
'num_of_keccak_iterations'/Int8ub,
|
||||
'op_opc'/Bytes(32),
|
||||
'k'/Bytes(this.cfg.key_length//8))
|
||||
'op_opc'/HexAdapter(Bytes(32)),
|
||||
'k'/HexAdapter(Bytes(this.cfg.key_length//8)))
|
||||
|
||||
def _decode_bin(self, raw_bin_data: bytearray) -> dict:
|
||||
if raw_bin_data[0] & 0x0F == 0x06:
|
||||
@@ -267,9 +264,8 @@ class EF_USIM_AUTH_KEY_2G(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '14000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f',
|
||||
{"cfg": {"only_4bytes_res_in_3g": False, "sres_deriv_func_in_2g": 1, "use_opc_instead_of_op": True,
|
||||
"algorithm": "milenage"},
|
||||
"key": h2b("000102030405060708090a0b0c0d0e0f"),
|
||||
"op_opc": h2b("101112131415161718191a1b1c1d1e1f")} ),
|
||||
"algorithm": "milenage"}, "key": "000102030405060708090a0b0c0d0e0f", "op_opc":
|
||||
"101112131415161718191a1b1c1d1e1f"} ),
|
||||
]
|
||||
def __init__(self, fid='af22', name='EF.USIM_AUTH_KEY_2G'):
|
||||
super().__init__(fid, name=name, desc='USIM authentication key in 2G context')
|
||||
@@ -278,8 +274,8 @@ class EF_USIM_AUTH_KEY_2G(TransparentEF):
|
||||
'use_opc_instead_of_op'/Flag,
|
||||
'algorithm'/Enum(Nibble, milenage=4, comp128v1=1, comp128v2=2, comp128v3=3, xor=14))
|
||||
self._construct = Struct('cfg'/CfgByte,
|
||||
'key'/Bytes(16),
|
||||
'op_opc'/Bytes(16))
|
||||
'key'/HexAdapter(Bytes(16)),
|
||||
'op_opc' /HexAdapter(Bytes(16)))
|
||||
|
||||
|
||||
class EF_GBA_SK(TransparentEF):
|
||||
@@ -303,9 +299,9 @@ class EF_GBA_INT_KEY(LinFixedEF):
|
||||
|
||||
|
||||
class SysmocomSJA2(CardModel):
|
||||
_atrs = ["3b9f96801f878031e073fe211b674a4c753034054ba9",
|
||||
"3b9f96801f878031e073fe211b674a4c7531330251b2",
|
||||
"3b9f96801f878031e073fe211b674a4c5275310451d5"]
|
||||
_atrs = ["3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 30 34 05 4B A9",
|
||||
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 75 31 33 02 51 B2",
|
||||
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 4C 52 75 31 04 51 D5"]
|
||||
|
||||
@classmethod
|
||||
def add_files(cls, rs: RuntimeState):
|
||||
@@ -334,9 +330,9 @@ class SysmocomSJA2(CardModel):
|
||||
isim_adf.add_files(files_adf_isim)
|
||||
|
||||
class SysmocomSJA5(CardModel):
|
||||
_atrs = ["3b9f96801f878031e073fe211b674a357530350251cc",
|
||||
"3b9f96801f878031e073fe211b674a357530350265f8",
|
||||
"3b9f96801f878031e073fe211b674a357530350259c4"]
|
||||
_atrs = ["3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 51 CC",
|
||||
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 65 F8",
|
||||
"3B 9F 96 80 1F 87 80 31 E0 73 FE 21 1B 67 4A 35 75 30 35 02 59 C4"]
|
||||
|
||||
@classmethod
|
||||
def add_files(cls, rs: RuntimeState):
|
||||
|
||||
@@ -119,11 +119,6 @@ class LinkBase(abc.ABC):
|
||||
"""Connect to a card immediately
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_atr(self) -> Hexstr:
|
||||
"""Retrieve card ATR
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def disconnect(self):
|
||||
"""Disconnect from card
|
||||
@@ -164,8 +159,8 @@ class LinkBase(abc.ABC):
|
||||
if self.apdu_tracer:
|
||||
self.apdu_tracer.trace_response(apdu, sw, data)
|
||||
|
||||
# The APDU case (See also ISO/IEC 7816-3, table 12) dictates if we should receive a response or not. If we
|
||||
# receive a response in an APDU case that does not allow the reception of a response we print a warning to
|
||||
# The APDU case (See aso ISO/IEC 7816-3, table 12) dictates if we should receive a response or not. If we
|
||||
# receive a response in an APDU case that does not allow the reception of a respnse we print a warning to
|
||||
# make the user/caller aware of the problem. Since the transaction is over at this point and data was received
|
||||
# we count it as a successful transaction anyway, even though the spec was violated. The problem is most likely
|
||||
# caused by a missing Le field in the APDU. This is an error that the caller/user should correct to avoid
|
||||
@@ -333,11 +328,13 @@ def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
|
||||
from pySim.transport.pcsc import PcscSimLink
|
||||
from pySim.transport.modem_atcmd import ModemATCommandLink
|
||||
from pySim.transport.calypso import CalypsoSimLink
|
||||
from pySim.transport.wsrc import WsrcSimLink
|
||||
|
||||
SerialSimLink.argparse_add_reader_args(arg_parser)
|
||||
PcscSimLink.argparse_add_reader_args(arg_parser)
|
||||
ModemATCommandLink.argparse_add_reader_args(arg_parser)
|
||||
CalypsoSimLink.argparse_add_reader_args(arg_parser)
|
||||
WsrcSimLink.argparse_add_reader_args(arg_parser)
|
||||
arg_parser.add_argument('--apdu-trace', action='store_true',
|
||||
help='Trace the command/response APDUs exchanged with the card')
|
||||
|
||||
@@ -360,6 +357,9 @@ def init_reader(opts, **kwargs) -> LinkBase:
|
||||
elif opts.modem_dev is not None:
|
||||
from pySim.transport.modem_atcmd import ModemATCommandLink
|
||||
sl = ModemATCommandLink(opts, **kwargs)
|
||||
elif opts.wsrc_server_url is not None:
|
||||
from pySim.transport.wsrc import WsrcSimLink
|
||||
sl = WsrcSimLink(opts, **kwargs)
|
||||
else: # Serial reader is default
|
||||
print("No reader/driver specified; falling back to default (Serial reader)")
|
||||
from pySim.transport.serial import SerialSimLink
|
||||
|
||||
@@ -123,9 +123,6 @@ class CalypsoSimLink(LinkBaseTpdu):
|
||||
def connect(self):
|
||||
self.reset_card()
|
||||
|
||||
def get_atr(self) -> Hexstr:
|
||||
return "3b00" # Dummy ATR
|
||||
|
||||
def disconnect(self):
|
||||
pass # Nothing to do really ...
|
||||
|
||||
|
||||
@@ -139,9 +139,6 @@ class ModemATCommandLink(LinkBaseTpdu):
|
||||
def connect(self):
|
||||
pass # Nothing to do really ...
|
||||
|
||||
def get_atr(self) -> Hexstr:
|
||||
return "3b00" # Dummy ATR
|
||||
|
||||
def disconnect(self):
|
||||
pass # Nothing to do really ...
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ class PcscSimLink(LinkBaseTpdu):
|
||||
raise NoCardError() from exc
|
||||
|
||||
def get_atr(self) -> Hexstr:
|
||||
return i2h(self._con.getATR())
|
||||
return self._con.getATR()
|
||||
|
||||
def disconnect(self):
|
||||
self._con.disconnect()
|
||||
|
||||
@@ -21,7 +21,7 @@ import os
|
||||
import argparse
|
||||
from typing import Optional
|
||||
import serial
|
||||
from osmocom.utils import h2b, b2h, i2h, Hexstr
|
||||
from osmocom.utils import h2b, b2h, Hexstr
|
||||
|
||||
from pySim.exceptions import NoCardError, ProtocolError
|
||||
from pySim.transport import LinkBaseTpdu
|
||||
@@ -96,7 +96,7 @@ class SerialSimLink(LinkBaseTpdu):
|
||||
self.reset_card()
|
||||
|
||||
def get_atr(self) -> Hexstr:
|
||||
return i2h(self._atr)
|
||||
return self._atr
|
||||
|
||||
def disconnect(self):
|
||||
pass # Nothing to do really ...
|
||||
|
||||
99
pySim/transport/wsrc.py
Normal file
99
pySim/transport/wsrc.py
Normal file
@@ -0,0 +1,99 @@
|
||||
# Copyright (C) 2024 Harald Welte <laforge@gnumonks.org>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 2 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 General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
import argparse
|
||||
from typing import Optional
|
||||
|
||||
from osmocom.utils import h2i, i2h, Hexstr, is_hexstr
|
||||
|
||||
from pySim.exceptions import NoCardError, ProtocolError, ReaderError
|
||||
from pySim.transport import LinkBase
|
||||
from pySim.utils import ResTuple
|
||||
from pySim.wsrc import WSRC_DEFAULT_PORT_USER
|
||||
from pySim.wsrc.client_blocking import WsClientBlocking
|
||||
|
||||
class UserWsClientBlocking(WsClientBlocking):
|
||||
def __init__(self, ws_uri: str, **kwargs):
|
||||
super().__init__('user', ws_uri, **kwargs)
|
||||
|
||||
def select_card(self, id_type:str, id_str:str):
|
||||
rx = self.transceive_json('select_card', {'id_type': id_type, 'id_str': id_str},
|
||||
'select_card_ack')
|
||||
return rx
|
||||
|
||||
def reset_card(self):
|
||||
self.transceive_json('reset_req', {}, 'reset_resp')
|
||||
|
||||
def xceive_apdu_raw(self, cmd: Hexstr) -> ResTuple:
|
||||
rx = self.transceive_json('c_apdu', {'command': cmd}, 'r_apdu')
|
||||
return rx['response'], rx['sw']
|
||||
|
||||
|
||||
class WsrcSimLink(LinkBase):
|
||||
""" pySim: WSRC (WebSocket Remote Card) reader transport link."""
|
||||
name = 'WSRC'
|
||||
|
||||
def __init__(self, opts: argparse.Namespace, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.identities = {}
|
||||
self.server_url = opts.wsrc_server_url
|
||||
if opts.wsrc_eid:
|
||||
self.id_type = 'eid'
|
||||
self.id_str = opts.wsrc_eid
|
||||
elif opts.wsrc_iccid:
|
||||
self.id_type = 'iccid'
|
||||
self.id_str = opts.wsrc_iccid
|
||||
self.client = UserWsClientBlocking(self.server_url)
|
||||
self.client.connect()
|
||||
|
||||
def __del__(self):
|
||||
# FIXME: disconnect from server
|
||||
pass
|
||||
|
||||
def wait_for_card(self, timeout: Optional[int] = None, newcardonly: bool = False):
|
||||
self.connect()
|
||||
|
||||
def connect(self):
|
||||
rx = self.client.select_card(self.id_type, self.id_str)
|
||||
self.identities = rx['identities']
|
||||
|
||||
def get_atr(self) -> Hexstr:
|
||||
return h2i(self.identities['ATR'])
|
||||
|
||||
def disconnect(self):
|
||||
self.__delete__()
|
||||
|
||||
def _reset_card(self):
|
||||
self.client.reset_card()
|
||||
return 1
|
||||
|
||||
def _send_apdu_raw(self, pdu: Hexstr) -> ResTuple:
|
||||
return self.client.xceive_apdu_raw(pdu)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "WSRC[%s=%s]" % (self.id_type, self.id_str)
|
||||
|
||||
@staticmethod
|
||||
def argparse_add_reader_args(arg_parser: argparse.ArgumentParser):
|
||||
wsrc_group = arg_parser.add_argument_group('WebSocket Remote Card',
|
||||
"""WebSocket Remote Card (WSRC) is a protoocl by which remot cards / card readers
|
||||
can be accessed via a network.""")
|
||||
wsrc_group.add_argument('--wsrc-server-url', default='ws://localhost:%u' % WSRC_DEFAULT_PORT_USER,
|
||||
help='URI of the WSRC server to connect to')
|
||||
wsrc_group.add_argument('--wsrc-iccid', type=is_hexstr,
|
||||
help='ICCID of the card to open via WSRC')
|
||||
wsrc_group.add_argument('--wsrc-eid', type=is_hexstr,
|
||||
help='EID of the card to open via WSRC')
|
||||
@@ -18,22 +18,16 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
"""
|
||||
from bidict import bidict
|
||||
|
||||
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, GreedyString, ValidationError
|
||||
from construct import Select, Const, Bit, Struct, Int16ub, FlagsEnum, ValidationError
|
||||
from construct import Optional as COptional, Computed
|
||||
|
||||
from osmocom.construct import *
|
||||
from osmocom.utils import *
|
||||
from osmocom.tlv import *
|
||||
from osmocom.tlv import BER_TLV_IE, flatten_dict_lists
|
||||
from pySim.utils import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.profile import CardProfile
|
||||
from pySim import iso7816_4
|
||||
|
||||
# A UICC will usually also support 2G functionality. If this is the case, we
|
||||
# need to add DF_GSM and DF_TELECOM along with the UICC related files
|
||||
from pySim.ts_51_011 import AddonSIM, EF_ICCID, EF_PL
|
||||
from pySim.gsm_r import AddonGSMR
|
||||
from pySim.cdma_ruim import AddonRUIM
|
||||
#from pySim.filesystem import *
|
||||
#from pySim.profile import CardProfile
|
||||
|
||||
ts_102_22x_cmdset = CardCommandSet('TS 102 22x', [
|
||||
# TS 102 221 Section 10.1.2 Table 10.5 "Coding of Instruction Byte"
|
||||
@@ -119,11 +113,11 @@ class FileDescriptor(BER_TLV_IE, tag=0x82):
|
||||
|
||||
# ETSI TS 102 221 11.1.1.4.4
|
||||
class FileIdentifier(BER_TLV_IE, tag=0x83):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# ETSI TS 102 221 11.1.1.4.5
|
||||
class DfName(BER_TLV_IE, tag=0x84):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# ETSI TS 102 221 11.1.1.4.6.1
|
||||
class UiccCharacteristics(BER_TLV_IE, tag=0x80):
|
||||
@@ -217,7 +211,7 @@ class SecurityAttribExpanded(BER_TLV_IE, tag=0xab):
|
||||
# ETSI TS 102 221 11.1.1.4.7.3
|
||||
class SecurityAttribReferenced(BER_TLV_IE, tag=0x8b):
|
||||
# TODO: longer format with SEID
|
||||
_construct = Struct('ef_arr_file_id'/Bytes(2), 'ef_arr_record_nr'/Int8ub)
|
||||
_construct = Struct('ef_arr_file_id'/HexAdapter(Bytes(2)), 'ef_arr_record_nr'/Int8ub)
|
||||
|
||||
# ETSI TS 102 221 11.1.1.4.8
|
||||
class ShortFileIdentifier(BER_TLV_IE, tag=0x88):
|
||||
@@ -288,6 +282,12 @@ class FcpTemplate(BER_TLV_IE, tag=0x62, nested=[FileSize, TotalFileSize, FileDes
|
||||
PinStatusTemplate_DO]):
|
||||
pass
|
||||
|
||||
def decode_select_response(data_hex: str) -> object:
|
||||
"""ETSI TS 102 221 Section 11.1.1.3"""
|
||||
t = FcpTemplate()
|
||||
t.from_tlv(h2b(data_hex))
|
||||
d = t.to_dict()
|
||||
return flatten_dict_lists(d['fcp_template'])
|
||||
|
||||
def tlv_key_replace(inmap, indata):
|
||||
def newkey(inmap, key):
|
||||
@@ -634,361 +634,3 @@ NOT_DO = Nested_DO('not', 0xaf, NOT_Template)
|
||||
SC_DO = DataObjectChoice('security_condition', 'Security Condition',
|
||||
members=[Always_DO, Never_DO, SecCondByte_DO(), SecCondByte_DO(0x9e), CRT_DO(),
|
||||
OR_DO, AND_DO, NOT_DO])
|
||||
|
||||
# TS 102 221 Section 13.1
|
||||
class EF_DIR(LinFixedEF):
|
||||
_test_de_encode = [
|
||||
( '61294f10a0000000871002ffffffff890709000050055553696d31730ea00c80011781025f608203454150',
|
||||
{ "application_template": [ { "application_id": h2b("a0000000871002ffffffff8907090000") },
|
||||
{ "application_label": "USim1" },
|
||||
{ "discretionary_template": h2b("a00c80011781025f608203454150") } ] }
|
||||
),
|
||||
( '61194f10a0000000871004ffffffff890709000050054953696d31',
|
||||
{ "application_template": [ { "application_id": h2b("a0000000871004ffffffff8907090000") },
|
||||
{ "application_label": "ISim1" } ] }
|
||||
),
|
||||
]
|
||||
class ApplicationLabel(BER_TLV_IE, tag=0x50):
|
||||
# TODO: UCS-2 coding option as per Annex A of TS 102 221
|
||||
_construct = GreedyString('ascii')
|
||||
|
||||
# see https://github.com/PyCQA/pylint/issues/5794
|
||||
#pylint: disable=undefined-variable
|
||||
class ApplicationTemplate(BER_TLV_IE, tag=0x61,
|
||||
nested=[iso7816_4.ApplicationId, ApplicationLabel, iso7816_4.FileReference,
|
||||
iso7816_4.CommandApdu, iso7816_4.DiscretionaryData,
|
||||
iso7816_4.DiscretionaryTemplate, iso7816_4.URL,
|
||||
iso7816_4.ApplicationRelatedDOSet]):
|
||||
pass
|
||||
|
||||
def __init__(self, fid='2f00', sfid=0x1e, name='EF.DIR', desc='Application Directory'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, rec_len=(5, 54))
|
||||
self._tlv = EF_DIR.ApplicationTemplate
|
||||
|
||||
|
||||
# TS 102 221 Section 13.4
|
||||
class EF_ARR(LinFixedEF):
|
||||
_test_de_encode = [
|
||||
( '800101a40683010a950108800106900080016097008401d4a40683010a950108',
|
||||
[ [ { "access_mode": [ "read_search_compare" ] },
|
||||
{ "control_reference_template": "ADM1" } ],
|
||||
[ { "access_mode": [ "write_append", "update_erase" ] },
|
||||
{ "always": None } ],
|
||||
[ { "access_mode": [ "delete_file", "terminate_ef" ] },
|
||||
{ "never": None } ],
|
||||
[ { "command_header": { "INS": 212 } },
|
||||
{ "control_reference_template": "ADM1" } ]
|
||||
] ),
|
||||
( '80010190008001029700800118a40683010a9501088401d4a40683010a950108',
|
||||
[ [ { "access_mode": [ "read_search_compare" ] },
|
||||
{ "always": None } ],
|
||||
[ { "access_mode": [ "update_erase" ] },
|
||||
{ "never": None } ],
|
||||
[ { "access_mode": [ "activate_file_or_record", "deactivate_file_or_record" ] },
|
||||
{ "control_reference_template": "ADM1" } ],
|
||||
[ { "command_header": { "INS": 212 } },
|
||||
{ "control_reference_template": "ADM1" } ]
|
||||
] ),
|
||||
]
|
||||
def __init__(self, fid='2f06', sfid=0x06, name='EF.ARR', desc='Access Rule Reference'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc)
|
||||
# add those commands to the general commands of a TransparentEF
|
||||
self.shell_commands += [self.AddlShellCommands()]
|
||||
|
||||
@staticmethod
|
||||
def flatten(inp: list):
|
||||
"""Flatten the somewhat deep/complex/nested data returned from decoder."""
|
||||
def sc_abbreviate(sc):
|
||||
if 'always' in sc:
|
||||
return 'always'
|
||||
elif 'never' in sc:
|
||||
return 'never'
|
||||
elif 'control_reference_template' in sc:
|
||||
return sc['control_reference_template']
|
||||
else:
|
||||
return sc
|
||||
|
||||
by_mode = {}
|
||||
for t in inp:
|
||||
am = t[0]
|
||||
sc = t[1]
|
||||
sc_abbr = sc_abbreviate(sc)
|
||||
if 'access_mode' in am:
|
||||
for m in am['access_mode']:
|
||||
by_mode[m] = sc_abbr
|
||||
elif 'command_header' in am:
|
||||
ins = am['command_header']['INS']
|
||||
if 'CLA' in am['command_header']:
|
||||
cla = am['command_header']['CLA']
|
||||
else:
|
||||
cla = None
|
||||
cmd = ts_102_22x_cmdset.lookup(ins, cla)
|
||||
if cmd:
|
||||
name = cmd.name.lower().replace(' ', '_')
|
||||
by_mode[name] = sc_abbr
|
||||
else:
|
||||
raise ValueError
|
||||
else:
|
||||
raise ValueError
|
||||
return by_mode
|
||||
|
||||
def _decode_record_bin(self, raw_bin_data, **kwargs):
|
||||
# we can only guess if we should decode for EF or DF here :(
|
||||
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
||||
dec = arr_seq.decode_multi(raw_bin_data)
|
||||
# we cannot pass the result through flatten() here, as we don't have a related
|
||||
# 'un-flattening' decoder, and hence would be unable to encode :(
|
||||
return dec[0]
|
||||
|
||||
def _encode_record_bin(self, in_json, **kwargs):
|
||||
# we can only guess if we should decode for EF or DF here :(
|
||||
arr_seq = DataObjectSequence('arr', sequence=[AM_DO_EF, SC_DO])
|
||||
return arr_seq.encode_multi(in_json)
|
||||
|
||||
@with_default_category('File-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_rec_dec_parser)
|
||||
def do_read_arr_record(self, opts):
|
||||
"""Read one EF.ARR record in flattened, human-friendly form."""
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(opts.record_nr)
|
||||
data = self._cmd.lchan.selected_file.flatten(data)
|
||||
self._cmd.poutput_json(data, opts.oneline)
|
||||
|
||||
@cmd2.with_argparser(LinFixedEF.ShellCommands.read_recs_dec_parser)
|
||||
def do_read_arr_records(self, opts):
|
||||
"""Read + decode all EF.ARR records in flattened, human-friendly form."""
|
||||
num_of_rec = self._cmd.lchan.selected_file_num_of_rec()
|
||||
# collect all results in list so they are rendered as JSON list when printing
|
||||
data_list = []
|
||||
for recnr in range(1, 1 + num_of_rec):
|
||||
(data, _sw) = self._cmd.lchan.read_record_dec(recnr)
|
||||
data = self._cmd.lchan.selected_file.flatten(data)
|
||||
data_list.append(data)
|
||||
self._cmd.poutput_json(data_list, opts.oneline)
|
||||
|
||||
|
||||
# TS 102 221 Section 13.6
|
||||
class EF_UMPC(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '3cff02', { "max_current_mA": 60, "t_op_s": 255,
|
||||
"addl_info": { "req_inc_idle_current": False, "support_uicc_suspend": True } } ),
|
||||
( '320500', { "max_current_mA": 50, "t_op_s": 5, "addl_info": {"req_inc_idle_current": False,
|
||||
"support_uicc_suspend": False } } ),
|
||||
]
|
||||
def __init__(self, fid='2f08', sfid=0x08, name='EF.UMPC', desc='UICC Maximum Power Consumption'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=(5, 5))
|
||||
addl_info = FlagsEnum(Byte, req_inc_idle_current=1,
|
||||
support_uicc_suspend=2)
|
||||
self._construct = Struct(
|
||||
'max_current_mA'/Int8ub, 't_op_s'/Int8ub, 'addl_info'/addl_info)
|
||||
|
||||
|
||||
class CardProfileUICC(CardProfile):
|
||||
|
||||
ORDER = 10
|
||||
|
||||
def __init__(self, name='UICC'):
|
||||
files = [
|
||||
EF_DIR(),
|
||||
EF_ICCID(),
|
||||
EF_PL(),
|
||||
EF_ARR(),
|
||||
# FIXME: DF.CD
|
||||
EF_UMPC(),
|
||||
]
|
||||
addons = [
|
||||
AddonSIM,
|
||||
AddonGSMR,
|
||||
AddonRUIM,
|
||||
]
|
||||
sw = {
|
||||
'Normal': {
|
||||
'9000': 'Normal ending of the command',
|
||||
'91xx': 'Normal ending of the command, with extra information from the proactive UICC containing a command for the terminal',
|
||||
'92xx': 'Normal ending of the command, with extra information concerning an ongoing data transfer session',
|
||||
},
|
||||
'Postponed processing': {
|
||||
'9300': 'SIM Application Toolkit is busy. Command cannot be executed at present, further normal commands are allowed',
|
||||
},
|
||||
'Warnings': {
|
||||
'6200': 'No information given, state of non-volatile memory unchanged',
|
||||
'6281': 'Part of returned data may be corrupted',
|
||||
'6282': 'End of file/record reached before reading Le bytes or unsuccessful search',
|
||||
'6283': 'Selected file invalidated/disabled; needs to be activated before use',
|
||||
'6284': 'Selected file in termination state',
|
||||
'62f1': 'More data available',
|
||||
'62f2': 'More data available and proactive command pending',
|
||||
'62f3': 'Response data available',
|
||||
'63f1': 'More data expected',
|
||||
'63f2': 'More data expected and proactive command pending',
|
||||
'63cx': 'Command successful but after using an internal update retry routine X times',
|
||||
},
|
||||
'Execution errors': {
|
||||
'6400': 'No information given, state of non-volatile memory unchanged',
|
||||
'6500': 'No information given, state of non-volatile memory changed',
|
||||
'6581': 'Memory problem',
|
||||
},
|
||||
'Checking errors': {
|
||||
'6700': 'Wrong length',
|
||||
'67xx': 'The interpretation of this status word is command dependent',
|
||||
'6b00': 'Wrong parameter(s) P1-P2',
|
||||
'6d00': 'Instruction code not supported or invalid',
|
||||
'6e00': 'Class not supported',
|
||||
'6f00': 'Technical problem, no precise diagnosis',
|
||||
'6fxx': 'The interpretation of this status word is command dependent',
|
||||
},
|
||||
'Functions in CLA not supported': {
|
||||
'6800': 'No information given',
|
||||
'6881': 'Logical channel not supported',
|
||||
'6882': 'Secure messaging not supported',
|
||||
},
|
||||
'Command not allowed': {
|
||||
'6900': 'No information given',
|
||||
'6981': 'Command incompatible with file structure',
|
||||
'6982': 'Security status not satisfied',
|
||||
'6983': 'Authentication/PIN method blocked',
|
||||
'6984': 'Referenced data invalidated',
|
||||
'6985': 'Conditions of use not satisfied',
|
||||
'6986': 'Command not allowed (no EF selected)',
|
||||
'6989': 'Command not allowed - secure channel - security not satisfied',
|
||||
},
|
||||
'Wrong parameters': {
|
||||
'6a80': 'Incorrect parameters in the data field',
|
||||
'6a81': 'Function not supported',
|
||||
'6a82': 'File not found',
|
||||
'6a83': 'Record not found',
|
||||
'6a84': 'Not enough memory space',
|
||||
'6a86': 'Incorrect parameters P1 to P2',
|
||||
'6a87': 'Lc inconsistent with P1 to P2',
|
||||
'6a88': 'Referenced data not found',
|
||||
},
|
||||
'Application errors': {
|
||||
'9850': 'INCREASE cannot be performed, max value reached',
|
||||
'9862': 'Authentication error, application specific',
|
||||
'9863': 'Security session or association expired',
|
||||
'9864': 'Minimum UICC suspension time is too long',
|
||||
},
|
||||
}
|
||||
|
||||
super().__init__(name, desc='ETSI TS 102 221', cla="00",
|
||||
sel_ctrl="0004", files_in_mf=files, sw=sw,
|
||||
shell_cmdsets = [self.AddlShellCommands()], addons = addons)
|
||||
|
||||
@staticmethod
|
||||
def decode_select_response(data_hex: str) -> object:
|
||||
"""ETSI TS 102 221 Section 11.1.1.3"""
|
||||
t = FcpTemplate()
|
||||
t.from_tlv(h2b(data_hex))
|
||||
d = t.to_dict()
|
||||
return flatten_dict_lists(d['fcp_template'])
|
||||
|
||||
@classmethod
|
||||
def _try_match_card(cls, scc: SimCardCommands) -> None:
|
||||
""" Try to access MF via UICC APDUs (3GPP TS 102.221), if this works, the
|
||||
card is considered a UICC card."""
|
||||
cls._mf_select_test(scc, "00", "0004", ["3f00"])
|
||||
|
||||
@with_default_category('TS 102 221 Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
suspend_uicc_parser = argparse.ArgumentParser()
|
||||
suspend_uicc_parser.add_argument('--min-duration-secs', type=int, default=60,
|
||||
help='Proposed minimum duration of suspension')
|
||||
suspend_uicc_parser.add_argument('--max-duration-secs', type=int, default=24*60*60,
|
||||
help='Proposed maximum duration of suspension')
|
||||
|
||||
# not ISO7816-4 but TS 102 221
|
||||
@cmd2.with_argparser(suspend_uicc_parser)
|
||||
def do_suspend_uicc(self, opts):
|
||||
"""Perform the SUSPEND UICC command. Only supported on some UICC (check EF.UMPC)."""
|
||||
(duration, token, sw) = self._cmd.card._scc.suspend_uicc(min_len_secs=opts.min_duration_secs,
|
||||
max_len_secs=opts.max_duration_secs)
|
||||
self._cmd.poutput(
|
||||
'Negotiated Duration: %u secs, Token: %s, SW: %s' % (duration, token, sw))
|
||||
|
||||
resume_uicc_parser = argparse.ArgumentParser()
|
||||
resume_uicc_parser.add_argument('TOKEN', type=str, help='Token provided during SUSPEND')
|
||||
|
||||
@cmd2.with_argparser(resume_uicc_parser)
|
||||
def do_resume_uicc(self, opts):
|
||||
"""Perform the REUSME UICC operation. Only supported on some UICC. Also: A power-cycle
|
||||
of the card is required between SUSPEND and RESUME, and only very few non-RESUME
|
||||
commands are permitted between SUSPEND and RESUME. See TS 102 221 Section 11.1.22."""
|
||||
self._cmd.card._scc.resume_uicc(opts.TOKEN)
|
||||
|
||||
term_cap_parser = argparse.ArgumentParser()
|
||||
# power group
|
||||
tc_power_grp = term_cap_parser.add_argument_group('Terminal Power Supply')
|
||||
tc_power_grp.add_argument('--used-supply-voltage-class', type=str, choices=['a','b','c','d','e'],
|
||||
help='Actual used Supply voltage class')
|
||||
tc_power_grp.add_argument('--maximum-available-power-supply', type=auto_uint8,
|
||||
help='Maximum available power supply of the terminal')
|
||||
tc_power_grp.add_argument('--actual-used-freq-100k', type=auto_uint8,
|
||||
help='Actual used clock frequency (in units of 100kHz)')
|
||||
# no separate groups for those two
|
||||
tc_elc_grp = term_cap_parser.add_argument_group('Extended logical channels terminal support')
|
||||
tc_elc_grp.add_argument('--extended-logical-channel', action='store_true',
|
||||
help='Extended Logical Channel supported')
|
||||
tc_aif_grp = term_cap_parser.add_argument_group('Additional interfaces support')
|
||||
tc_aif_grp.add_argument('--uicc-clf', action='store_true',
|
||||
help='Local User Interface in the Device (LUId) supported')
|
||||
# eUICC group
|
||||
tc_euicc_grp = term_cap_parser.add_argument_group('Additional Terminal capability indications related to eUICC')
|
||||
tc_euicc_grp.add_argument('--lui-d', action='store_true',
|
||||
help='Local User Interface in the Device (LUId) supported')
|
||||
tc_euicc_grp.add_argument('--lpd-d', action='store_true',
|
||||
help='Local Profile Download in the Device (LPDd) supported')
|
||||
tc_euicc_grp.add_argument('--lds-d', action='store_true',
|
||||
help='Local Discovery Service in the Device (LPDd) supported')
|
||||
tc_euicc_grp.add_argument('--lui-e-scws', action='store_true',
|
||||
help='LUIe based on SCWS supported')
|
||||
tc_euicc_grp.add_argument('--metadata-update-alerting', action='store_true',
|
||||
help='Metadata update alerting supported')
|
||||
tc_euicc_grp.add_argument('--enterprise-capable-device', action='store_true',
|
||||
help='Enterprise Capable Device')
|
||||
tc_euicc_grp.add_argument('--lui-e-e4e', action='store_true',
|
||||
help='LUIe using E4E (ENVELOPE tag E4) supported')
|
||||
tc_euicc_grp.add_argument('--lpr', action='store_true',
|
||||
help='LPR (LPA Proxy) supported')
|
||||
|
||||
@cmd2.with_argparser(term_cap_parser)
|
||||
def do_terminal_capability(self, opts):
|
||||
"""Perform the TERMINAL CAPABILITY function. Used to inform the UICC about terminal capability."""
|
||||
ps_flags = {}
|
||||
addl_if_flags = {}
|
||||
euicc_flags = {}
|
||||
|
||||
opts_dict = vars(opts)
|
||||
|
||||
power_items = ['used_supply_voltage_class', 'maximum_available_power_supply', 'actual_used_freq_100k']
|
||||
if any(opts_dict[x] for x in power_items):
|
||||
if not all(opts_dict[x] for x in power_items):
|
||||
raise argparse.ArgumentTypeError('If any of the Terminal Power Supply group options are used, all must be specified')
|
||||
|
||||
for k, v in opts_dict.items():
|
||||
if k in AdditionalInterfacesSupport._construct.flags.keys():
|
||||
addl_if_flags[k] = v
|
||||
elif k in AdditionalTermCapEuicc._construct.flags.keys():
|
||||
euicc_flags[k] = v
|
||||
elif k in [f.name for f in TerminalPowerSupply._construct.subcons]:
|
||||
if k == 'used_supply_voltage_class' and v:
|
||||
v = {v: True}
|
||||
ps_flags[k] = v
|
||||
|
||||
child_list = []
|
||||
if any(x for x in ps_flags.values()):
|
||||
child_list.append(TerminalPowerSupply(decoded=ps_flags))
|
||||
|
||||
if opts.extended_logical_channel:
|
||||
child_list.append(ExtendedLchanTerminalSupport())
|
||||
if any(x for x in addl_if_flags.values()):
|
||||
child_list.append(AdditionalInterfacesSupport(decoded=addl_if_flags))
|
||||
if any(x for x in euicc_flags.values()):
|
||||
child_list.append(AdditionalTermCapEuicc(decoded=euicc_flags))
|
||||
|
||||
print(child_list)
|
||||
tc = TerminalCapability(children=child_list)
|
||||
self.terminal_capability(b2h(tc.to_tlv()))
|
||||
|
||||
def terminal_capability(self, data:Hexstr):
|
||||
cmd_hex = "80AA0000%02x%s" % (len(data)//2, data)
|
||||
_rsp_hex, _sw = self._cmd.lchan.scc.send_apdu_checksw(cmd_hex)
|
||||
|
||||
@@ -25,16 +25,6 @@ from osmocom.utils import b2h, auto_uint8, auto_uint16, is_hexstr
|
||||
|
||||
from pySim.ts_102_221 import *
|
||||
|
||||
def expand_pattern(pattern: bytes, repeat: bool, size: int) -> bytes:
|
||||
"""Expand the fill/repeat pattern as per TS 102 222 Section 6.3.2.2.2 Tags C1/C2."""
|
||||
if not repeat:
|
||||
pad_len = size - len(pattern)
|
||||
return pattern + pattern[-1:] * pad_len
|
||||
else:
|
||||
count = size // len(pattern)
|
||||
part_len = size - count * len(pattern)
|
||||
return pattern * count + pattern[:part_len]
|
||||
|
||||
@with_default_category('TS 102 222 Administrative Commands')
|
||||
class Ts102222Commands(CommandSet):
|
||||
"""Administrative commands for telecommunication applications."""
|
||||
|
||||
@@ -27,9 +27,9 @@ from pySim.filesystem import CardDF, TransparentEF
|
||||
# TS102 310 Section 7.1
|
||||
class EF_EAPKEYS(TransparentEF):
|
||||
class Msk(BER_TLV_IE, tag=0x80):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class Emsk(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
class MskCollection(TLV_IE_Collection, nested=[EF_EAPKEYS.Msk, EF_EAPKEYS.Emsk]):
|
||||
pass
|
||||
|
||||
|
||||
@@ -36,14 +36,13 @@ from osmocom.utils import is_hexstr
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
|
||||
import pySim.ts_102_221
|
||||
from pySim.ts_51_011 import EF_ACMmax, EF_AAeM, EF_eMLPP, EF_CMI, EF_PNN
|
||||
from pySim.ts_51_011 import EF_MMSN, EF_MMSICP, EF_MMSUP, EF_MMSUCP, EF_VGCS, EF_VGCSS, EF_NIA
|
||||
from pySim.ts_51_011 import EF_SMSR, EF_DCK, EF_EXT, EF_CNL, EF_OPL, EF_MBI, EF_MWIS
|
||||
from pySim.ts_51_011 import EF_CBMID, EF_CBMIR, EF_ADN, EF_CFIS, EF_SMS, EF_MSISDN, EF_SMSP, EF_SMSS
|
||||
from pySim.ts_51_011 import EF_IMSI, EF_xPLMNwAcT, EF_SPN, EF_CBMI, EF_ACC, EF_PLMNsel
|
||||
from pySim.ts_51_011 import EF_Kc, EF_CPBCCH, EF_InvScan
|
||||
from pySim.ts_102_221 import EF_ARR
|
||||
from pySim.profile.ts_51_011 import EF_ACMmax, EF_AAeM, EF_eMLPP, EF_CMI, EF_PNN
|
||||
from pySim.profile.ts_51_011 import EF_MMSN, EF_MMSICP, EF_MMSUP, EF_MMSUCP, EF_VGCS, EF_VGCSS, EF_NIA
|
||||
from pySim.profile.ts_51_011 import EF_SMSR, EF_DCK, EF_EXT, EF_CNL, EF_OPL, EF_MBI, EF_MWIS
|
||||
from pySim.profile.ts_51_011 import EF_CBMID, EF_CBMIR, EF_ADN, EF_CFIS, EF_SMS, EF_MSISDN, EF_SMSP, EF_SMSS
|
||||
from pySim.profile.ts_51_011 import EF_IMSI, EF_xPLMNwAcT, EF_SPN, EF_CBMI, EF_ACC, EF_PLMNsel
|
||||
from pySim.profile.ts_51_011 import EF_Kc, EF_CPBCCH, EF_InvScan
|
||||
from pySim.profile.ts_102_221 import EF_ARR, CardProfileUICC
|
||||
from pySim.filesystem import *
|
||||
from pySim.ts_31_102_telecom import DF_PHONEBOOK, EF_UServiceTable
|
||||
from pySim.ts_31_103_shared import EF_IMSConfigData, EF_XCAPConfigData, EF_MuDMiDConfigData
|
||||
@@ -217,7 +216,7 @@ EF_EST_map = {
|
||||
|
||||
# 3gPP TS 31.102 Section 7.5.2.1
|
||||
class SUCI_TlvDataObject(BER_TLV_IE, tag=0xA1):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
######################################################################
|
||||
# ADF.USIM
|
||||
@@ -230,7 +229,7 @@ class EF_5GS3GPPNSC(LinFixedEF):
|
||||
_construct = Int8ub
|
||||
|
||||
class K_AMF(BER_TLV_IE, tag=0x81):
|
||||
_construct = Bytes(32)
|
||||
_construct = HexAdapter(Bytes(32))
|
||||
|
||||
class UplinkNASCount(BER_TLV_IE, tag=0x82):
|
||||
_construct = Int32ub
|
||||
@@ -260,10 +259,10 @@ class EF_5GS3GPPNSC(LinFixedEF):
|
||||
# 3GPP TS 31.102 Section 4.4.11.6
|
||||
class EF_5GAUTHKEYS(TransparentEF):
|
||||
class K_AUSF(BER_TLV_IE, tag=0x80):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class K_SEAF(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class FiveGAuthKeys(TLV_IE_Collection, nested=[K_AUSF, K_SEAF]):
|
||||
pass
|
||||
@@ -281,9 +280,9 @@ class EF_SUCI_Calc_Info(TransparentEF):
|
||||
"identifier": 0,
|
||||
"key_index": 0}],
|
||||
"hnet_pubkey_list": [{"hnet_pubkey_identifier": 10, "hnet_pubkey":
|
||||
h2b("4e858c4d49d1343e6181284c47ca721730c98742cb7c6182d2e8126e08088d36")},
|
||||
"4e858c4d49d1343e6181284c47ca721730c98742cb7c6182d2e8126e08088d36"},
|
||||
{"hnet_pubkey_identifier": 11, "hnet_pubkey":
|
||||
h2b("d1bc365f4997d17ce4374e72181431cbfeba9e1b98d7618f79d48561b144672a")}]} ),
|
||||
"d1bc365f4997d17ce4374e72181431cbfeba9e1b98d7618f79d48561b144672a"}]} ),
|
||||
]
|
||||
# 3GPP TS 31.102 Section 4.4.11.8
|
||||
class ProtSchemeIdList(BER_TLV_IE, tag=0xa0):
|
||||
@@ -298,7 +297,7 @@ class EF_SUCI_Calc_Info(TransparentEF):
|
||||
|
||||
class HnetPubkey(BER_TLV_IE, tag=0x81):
|
||||
# contents according to RFC 7748 / RFC 5480
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class HnetPubkeyList(BER_TLV_IE, tag=0xa1, nested=[HnetPubkeyIdentifier, HnetPubkey]):
|
||||
pass
|
||||
@@ -425,7 +424,7 @@ class EF_Keys(TransparentEF):
|
||||
desc='Ciphering and Integrity Keys'):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size)
|
||||
self._construct = Struct(
|
||||
'ksi'/Int8ub, 'ck'/Bytes(16), 'ik'/Bytes(16))
|
||||
'ksi'/Int8ub, 'ck'/HexAdapter(Bytes(16)), 'ik'/HexAdapter(Bytes(16)))
|
||||
|
||||
# TS 31.102 Section 4.2.6
|
||||
class EF_HPPLMN(TransparentEF):
|
||||
@@ -536,15 +535,15 @@ class EF_ECC(LinFixedEF):
|
||||
class EF_LOCI(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '47d1264a62f21037211e00',
|
||||
{ "tmsi": h2b("47d1264a"), "lai": { "mcc_mnc": "262-01", "lac": h2b("3721") },
|
||||
{ "tmsi": "47d1264a", "lai": { "mcc_mnc": "262-01", "lac": "3721" },
|
||||
"rfu": 30, "lu_status": 0 } ),
|
||||
( 'ffffffff62f2200000ff01',
|
||||
{"tmsi": h2b("ffffffff"), "lai": {"mcc_mnc": "262-02", "lac": h2b("0000") }, "rfu": 255, "lu_status": 1} ),
|
||||
{"tmsi": "ffffffff", "lai": {"mcc_mnc": "262-02", "lac": "0000"}, "rfu": 255, "lu_status": 1} ),
|
||||
]
|
||||
def __init__(self, fid='6f7e', sfid=0x0b, name='EF.LOCI', desc='Location information', size=(11, 11)):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size)
|
||||
Lai = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)), 'lac'/Bytes(2))
|
||||
self._construct = Struct('tmsi'/Bytes(4), 'lai'/Lai, 'rfu'/Int8ub, 'lu_status'/Int8ub)
|
||||
Lai = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)), 'lac'/HexAdapter(Bytes(2)))
|
||||
self._construct = Struct('tmsi'/HexAdapter(Bytes(4)), 'lai'/Lai, 'rfu'/Int8ub, 'lu_status'/Int8ub)
|
||||
|
||||
# TS 31.102 Section 4.2.18
|
||||
class EF_AD(TransparentEF):
|
||||
@@ -585,15 +584,15 @@ class EF_AD(TransparentEF):
|
||||
class EF_PSLOCI(TransparentEF):
|
||||
def __init__(self, fid='6f73', sfid=0x0c, name='EF.PSLOCI', desc='PS Location information', size=(14, 14)):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size)
|
||||
self._construct = Struct('ptmsi'/Bytes(4), 'ptmsi_sig'/Bytes(3),
|
||||
'rai'/Bytes(6), 'rau_status'/Int8ub)
|
||||
self._construct = Struct('ptmsi'/HexAdapter(Bytes(4)), 'ptmsi_sig'/HexAdapter(Bytes(3)),
|
||||
'rai'/HexAdapter(Bytes(6)), 'rau_status'/Int8ub)
|
||||
|
||||
# TS 31.102 Section 4.2.33
|
||||
class EF_ICI(CyclicEF):
|
||||
def __init__(self, fid='6f80', sfid=0x14, name='EF.ICI', rec_len=(28, 48),
|
||||
desc='Incoming Call Information', **kwargs):
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
|
||||
self._construct = Struct('alpha_id'/Bytes(this._.total_len-28),
|
||||
self._construct = Struct('alpha_id'/HexAdapter(Bytes(this._.total_len-28)),
|
||||
'len_of_bcd_contents'/Int8ub,
|
||||
'ton_npi'/Int8ub,
|
||||
'call_number'/BcdAdapter(Bytes(10)),
|
||||
@@ -602,14 +601,14 @@ class EF_ICI(CyclicEF):
|
||||
'date_and_time'/BcdAdapter(Bytes(7)),
|
||||
'duration'/Int24ub,
|
||||
'status'/Byte,
|
||||
'link_to_phonebook'/Bytes(3))
|
||||
'link_to_phonebook'/HexAdapter(Bytes(3)))
|
||||
|
||||
# TS 31.102 Section 4.2.34
|
||||
class EF_OCI(CyclicEF):
|
||||
def __init__(self, fid='6f81', sfid=0x15, name='EF.OCI', rec_len=(27, 47),
|
||||
desc='Outgoing Call Information', **kwargs):
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
|
||||
self._construct = Struct('alpha_id'/Bytes(this._.total_len-27),
|
||||
self._construct = Struct('alpha_id'/HexAdapter(Bytes(this._.total_len-27)),
|
||||
'len_of_bcd_contents'/Int8ub,
|
||||
'ton_npi'/Int8ub,
|
||||
'call_number'/BcdAdapter(Bytes(10)),
|
||||
@@ -617,7 +616,7 @@ class EF_OCI(CyclicEF):
|
||||
'ext5_record_id'/Int8ub,
|
||||
'date_and_time'/BcdAdapter(Bytes(7)),
|
||||
'duration'/Int24ub,
|
||||
'link_to_phonebook'/Bytes(3))
|
||||
'link_to_phonebook'/HexAdapter(Bytes(3)))
|
||||
|
||||
# TS 31.102 Section 4.2.35
|
||||
class EF_ICT(CyclicEF):
|
||||
@@ -655,7 +654,7 @@ class EF_ACL(TransparentEF):
|
||||
def __init__(self, fid='6f57', sfid=None, name='EF.ACL', size=(32, None),
|
||||
desc='Access Point Name Control List', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||
self._construct = Struct('num_of_apns'/Int8ub, 'tlvs'/GreedyBytes)
|
||||
self._construct = Struct('num_of_apns'/Int8ub, 'tlvs'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 31.102 Section 4.2.51
|
||||
class EF_START_HFN(TransparentEF):
|
||||
@@ -705,16 +704,16 @@ class EF_MSK(LinFixedEF):
|
||||
def __init__(self, fid='6fd7', sfid=None, name='EF.MSK', desc='MBMS Service Key List', **kwargs):
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, rec_len=(20, None), **kwargs)
|
||||
msk_ts_constr = Struct('msk_id'/Int32ub, 'timestamp_counter'/Int32ub)
|
||||
self._construct = Struct('key_domain_id'/Bytes(3),
|
||||
self._construct = Struct('key_domain_id'/HexAdapter(Bytes(3)),
|
||||
'num_msk_id'/Int8ub,
|
||||
'msk_ids'/msk_ts_constr[this.num_msk_id])
|
||||
# TS 31.102 Section 4.2.81
|
||||
class EF_MUK(LinFixedEF):
|
||||
class MUK_Idr(BER_TLV_IE, tag=0x80):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class MUK_Idi(BER_TLV_IE, tag=0x82):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class MUK_ID(BER_TLV_IE, tag=0xA0, nested=[MUK_Idr, MUK_Idi]):
|
||||
pass
|
||||
@@ -732,10 +731,10 @@ class EF_MUK(LinFixedEF):
|
||||
# TS 31.102 Section 4.2.83
|
||||
class EF_GBANL(LinFixedEF):
|
||||
class NAF_ID(BER_TLV_IE, tag=0x80):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class B_TID(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class EF_GBANL_Collection(BER_TLV_IE, nested=[NAF_ID, B_TID]):
|
||||
pass
|
||||
@@ -759,7 +758,7 @@ class EF_EHPLMNPI(TransparentEF):
|
||||
# TS 31.102 Section 4.2.87
|
||||
class EF_NAFKCA(LinFixedEF):
|
||||
class NAF_KeyCentreAddress(BER_TLV_IE, tag=0x80):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
def __init__(self, fid='6fdd', sfid=None, name='EF.NAFKCA', rec_len=(None, None),
|
||||
desc='NAF Key Centre Address', **kwargs):
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, rec_len=rec_len, **kwargs)
|
||||
@@ -770,11 +769,11 @@ class EF_NCP_IP(LinFixedEF):
|
||||
class DataDestAddrRange(TLV_IE, tag=0x83):
|
||||
_construct = Struct('type_of_address'/Enum(Byte, IPv4=0x21, IPv6=0x56),
|
||||
'prefix_length'/Int8ub,
|
||||
'prefix'/GreedyBytes)
|
||||
'prefix'/HexAdapter(GreedyBytes))
|
||||
|
||||
class AccessPointName(TLV_IE, tag=0x80):
|
||||
# coded as per TS 23.003
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class Login(TLV_IE, tag=0x81):
|
||||
# as per SMS DCS TS 23.038
|
||||
@@ -803,8 +802,8 @@ class EF_EPSLOCI(TransparentEF):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||
upd_status_constr = Enum(
|
||||
Byte, updated=0, not_updated=1, roaming_not_allowed=2)
|
||||
self._construct = Struct('guti'/Bytes(12),
|
||||
'last_visited_registered_tai'/Bytes(5),
|
||||
self._construct = Struct('guti'/HexAdapter(Bytes(12)),
|
||||
'last_visited_registered_tai'/HexAdapter(Bytes(5)),
|
||||
'eps_update_status'/upd_status_constr)
|
||||
|
||||
# TS 31.102 Section 4.2.92
|
||||
@@ -813,7 +812,7 @@ class EF_EPSNSC(LinFixedEF):
|
||||
_construct = Int8ub
|
||||
|
||||
class K_ASME(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class UplinkNASCount(BER_TLV_IE, tag=0x82):
|
||||
_construct = Int32ub
|
||||
@@ -822,7 +821,7 @@ class EF_EPSNSC(LinFixedEF):
|
||||
_construct = Int32ub
|
||||
|
||||
class IDofNASAlgorithms(BER_TLV_IE, tag=0x84):
|
||||
_construct = GreedyBytes
|
||||
_construct = HexAdapter(GreedyBytes)
|
||||
|
||||
class EPS_NAS_Security_Context(BER_TLV_IE, tag=0xa0,
|
||||
nested=[KSI_ASME, K_ASME, UplinkNASCount, DownlinkNASCount,
|
||||
@@ -875,8 +874,7 @@ class EF_ePDGId(TransparentEF):
|
||||
class EF_ePDGSelection(TransparentEF):
|
||||
_test_de_encode = [
|
||||
( '800600f110000100', {'e_pdg_selection': [{'plmn': '001-01', 'epdg_priority': 1, 'epdg_fqdn_format': 'operator_identified' }] }),
|
||||
( '800600110000a001', {'e_pdg_selection': [{'plmn': '001-001', 'epdg_priority': 160, 'epdg_fqdn_format': 'location_based' }] }),
|
||||
( '800600011000a001', {'e_pdg_selection': [{'plmn': '001-010', 'epdg_priority': 160, 'epdg_fqdn_format': 'location_based' }] }),
|
||||
( '800600011000a001', {'e_pdg_selection': [{'plmn': '001-001', 'epdg_priority': 160, 'epdg_fqdn_format': 'location_based' }] }),
|
||||
]
|
||||
class ePDGSelection(BER_TLV_IE, tag=0x80):
|
||||
_construct = GreedyRange(Struct('plmn'/PlmnAdapter(Bytes(3)),
|
||||
@@ -1062,8 +1060,8 @@ class EF_5GS3GPPLOCI(TransparentEF):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, size=size, **kwargs)
|
||||
upd_status_constr = Enum(
|
||||
Byte, updated=0, not_updated=1, roaming_not_allowed=2)
|
||||
self._construct = Struct('5g_guti'/Bytes(13),
|
||||
'last_visited_registered_tai_in_5gs'/Bytes(6),
|
||||
self._construct = Struct('5g_guti'/HexAdapter(Bytes(13)),
|
||||
'last_visited_registered_tai_in_5gs'/HexAdapter(Bytes(6)),
|
||||
'5gs_update_status'/upd_status_constr)
|
||||
|
||||
# TS 31.102 Section 4.4.11.7 (Rel 15)
|
||||
@@ -1083,8 +1081,8 @@ class EF_UAC_AIC(TransparentEF):
|
||||
class EF_OPL5G(LinFixedEF):
|
||||
def __init__(self, fid='4f08', sfid=0x08, name='EF.OPL5G', desc='5GS Operator PLMN List', **kwargs):
|
||||
super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, rec_len=(10, None), **kwargs)
|
||||
Tai = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)), 'tac_min'/Bytes(3),
|
||||
'tac_max'/Bytes(3))
|
||||
Tai = Struct('mcc_mnc'/PlmnAdapter(Bytes(3)), 'tac_min'/HexAdapter(Bytes(3)),
|
||||
'tac_max'/HexAdapter(Bytes(3)))
|
||||
self._construct = Struct('tai'/Tai, 'pnn_record_id'/Int8ub)
|
||||
|
||||
# TS 31.102 Section 4.4.11.10 (Rel 15)
|
||||
@@ -1119,7 +1117,7 @@ class EF_Routing_Indicator(TransparentEF):
|
||||
# operator decides to assign less than 4 digits to Routing Indicator, the remaining digits
|
||||
# shall be coded as "1111" to fill the 4 digits coding of Routing Indicator
|
||||
self._construct = Struct('routing_indicator'/Rpad(BcdAdapter(Bytes(2)), 'f', 2),
|
||||
'rfu'/Bytes(2))
|
||||
'rfu'/HexAdapter(Bytes(2)))
|
||||
|
||||
# TS 31.102 Section 4.4.11.13 (Rel 16)
|
||||
class EF_TN3GPPSNN(TransparentEF):
|
||||
@@ -1135,14 +1133,14 @@ class EF_CAG(TransparentEF):
|
||||
def __init__(self, fid='4f0d', sfid=0x0d, name='EF.CAG',
|
||||
desc='Pre-configured CAG information list EF', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._construct = GreedyBytes
|
||||
self._construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 31.102 Section 4.4.11.15 (Rel 17)
|
||||
class EF_SOR_CMCI(TransparentEF):
|
||||
def __init__(self, fid='4f0e', sfid=0x0e, name='EF.SOR-CMCI',
|
||||
desc='Steering Of Roaming - Connected Mode Control Information', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._construct = GreedyBytes
|
||||
self._construct = HexAdapter(GreedyBytes)
|
||||
|
||||
# TS 31.102 Section 4.4.11.17 (Rel 17)
|
||||
class EF_DRI(TransparentEF):
|
||||
@@ -1153,9 +1151,9 @@ class EF_DRI(TransparentEF):
|
||||
'parameters_indicator_status'/FlagsEnum(Byte, roaming_wait_range=1,
|
||||
return_wait_range=2,
|
||||
applicability_indicator=3),
|
||||
'roaming_wait_range'/Bytes(2),
|
||||
'return_wait_range'/Bytes(2),
|
||||
'applicability_indicator'/Byte)
|
||||
'roaming_wait_range'/HexAdapter(Bytes(2)),
|
||||
'return_wait_range'/HexAdapter(Bytes(2)),
|
||||
'applicability_indicator'/HexAdapter(Byte))
|
||||
|
||||
# TS 31.102 Section 4.4.12.2 (Rel 17)
|
||||
class EF_PWS_SNPN(TransparentEF):
|
||||
@@ -1173,7 +1171,7 @@ class EF_NID(LinFixedEF):
|
||||
self._construct = Struct('assignment_mode'/Enum(Byte, coordinated_ass_opt1=0,
|
||||
self_ass=1,
|
||||
coordinated_ass_opt2=2),
|
||||
'network_identifier'/Bytes(5))
|
||||
'network_identifier'/HexAdapter(Bytes(5)))
|
||||
|
||||
# TS 31.102 Section 4.4.12 (Rel 17)
|
||||
class DF_SNPN(CardDF):
|
||||
@@ -1413,7 +1411,7 @@ class EF_5MBSUECONFIG(TransparentEF):
|
||||
'nid'/COptional(Bytes(6)))
|
||||
class Tmgi(BER_TLV_IE, tag=0x81):
|
||||
TmgiEntry = Struct('tmgi'/Bytes(6),
|
||||
'usd_fid'/Bytes(2),
|
||||
'usd_fid'/HexAdapter(Bytes(2)),
|
||||
'service_type'/FlagsEnum(Byte, mbs_service_announcement=1, mbs_user_service=2))
|
||||
_construct = GreedyRange(TmgiEntry)
|
||||
class NrArfcnList(BER_TLV_IE, tag=0x82):
|
||||
@@ -1482,7 +1480,7 @@ class EF_KAUSF_DERIVATION(TransparentEF):
|
||||
def __init__(self, fid='4f16', sfid=0x16, name='EF.KAUSF_DERIVATION',
|
||||
desc='K_AUSF derivation configuration', **kwargs):
|
||||
super().__init__(fid, sfid=sfid, name=name, desc=desc, **kwargs)
|
||||
self._construct = Struct('k_ausf_deriv_cfg'/FlagsEnum(Byte, use_msk=1), 'rfu'/GreedyBytes)
|
||||
self._construct = Struct('k_ausf_deriv_cfg'/FlagsEnum(Byte, use_msk=1), 'rfu'/HexAdapter(GreedyBytes))
|
||||
|
||||
# TS 31.102 Section 4.4.5
|
||||
class DF_WLAN(CardDF):
|
||||
@@ -1751,7 +1749,7 @@ class ADF_USIM(CardADF):
|
||||
self.add_files(files)
|
||||
|
||||
def decode_select_response(self, data_hex):
|
||||
return pySim.ts_102_221.CardProfileUICC.decode_select_response(data_hex)
|
||||
return CardProfileUICC.decode_select_response(data_hex)
|
||||
|
||||
@with_default_category('Application-Specific Commands')
|
||||
class AddlShellCommands(CommandSet):
|
||||
@@ -1826,7 +1824,7 @@ class ADF_USIM(CardADF):
|
||||
do = SUCI_TlvDataObject()
|
||||
do.from_tlv(h2b(data))
|
||||
do_d = do.to_dict()
|
||||
self._cmd.poutput('SUCI TLV Data Object: %s' % b2h(do_d['suci__tlv_data_object']))
|
||||
self._cmd.poutput('SUCI TLV Data Object: %s' % do_d['suci__tlv_data_object'])
|
||||
|
||||
|
||||
# TS 31.102 Section 7.3
|
||||
|
||||
@@ -22,17 +22,16 @@ Various constants from 3GPP TS 31.103 V18.1.0
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from construct import Struct, Switch, this, GreedyString
|
||||
from construct import Struct, Switch, this, Bytes, GreedyString
|
||||
from osmocom.utils import *
|
||||
from osmocom.tlv import *
|
||||
from osmocom.construct import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.ts_51_011 import EF_AD, EF_SMS, EF_SMSS, EF_SMSR, EF_SMSP
|
||||
from pySim.profile.ts_51_011 import EF_AD, EF_SMS, EF_SMSS, EF_SMSR, EF_SMSP
|
||||
from pySim.ts_31_102 import ADF_USIM, EF_FromPreferred
|
||||
from pySim.ts_31_102_telecom import EF_UServiceTable
|
||||
from pySim.ts_31_103_shared import *
|
||||
import pySim.ts_102_221
|
||||
from pySim.ts_102_221 import EF_ARR
|
||||
from pySim.profile.ts_102_221 import EF_ARR, CardProfileUICC
|
||||
|
||||
# Mapping between ISIM Service Number and its description
|
||||
EF_IST_map = {
|
||||
@@ -167,7 +166,7 @@ class EF_GBABP(TransparentEF):
|
||||
class EF_GBANL(LinFixedEF):
|
||||
class NAF_ID(BER_TLV_IE, tag=0x80):
|
||||
_construct = Struct('fqdn'/Utf8Adapter(Bytes(this._.total_len-5)),
|
||||
'ua_spi'/Bytes(5))
|
||||
'ua_spi'/HexAdapter(Bytes(5)))
|
||||
class B_TID(BER_TLV_IE, tag=0x81):
|
||||
_construct = Utf8Adapter(GreedyBytes)
|
||||
# pylint: disable=undefined-variable
|
||||
@@ -226,7 +225,7 @@ class ADF_ISIM(CardADF):
|
||||
self.shell_commands += [ADF_USIM.AddlShellCommands()]
|
||||
|
||||
def decode_select_response(self, data_hex):
|
||||
return pySim.ts_102_221.CardProfileUICC.decode_select_response(data_hex)
|
||||
return CardProfileUICC.decode_select_response(data_hex)
|
||||
|
||||
|
||||
# TS 31.103 Section 7.1
|
||||
|
||||
@@ -19,9 +19,9 @@ hence need to be in a separate python module to avoid circular dependencies.
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
from construct import Struct, Switch, GreedyString, Int8ub, Prefixed, Enum, Byte
|
||||
from construct import Struct, Switch, Bytes, GreedyString, GreedyBytes, Int8ub, Prefixed, Enum, Byte
|
||||
from osmocom.tlv import BER_TLV_IE, TLV_IE_Collection
|
||||
from osmocom.construct import Bytes, Utf8Adapter, GreedyBytes
|
||||
from osmocom.construct import HexAdapter, Utf8Adapter
|
||||
from pySim.filesystem import *
|
||||
|
||||
# TS 31.103 Section 4.2.16
|
||||
@@ -36,7 +36,7 @@ class EF_UICCIARI(LinFixedEF):
|
||||
# TS 31.103 Section 4.2.18
|
||||
class EF_IMSConfigData(BerTlvEF):
|
||||
class ImsConfigDataEncoding(BER_TLV_IE, tag=0x80):
|
||||
_construct = Bytes(1)
|
||||
_construct = HexAdapter(Bytes(1))
|
||||
class ImsConfigData(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyString
|
||||
# pylint: disable=undefined-variable
|
||||
@@ -103,7 +103,7 @@ class EF_WebRTCURI(LinFixedEF):
|
||||
# TS 31.103 Section 4.2.21
|
||||
class EF_MuDMiDConfigData(BerTlvEF):
|
||||
class MudMidConfigDataEncoding(BER_TLV_IE, tag=0x80):
|
||||
_construct = Bytes(1)
|
||||
_construct = HexAdapter(Bytes(1))
|
||||
class MudMidConfigData(BER_TLV_IE, tag=0x81):
|
||||
_construct = GreedyString
|
||||
# pylint: disable=undefined-variable
|
||||
|
||||
@@ -24,9 +24,8 @@ from osmocom.utils import *
|
||||
from osmocom.tlv import *
|
||||
from pySim.filesystem import *
|
||||
from pySim.ts_31_102 import ADF_USIM
|
||||
from pySim.ts_51_011 import EF_IMSI, EF_AD
|
||||
import pySim.ts_102_221
|
||||
from pySim.ts_102_221 import EF_ARR
|
||||
from pySim.profile.ts_51_011 import EF_IMSI, EF_AD
|
||||
from pySim.profile.ts_102_221 import EF_ARR, CardProfileUICC
|
||||
|
||||
|
||||
class ADF_HPSIM(CardADF):
|
||||
@@ -44,7 +43,7 @@ class ADF_HPSIM(CardADF):
|
||||
self.shell_commands += [ADF_USIM.AddlShellCommands()]
|
||||
|
||||
def decode_select_response(self, data_hex):
|
||||
return pySim.ts_102_221.CardProfileUICC.decode_select_response(data_hex)
|
||||
return CardProfileUICC.decode_select_response(data_hex)
|
||||
|
||||
|
||||
# TS 31.104 Section 7.1
|
||||
|
||||
3
pySim/wsrc/__init__.py
Normal file
3
pySim/wsrc/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
|
||||
WSRC_DEFAULT_PORT_USER = 4220
|
||||
WSRC_DEFAULT_PORT_CARD = 4221
|
||||
68
pySim/wsrc/client_blocking.py
Normal file
68
pySim/wsrc/client_blocking.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Connect smartcard to a remote server, so the remote server can take control and
|
||||
perform commands on it."""
|
||||
|
||||
import abc
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional
|
||||
from websockets.sync.client import connect
|
||||
from osmocom.utils import b2h
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WsClientBlocking(abc.ABC):
|
||||
"""Generalized synchronous/blocking client for the WSRC (Web Socket Remote Card) protocol"""
|
||||
|
||||
def __init__(self, cltype: str, ws_uri: str):
|
||||
self.client_type = cltype
|
||||
self.ws_uri = ws_uri
|
||||
self.ws = None
|
||||
|
||||
def connect(self):
|
||||
self.ws = connect(uri=self.ws_uri)
|
||||
self.perform_outbound_hello()
|
||||
|
||||
def tx_json(self, msg_type: str, d: dict = {}):
|
||||
"""JSON-Encode and transmit a message to the given websocket."""
|
||||
d['msg_type'] = msg_type
|
||||
d_js = json.dumps(d)
|
||||
logger.debug("Tx: %s", d_js)
|
||||
self.ws.send(d_js)
|
||||
|
||||
def tx_error(self, message: str):
|
||||
event = {
|
||||
'message': message,
|
||||
}
|
||||
self.tx_json('error', event)
|
||||
|
||||
def rx_json(self):
|
||||
"""Receive a single message from the given websocket and JSON-decode it."""
|
||||
rx = self.ws.recv()
|
||||
rx_js = json.loads(rx)
|
||||
logger.debug("Rx: %s", rx_js)
|
||||
assert 'msg_type' in rx
|
||||
return rx_js
|
||||
|
||||
def transceive_json(self, tx_msg_type: str, tx_d: Optional[dict], rx_msg_type: str) -> dict:
|
||||
self.tx_json(tx_msg_type, tx_d)
|
||||
rx = self.rx_json()
|
||||
assert rx['msg_type'] == rx_msg_type
|
||||
return rx
|
||||
|
||||
def perform_outbound_hello(self, tx: dict = {}):
|
||||
if not 'client_type' in tx:
|
||||
tx['client_type'] = self.client_type
|
||||
self.tx_json('hello', tx)
|
||||
rx = self.rx_json()
|
||||
assert rx['msg_type'] == 'hello_ack'
|
||||
return rx
|
||||
|
||||
def rx_and_execute_cmd(self):
|
||||
"""Receve and dispatch/execute a single command from the server."""
|
||||
rx = self.rx_json()
|
||||
handler = getattr(self, 'handle_rx_%s' % rx['msg_type'], None)
|
||||
if handler:
|
||||
handler(rx)
|
||||
else:
|
||||
logger.error('Received unknown/unsupported msg_type %s' % rx['msg_type'])
|
||||
self.tx_error('Message type "%s" is not supported' % rx['msg_type'])
|
||||
@@ -5,7 +5,7 @@ cmd2>=1.5
|
||||
jsonpath-ng
|
||||
construct>=2.10.70
|
||||
bidict
|
||||
pyosmocom>=0.0.9
|
||||
pyosmocom>=0.0.6
|
||||
pyyaml>=5.1
|
||||
termcolor
|
||||
colorlog
|
||||
@@ -14,4 +14,3 @@ cryptography
|
||||
git+https://github.com/osmocom/asn1tools
|
||||
packaging
|
||||
git+https://github.com/hologram-io/smpp.pdu
|
||||
smpp.twisted3 @ git+https://github.com/jookies/smpp.twisted
|
||||
|
||||
23
setup.py
23
setup.py
@@ -8,7 +8,6 @@ setup(
|
||||
'pySim.apdu',
|
||||
'pySim.apdu_source',
|
||||
'pySim.esim',
|
||||
'pySim.esim.saip',
|
||||
'pySim.global_platform',
|
||||
'pySim.legacy',
|
||||
'pySim.transport',
|
||||
@@ -25,36 +24,18 @@ setup(
|
||||
"jsonpath-ng",
|
||||
"construct >= 2.10.70",
|
||||
"bidict",
|
||||
"pyosmocom >= 0.0.9",
|
||||
"pyosmocom >= 0.0.6",
|
||||
"pyyaml >= 5.1",
|
||||
"termcolor",
|
||||
"colorlog",
|
||||
"pycryptodomex",
|
||||
"packaging",
|
||||
"smpp.pdu @ git+https://github.com/hologram-io/smpp.pdu",
|
||||
"asn1tools",
|
||||
"smpp.twisted3 @ git+https://github.com/jookies/smpp.twisted",
|
||||
],
|
||||
scripts=[
|
||||
'pySim-prog.py',
|
||||
'pySim-read.py',
|
||||
'pySim-shell.py',
|
||||
'pySim-trace.py',
|
||||
'pySim-smpp2sim.py',
|
||||
],
|
||||
package_data={
|
||||
'pySim.esim':
|
||||
[
|
||||
'asn1/rsp/*.asn',
|
||||
'asn1/saip/*.asn',
|
||||
],
|
||||
},
|
||||
extras_require={
|
||||
"smdpp": [
|
||||
"klein",
|
||||
"service-identity",
|
||||
"pyopenssl",
|
||||
"requests",
|
||||
]
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,16 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICgjCCAimgAwIBAgIBCTAKBggqhkjOPQQDAjBEMRAwDgYDVQQDDAdUZXN0IENJ
|
||||
MREwDwYDVQQLDAhURVNUQ0VSVDEQMA4GA1UECgwHUlNQVEVTVDELMAkGA1UEBhMC
|
||||
SVQwHhcNMjQwNzA5MTUyOTM2WhcNMjUwODExMTUyOTM2WjAzMQ0wCwYDVQQKDARB
|
||||
Q01FMSIwIAYDVQQDDBl0ZXN0c21kcHBsdXMxLmV4YW1wbGUuY29tMFowFAYHKoZI
|
||||
zj0CAQYJKyQDAwIIAQEHA0IABEwizNgsjQIh+dhUO3LhB7zJ/ZBU1mx1wOt0p73n
|
||||
MOdhjvZbJwteguQ6eW+N7guvivvrilNiU3oC/WXHnkEZa7WjggEaMIIBFjAOBgNV
|
||||
HQ8BAf8EBAMCB4AwIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBQG
|
||||
A1UdIAQNMAswCQYHZ4ESAQIBAzAdBgNVHQ4EFgQUPTMJg/OfzFvS5K1ophmnR0iu
|
||||
i50wHwYDVR0jBBgwFoAUwLxwujaSnUO0Z/9XVwUw5Xq4/NgwKQYDVR0RBCIwIIIZ
|
||||
dGVzdHNtZHBwbHVzMS5leGFtcGxlLmNvbYgDiDcKMGEGA1UdHwRaMFgwKqAooCaG
|
||||
JGh0dHA6Ly9jaS50ZXN0LmV4YW1wbGUuY29tL0NSTC1BLmNybDAqoCigJoYkaHR0
|
||||
cDovL2NpLnRlc3QuZXhhbXBsZS5jb20vQ1JMLUIuY3JsMAoGCCqGSM49BAMCA0cA
|
||||
MEQCIHHmXEy9mgudh/VbK0hJwmX7eOgbvHLnlujrpQzvUd4uAiBFVJgSdzYvrmJ9
|
||||
5yeIvmjHwxSMBgQp2dde7OtdVEK8Kw==
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
@@ -1,16 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICgzCCAiigAwIBAgIBCTAKBggqhkjOPQQDAjBEMRAwDgYDVQQDDAdUZXN0IENJ
|
||||
MREwDwYDVQQLDAhURVNUQ0VSVDEQMA4GA1UECgwHUlNQVEVTVDELMAkGA1UEBhMC
|
||||
SVQwHhcNMjQwNzA5MTUyODMzWhcNMjUwODExMTUyODMzWjAzMQ0wCwYDVQQKDARB
|
||||
Q01FMSIwIAYDVQQDDBl0ZXN0c21kcHBsdXMxLmV4YW1wbGUuY29tMFkwEwYHKoZI
|
||||
zj0CAQYIKoZIzj0DAQcDQgAEKCQwdc6O/R+uZ2g5QH2ybkzLQ3CUYhybOWEz8bJL
|
||||
tQG4/k6yTT4NOS8lP28blGJws8opLjTbb3qHs6X2rJRfCKOCARowggEWMA4GA1Ud
|
||||
DwEB/wQEAwIHgDAgBgNVHSUBAf8EFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwFAYD
|
||||
VR0gBA0wCzAJBgdngRIBAgEDMB0GA1UdDgQWBBQn/vHyKRh+x4Pt9uApZKRRjVfU
|
||||
qTAfBgNVHSMEGDAWgBT1QXK9+YqV1ly+uIo4ocEdgAqFwzApBgNVHREEIjAgghl0
|
||||
ZXN0c21kcHBsdXMxLmV4YW1wbGUuY29tiAOINwowYQYDVR0fBFowWDAqoCigJoYk
|
||||
aHR0cDovL2NpLnRlc3QuZXhhbXBsZS5jb20vQ1JMLUEuY3JsMCqgKKAmhiRodHRw
|
||||
Oi8vY2kudGVzdC5leGFtcGxlLmNvbS9DUkwtQi5jcmwwCgYIKoZIzj0EAwIDSQAw
|
||||
RgIhAL1qQ/cnrCZC7UnnLJ8WeK+0aWUJFWh1cOlBEzw0NlTVAiEA25Vf4WHzwmJi
|
||||
zkARzxJ1qB0qfBofuJrtfPM4gNJ4Quw=
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
/tmp/sm-dp-sessions-BRP
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user