mirror of
https://github.com/infiniteCable2/openpilot.git
synced 2026-08-05 00:05:57 +08:00
Merge branch 'upstream/openpilot/master' into sync-20260417
# Conflicts: # docs/CARS.md # opendbc_repo # panda # system/updated/updated.py # tinygrad_repo
This commit is contained in:
@@ -91,6 +91,9 @@ class LPABase(ABC):
|
||||
def switch_profile(self, iccid: str) -> None:
|
||||
pass
|
||||
|
||||
def process_notifications(self) -> None:
|
||||
pass
|
||||
|
||||
def is_comma_profile(self, iccid: str) -> bool:
|
||||
return any(iccid.startswith(prefix) for prefix in ('8985235',))
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# GSMA Certificate Issuer (CI) bundle for eSIM RSP
|
||||
# Source: https://euicc-manual.osmocom.org/docs/pki/ci/bundle.pem
|
||||
|
||||
issuer=
|
||||
countryName = CH
|
||||
organizationName = OISTE Foundation
|
||||
commonName = OISTE GSMA CI G1
|
||||
notBefore=2024-01-16 23:17:39Z
|
||||
notAfter=2059-01-07 23:17:38Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIB9zCCAZ2gAwIBAgIUSpBSCCDYPOEG/IFHUCKpZ2pIAQMwCgYIKoZIzj0EAwIw
|
||||
QzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRpb24xGTAXBgNV
|
||||
BAMMEE9JU1RFIEdTTUEgQ0kgRzEwIBcNMjQwMTE2MjMxNzM5WhgPMjA1OTAxMDcy
|
||||
MzE3MzhaMEMxCzAJBgNVBAYTAkNIMRkwFwYDVQQKDBBPSVNURSBGb3VuZGF0aW9u
|
||||
MRkwFwYDVQQDDBBPSVNURSBHU01BIENJIEcxMFkwEwYHKoZIzj0CAQYIKoZIzj0D
|
||||
AQcDQgAEvZ3s3PFC4NgrCcCMmHJ6DJ66uzAHuLcvjJnOn+TtBNThS7YHLDyHCa2v
|
||||
7D+zTP+XTtgqgcLoB56Gha9EQQQ4xKNtMGswDwYDVR0TAQH/BAUwAwEB/zAQBgNV
|
||||
HREECTAHiAVghXQFDjAXBgNVHSABAf8EDTALMAkGB2eBEgECAQAwHQYDVR0OBBYE
|
||||
FEwnlnrSDBSzkelgHkHmBK1XwCIvMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQD
|
||||
AgNIADBFAiBVcywTj017jKpAQ+gwy4MqK2hQvzve6lkvQkgSP6ykHwIhAI0KFwCD
|
||||
jnPbmcJsG41hUrWNlf+IcrMvFuYii0DasBNi
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
organizationName = GSM Association
|
||||
commonName = GSM Association - RSP2 Root CI1
|
||||
notBefore=2017-02-22 00:00:00Z
|
||||
notAfter=2052-02-21 23:59:59Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICSTCCAe+gAwIBAgIQbmhWeneg7nyF7hg5Y9+qejAKBggqhkjOPQQDAjBEMRgw
|
||||
FgYDVQQKEw9HU00gQXNzb2NpYXRpb24xKDAmBgNVBAMTH0dTTSBBc3NvY2lhdGlv
|
||||
biAtIFJTUDIgUm9vdCBDSTEwIBcNMTcwMjIyMDAwMDAwWhgPMjA1MjAyMjEyMzU5
|
||||
NTlaMEQxGDAWBgNVBAoTD0dTTSBBc3NvY2lhdGlvbjEoMCYGA1UEAxMfR1NNIEFz
|
||||
c29jaWF0aW9uIC0gUlNQMiBSb290IENJMTBZMBMGByqGSM49AgEGCCqGSM49AwEH
|
||||
A0IABJ1qutL0HCMX52GJ6/jeibsAqZfULWj/X10p/Min6seZN+hf5llovbCNuB2n
|
||||
unLz+O8UD0SUCBUVo8e6n9X1TuajgcAwgb0wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
|
||||
EwEB/wQFMAMBAf8wEwYDVR0RBAwwCogIKwYBBAGC6WAwFwYDVR0gAQH/BA0wCzAJ
|
||||
BgdngRIBAgEAME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9nc21hLWNybC5zeW1h
|
||||
dXRoLmNvbS9vZmZsaW5lY2EvZ3NtYS1yc3AyLXJvb3QtY2kxLmNybDAdBgNVHQ4E
|
||||
FgQUgTcPUSXQsdQI1MOyMubSXnlb6/swCgYIKoZIzj0EAwIDSAAwRQIgIJdYsOMF
|
||||
WziPK7l8nh5mu0qiRiVf25oa9ullG/OIASwCIQDqCmDrYf+GziHXBOiwJwnBaeBO
|
||||
aFsiLzIEOaUuZwdNUw==
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
countryName = US
|
||||
organizationName = Entrust, Inc.
|
||||
organizationalUnitName = See www.entrust.net/legal-terms
|
||||
organizationalUnitName = (c) 2016 Entrust, Inc. - for authorized use only
|
||||
commonName = Entrust eSIM Certification Authority
|
||||
notBefore=2016-11-16 16:04:02Z
|
||||
notAfter=2051-10-16 16:34:02Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIC6DCCAo2gAwIBAgIRAIy4GT7M5nHsAAAAAFgsinowCgYIKoZIzj0EAwIwgbkx
|
||||
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9T
|
||||
ZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAx
|
||||
NiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLTArBgNV
|
||||
BAMTJEVudHJ1c3QgZVNJTSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAgFw0xNjEx
|
||||
MTYxNjA0MDJaGA8yMDUxMTAxNjE2MzQwMlowgbkxCzAJBgNVBAYTAlVTMRYwFAYD
|
||||
VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0
|
||||
L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNiBFbnRydXN0LCBJbmMuIC0g
|
||||
Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLTArBgNVBAMTJEVudHJ1c3QgZVNJTSBD
|
||||
ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
|
||||
BAdzwGHeQ1Wb2f4DmHTByR5/IWL3JugQ1U3908a++bHdlt+TTA7K4c5cYZ+51Yz/
|
||||
hg/bacxguPDh9uQUK6Wg3a6jcjBwMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
|
||||
BAQDAgEGMBcGA1UdIAEB/wQNMAswCQYHZ4ESAQIBADAVBgNVHREEDjAMiApghkgB
|
||||
hvpsFAoAMB0GA1UdDgQWBBQWcEt/NR42B/GMS3AAXDoAPf1BSjAKBggqhkjOPQQD
|
||||
AgNJADBGAiEAspjXMvaBZyAg86Z0AAtT0yBRAi1EyaAfNz9kDJeAE04CIQC3efj8
|
||||
ATL7/tDBOhANy3cK8PS/1NIlu9vqMLCZsZvJ0Q==
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
countryName = FR
|
||||
organizationName = OBERTHUR TECHNOLOGIES
|
||||
organizationalUnitName = TELECOM
|
||||
commonName = MC4 OT ROOT CI v1
|
||||
notBefore=2016-11-15 00:00:01Z
|
||||
notAfter=2046-11-08 23:59:59Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICOjCCAeGgAwIBAgIBATAKBggqhkjOPQQDAjBbMQswCQYDVQQGEwJGUjEeMBwG
|
||||
A1UEChMVT0JFUlRIVVIgVEVDSE5PTE9HSUVTMRAwDgYDVQQLEwdURUxFQ09NMRow
|
||||
GAYDVQQDExFNQzQgT1QgUk9PVCBDSSB2MTAeFw0xNjExMTUwMDAwMDFaFw00NjEx
|
||||
MDgyMzU5NTlaMFsxCzAJBgNVBAYTAkZSMR4wHAYDVQQKExVPQkVSVEhVUiBURUNI
|
||||
Tk9MT0dJRVMxEDAOBgNVBAsTB1RFTEVDT00xGjAYBgNVBAMTEU1DNCBPVCBST09U
|
||||
IENJIHYxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHb/Gajt3OZxuaDSklBQE
|
||||
D4lOd6PGPLSvtfkM952ubdyy45tJwAeA0eEii0CLrFT6tcfXkW+H/5mQyMRXaAUk
|
||||
T6OBlTCBkjAfBgNVHSMEGDAWgBTNbmC3LXoGPLyEYluR6A/jBAbhPjAdBgNVHQ4E
|
||||
FgQUzW5gty16Bjy8hGJbkegP4wQG4T4wDgYDVR0PAQH/BAQDAgAGMBcGA1UdIAEB
|
||||
/wQNMAswCQYHZ4ESAQIBADAWBgNVHREEDzANiAsrBgEEAYHvb7OITTAPBgNVHRMB
|
||||
Af8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIEw4Nc7f2fDtoH+6ON/bknfDQxmT
|
||||
ikThXjhpLtSrSKN2AiAxHxgC87L0FDnH8dJNlkdGX9c0JIx6oLheIplfS6k+jg==
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
commonName = SubMan V4.2 CI Google Pixel
|
||||
organizationName = Giesecke and Devrient GmbH
|
||||
organizationalUnitName = Mobile Security
|
||||
countryName = DE
|
||||
notBefore=2017-05-10 00:00:00Z
|
||||
notAfter=2027-05-10 00:00:00Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICaTCCAg6gAwIBAgICASwwCgYIKoZIzj0EAwIwczElMCMGA1UEAxMcIFN1Yk1h
|
||||
biBWNC4yIENJIEdvb2dsZSBQaXhlbDEjMCEGA1UEChMaR2llc2Vja2UgYW5kIERl
|
||||
dnJpZW50IEdtYkgxGDAWBgNVBAsTD01vYmlsZSBTZWN1cml0eTELMAkGA1UEBhMC
|
||||
REUwHhcNMTcwNTEwMDAwMDAwWhcNMjcwNTEwMDAwMDAwWjBzMSUwIwYDVQQDExwg
|
||||
U3ViTWFuIFY0LjIgQ0kgR29vZ2xlIFBpeGVsMSMwIQYDVQQKExpHaWVzZWNrZSBh
|
||||
bmQgRGV2cmllbnQgR21iSDEYMBYGA1UECxMPTW9iaWxlIFNlY3VyaXR5MQswCQYD
|
||||
VQQGEwJERTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHNorfaJsGzqWNawyAhl
|
||||
IAv9QL2/+b9RsUoso06t/dKX1MRr5CUJ51acvv5TAFhQKIml+dwLbFnV5aO+8W6Z
|
||||
wxajgZEwgY4wHwYDVR0jBBgwFoAUtg8LiX/WMLiM/tYWH46oCMU4KsMwHQYDVR0O
|
||||
BBYEFLYPC4l/1jC4jP7WFh+OqAjFOCrDMA4GA1UdDwEB/wQEAwIBBjAXBgNVHSAB
|
||||
Af8EDTALMAkGB2eBEgECAQAwDwYDVR0TAQH/BAUwAwEB/zASBgNVHREECzAJiAcr
|
||||
BgEEAdwPMAoGCCqGSM49BAMCA0kAMEYCIQDpoZcuAQrjATW8U+AWqMUJ0dY6nWW1
|
||||
R1QmFzVZ1yMXSwIhALCvRqkCtgiavdeFeSgsSNbY5Fhd+QoCltuSh1U4TE7A
|
||||
-----END CERTIFICATE-----
|
||||
issuer=
|
||||
countryName = DE
|
||||
commonName = SubMan V4.2 CI
|
||||
organizationName = Giesecke and Devrient
|
||||
organizationalUnitName = Mobile Security
|
||||
notBefore=2016-08-12 13:51:48Z
|
||||
notAfter=2026-08-12 13:51:48Z
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIICUjCCAfigAwIBAgIDQgAAMAoGCCqGSM49BAMCMGAxCzAJBgNVBAYTAkRFMRcw
|
||||
FQYDVQQDEw5TdWJNYW4gVjQuMiBDSTEeMBwGA1UEChMVR2llc2Vja2UgYW5kIERl
|
||||
dnJpZW50MRgwFgYDVQQLEw9Nb2JpbGUgU2VjdXJpdHkwHhcNMTYwODEyMTM1MTQ4
|
||||
WhcNMjYwODEyMTM1MTQ4WjBgMQswCQYDVQQGEwJERTEXMBUGA1UEAxMOU3ViTWFu
|
||||
IFY0LjIgQ0kxHjAcBgNVBAoTFUdpZXNlY2tlIGFuZCBEZXZyaWVudDEYMBYGA1UE
|
||||
CxMPTW9iaWxlIFNlY3VyaXR5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYIgl
|
||||
VQr9wbXOlwPp8qMg5Df08Cli9Mc+lpr3Lwa9PlVA3QWlLeX4GfD4H3phLBqVIa17
|
||||
yHttmtheTxi0KoEqhKOBoDCBnTAdBgNVHQ4EFgQU6lOt7zMpuVCa/XVf1Ei4LcG8
|
||||
7P8wDgYDVR0PAQH/BAQDAgEGMBcGA1UdIAEB/wQNMAswCQYHZ4ESAQIBADAPBgNV
|
||||
HRMBAf8EBTADAQH/MBIGA1UdEQQLMAmIBysGAQQB3A8wLgYDVR0fBCcwJTAjoCGg
|
||||
H4YdaHR0cDovL2dpLWRlLmNvbS90ZXN0LmNybC5wZW0wCgYIKoZIzj0EAwIDSAAw
|
||||
RQIhAMMx2L/VHDiOW+Fl/OuFmhCdizYM17Yn9zAVieKO2T0iAiANWtCMmY+DzkqK
|
||||
yHxBFX0U2tBd682zP4DpgRt8j3Ylew==
|
||||
-----END CERTIFICATE-----
|
||||
+349
-2
@@ -3,8 +3,10 @@
|
||||
import atexit
|
||||
import base64
|
||||
import fcntl
|
||||
import hashlib
|
||||
import math
|
||||
import os
|
||||
import requests
|
||||
import serial
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -15,8 +17,12 @@ from collections.abc import Callable, Generator
|
||||
from contextlib import contextmanager
|
||||
from typing import Any
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.time_helpers import system_time_valid
|
||||
from openpilot.system.hardware.base import LPABase, LPAError, Profile
|
||||
|
||||
GSMA_CI_BUNDLE = str(Path(__file__).parent / "gsma_ci_bundle.pem")
|
||||
|
||||
DEFAULT_DEVICE = "/dev/modem_at0"
|
||||
DEFAULT_BAUD = 9600
|
||||
@@ -26,6 +32,7 @@ ISDR_AID = "A0000005591010FFFFFFFF8900000100"
|
||||
MM = "org.freedesktop.ModemManager1"
|
||||
MM_MODEM = MM + ".Modem"
|
||||
ES10X_MSS = 120
|
||||
HTTP_TIMEOUT = 30
|
||||
OPEN_ISDR_RETRIES = 10
|
||||
OPEN_ISDR_RETRY_DELAY_S = 0.25
|
||||
OPEN_ISDR_RESET_ATTEMPT = 5
|
||||
@@ -37,10 +44,24 @@ DEBUG = os.environ.get("DEBUG") == "1"
|
||||
# TLV Tags
|
||||
TAG_ICCID = 0x5A
|
||||
TAG_STATUS = 0x80
|
||||
TAG_PROFILE_INFO_LIST = 0xBF2D
|
||||
TAG_EUICC_INFO = 0xBF20
|
||||
TAG_PREPARE_DOWNLOAD = 0xBF21
|
||||
TAG_BPP_COMMAND = 0xBF23
|
||||
TAG_PROFILE_METADATA = 0xBF25
|
||||
TAG_INSTALL_RESULT_DATA = 0xBF27
|
||||
TAG_LIST_NOTIFICATION = 0xBF28
|
||||
TAG_SET_NICKNAME = 0xBF29
|
||||
TAG_RETRIEVE_NOTIFICATION = 0xBF2B
|
||||
TAG_PROFILE_INFO_LIST = 0xBF2D
|
||||
TAG_EUICC_CHALLENGE = 0xBF2E
|
||||
TAG_NOTIFICATION_METADATA = 0xBF2F
|
||||
TAG_NOTIFICATION_SENT = 0xBF30
|
||||
TAG_ENABLE_PROFILE = 0xBF31
|
||||
TAG_DELETE_PROFILE = 0xBF33
|
||||
TAG_BPP = 0xBF36
|
||||
TAG_PROFILE_INSTALL_RESULT = 0xBF37
|
||||
TAG_AUTH_SERVER = 0xBF38
|
||||
TAG_CANCEL_SESSION = 0xBF41
|
||||
TAG_OK = 0xA0
|
||||
|
||||
PROFILE_OK = 0x00
|
||||
@@ -52,6 +73,42 @@ PROFILE_ERROR_CODES = {
|
||||
0x03: "disallowedByPolicy", 0x04: "wrongProfileReenabling",
|
||||
PROFILE_CAT_BUSY: "catBusy", 0x06: "undefinedError",
|
||||
}
|
||||
AUTH_SERVER_ERROR_CODES = {
|
||||
0x01: "eUICCVerificationFailed", 0x02: "eUICCCertificateExpired",
|
||||
0x03: "eUICCCertificateRevoked", 0x05: "invalidServerSignature",
|
||||
0x06: "euiccCiPKUnknown", 0x0A: "matchingIdRefused",
|
||||
0x10: "insufficientMemory",
|
||||
}
|
||||
BPP_COMMAND_NAMES = {
|
||||
0: "initialiseSecureChannel", 1: "configureISDP", 2: "storeMetadata",
|
||||
3: "storeMetadata2", 4: "replaceSessionKeys", 5: "loadProfileElements",
|
||||
}
|
||||
BPP_ERROR_REASONS = {
|
||||
1: "incorrectInputValues", 2: "invalidSignature", 3: "invalidTransactionId",
|
||||
4: "unsupportedCrtValues", 5: "unsupportedRemoteOperationType",
|
||||
6: "unsupportedProfileClass", 7: "scp03tStructureError", 8: "scp03tSecurityError",
|
||||
9: "iccidAlreadyExistsOnEuicc", 10: "insufficientMemoryForProfile",
|
||||
11: "installInterrupted", 12: "peProcessingError", 13: "dataMismatch",
|
||||
14: "invalidNAA",
|
||||
}
|
||||
BPP_ERROR_MESSAGES = {
|
||||
9: "This eSIM profile is already installed on this device.",
|
||||
10: "Not enough memory on the eUICC to install this profile.",
|
||||
12: "Profile installation failed. The QR code may have already been used.",
|
||||
}
|
||||
|
||||
# SGP.22 §5.2.6 — SM-DP+ reason/subject codes mapped to user-friendly messages
|
||||
ES9P_ERROR_MESSAGES: dict[tuple[str, str], str] = {
|
||||
('3.8', '8.2.6'): "This eSIM profile is already installed on another device. Please use a new QR code.",
|
||||
('3.8', '8.2.1'): "This eSIM profile has expired. Please request a new QR code.",
|
||||
('3.8', '8.1'): "The SM-DP+ server refused this request.",
|
||||
('3.1', '8.2.6'): "This eSIM profile has been revoked by the carrier.",
|
||||
('3.9', '8.2.6'): "This eSIM profile download has already been completed.",
|
||||
('2.1', '8.8'): "The device is not compatible with this eSIM profile.",
|
||||
('1.2', '8.1'): "The SM-DP+ server is temporarily unavailable. Try again later.",
|
||||
}
|
||||
|
||||
NOTIFICATION_OPERATIONS = {0x80: "install", 0x40: "enable", 0x20: "disable", 0x10: "delete"}
|
||||
|
||||
STATE_LABELS = {0: "disabled", 1: "enabled", 255: "unknown"}
|
||||
ICON_LABELS = {0: "jpeg", 1: "png", 255: "unknown"}
|
||||
@@ -345,6 +402,15 @@ def es10x_command(client: AtClient, data: bytes) -> bytes:
|
||||
|
||||
# --- Profile operations ---
|
||||
|
||||
NOTIFICATION: FieldMap = {
|
||||
TAG_STATUS: ("seqNumber", lambda v: int.from_bytes(v, "big")),
|
||||
0x81: ("profileManagementOperation",
|
||||
lambda v: NOTIFICATION_OPERATIONS.get(next((m for m in NOTIFICATION_OPERATIONS if len(v) >= 2 and v[1] & m), 0), "unknown")),
|
||||
0x0C: ("notificationAddress", lambda v: v.decode("utf-8", errors="ignore")),
|
||||
TAG_ICCID: ("iccid", tbcd_to_string),
|
||||
}
|
||||
|
||||
|
||||
def decode_profiles(blob: bytes) -> list[dict]:
|
||||
root = require_tag(blob, TAG_PROFILE_INFO_LIST, "ProfileInfoList")
|
||||
list_ok = find_tag(root, TAG_OK)
|
||||
@@ -370,6 +436,278 @@ def set_profile_nickname(client: AtClient, iccid: str, nickname: str) -> None:
|
||||
raise RuntimeError(f"SetNickname failed with status 0x{code:02X}")
|
||||
|
||||
|
||||
# --- ES9P HTTP ---
|
||||
|
||||
def es9p_request(smdp_address: str, endpoint: str, payload: dict, error_prefix: str = "Request", session: requests.Session | None = None) -> dict:
|
||||
url = f"https://{smdp_address}/gsma/rsp2/es9plus/{endpoint}"
|
||||
headers = {"User-Agent": "gsma-rsp-lpad", "X-Admin-Protocol": "gsma/rsp/v2.3.0", "Content-Type": "application/json"}
|
||||
http = session or requests
|
||||
resp = http.post(url, json=payload, headers=headers, timeout=HTTP_TIMEOUT, verify=GSMA_CI_BUNDLE)
|
||||
resp.raise_for_status()
|
||||
if not resp.content:
|
||||
return {}
|
||||
data = resp.json()
|
||||
if "header" in data and "functionExecutionStatus" in data["header"]:
|
||||
status = data["header"]["functionExecutionStatus"]
|
||||
if status.get("status") == "Failed":
|
||||
sd = status.get("statusCodeData", {})
|
||||
reason = sd.get("reasonCode", "unknown")
|
||||
subject = sd.get("subjectCode", "unknown")
|
||||
msg = ES9P_ERROR_MESSAGES.get((reason, subject),
|
||||
f"{error_prefix} failed: {reason}/{subject} - {sd.get('message', 'unknown')}")
|
||||
raise RuntimeError(msg)
|
||||
return data
|
||||
|
||||
|
||||
# --- Notifications ---
|
||||
|
||||
def list_notifications(client: AtClient) -> list[dict]:
|
||||
response = es10x_command(client, encode_tlv(TAG_LIST_NOTIFICATION, b""))
|
||||
root = require_tag(response, TAG_LIST_NOTIFICATION, "ListNotificationResponse")
|
||||
metadata_list = find_tag(root, TAG_OK)
|
||||
if metadata_list is None:
|
||||
return []
|
||||
return [decode_struct(value, NOTIFICATION) for tag, value in iter_tlv(metadata_list) if tag == TAG_NOTIFICATION_METADATA]
|
||||
|
||||
|
||||
def process_notifications(client: AtClient) -> None:
|
||||
for notification in list_notifications(client):
|
||||
seq_number, smdp_address = notification["seqNumber"], notification["notificationAddress"]
|
||||
try:
|
||||
request = encode_tlv(TAG_RETRIEVE_NOTIFICATION, encode_tlv(TAG_OK, encode_tlv(TAG_STATUS, int_bytes(seq_number))))
|
||||
response = es10x_command(client, request)
|
||||
content = require_tag(require_tag(response, TAG_RETRIEVE_NOTIFICATION, "RetrieveNotificationsListResponse"),
|
||||
TAG_OK, "RetrieveNotificationsListResponse")
|
||||
pending_notif = next((v for t, v in iter_tlv(content) if t in (TAG_PROFILE_INSTALL_RESULT, 0x30)), None)
|
||||
if pending_notif is None:
|
||||
raise RuntimeError("Missing PendingNotification")
|
||||
|
||||
es9p_request(smdp_address, "handleNotification", {"pendingNotification": b64e(pending_notif)}, "HandleNotification")
|
||||
|
||||
response = es10x_command(client, encode_tlv(TAG_NOTIFICATION_SENT, encode_tlv(TAG_STATUS, int_bytes(seq_number))))
|
||||
root = require_tag(response, TAG_NOTIFICATION_SENT, "NotificationSentResponse")
|
||||
if int.from_bytes(require_tag(root, TAG_STATUS, "RemoveNotificationFromList status"), "big") != 0:
|
||||
raise RuntimeError("RemoveNotificationFromList failed")
|
||||
except Exception as e:
|
||||
print(f"notification {seq_number} failed: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
# --- Authentication & Download ---
|
||||
|
||||
def get_challenge_and_info(client: AtClient) -> tuple[bytes, bytes]:
|
||||
challenge_resp = es10x_command(client, encode_tlv(TAG_EUICC_CHALLENGE, b""))
|
||||
challenge = require_tag(require_tag(challenge_resp, TAG_EUICC_CHALLENGE, "GetEuiccDataResponse"),
|
||||
TAG_STATUS, "challenge in response")
|
||||
info_resp = es10x_command(client, encode_tlv(TAG_EUICC_INFO, b""))
|
||||
require_tag(info_resp, TAG_EUICC_INFO, "GetEuiccInfo1Response")
|
||||
return challenge, info_resp
|
||||
|
||||
|
||||
def authenticate_server(client: AtClient, b64_signed1: str, b64_sig1: str, b64_pk_id: str, b64_cert: str, matching_id: str) -> str:
|
||||
tac = bytes([0x35, 0x29, 0x06, 0x11])
|
||||
device_info = encode_tlv(TAG_STATUS, tac) + encode_tlv(0xA1, b"")
|
||||
ctx_inner = encode_tlv(TAG_STATUS, matching_id.encode("utf-8")) + encode_tlv(0xA1, device_info)
|
||||
content = b64d(b64_signed1) + b64d(b64_sig1) + b64d(b64_pk_id) + b64d(b64_cert) + encode_tlv(0xA0, ctx_inner)
|
||||
response = es10x_command(client, encode_tlv(TAG_AUTH_SERVER, content))
|
||||
root = require_tag(response, TAG_AUTH_SERVER, "AuthenticateServerResponse")
|
||||
error_tag = find_tag(root, 0xA1)
|
||||
if error_tag is not None:
|
||||
code = int.from_bytes(error_tag, "big") if error_tag else 0
|
||||
raise RuntimeError(f"AuthenticateServer rejected by eUICC: {AUTH_SERVER_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})")
|
||||
return b64e(response)
|
||||
|
||||
|
||||
def prepare_download(client: AtClient, b64_signed2: str, b64_sig2: str, b64_cert: str, cc: str | None = None) -> str:
|
||||
smdp_signed2 = b64d(b64_signed2)
|
||||
smdp_signature2 = b64d(b64_sig2)
|
||||
smdp_certificate = b64d(b64_cert)
|
||||
smdp_signed2_root = find_tag(smdp_signed2, 0x30)
|
||||
if smdp_signed2_root is None:
|
||||
raise RuntimeError("Invalid smdpSigned2")
|
||||
transaction_id = find_tag(smdp_signed2_root, TAG_STATUS)
|
||||
cc_required_flag = find_tag(smdp_signed2_root, 0x01)
|
||||
if transaction_id is None or cc_required_flag is None:
|
||||
raise RuntimeError("Invalid smdpSigned2")
|
||||
content = smdp_signed2 + smdp_signature2
|
||||
if int.from_bytes(cc_required_flag, "big") != 0:
|
||||
if not cc:
|
||||
raise RuntimeError("Confirmation code required but not provided")
|
||||
content += encode_tlv(0x04, hashlib.sha256(hashlib.sha256(cc.encode("utf-8")).digest() + transaction_id).digest())
|
||||
content += smdp_certificate
|
||||
response = es10x_command(client, encode_tlv(TAG_PREPARE_DOWNLOAD, content))
|
||||
require_tag(response, TAG_PREPARE_DOWNLOAD, "PrepareDownloadResponse")
|
||||
return b64e(response)
|
||||
|
||||
|
||||
def _parse_tlv_header_len(data: bytes) -> int:
|
||||
tag_len = 2 if data[0] & 0x1F == 0x1F else 1
|
||||
length_byte = data[tag_len]
|
||||
return tag_len + (1 + (length_byte & 0x7F) if length_byte & 0x80 else 1)
|
||||
|
||||
|
||||
def _split_bpp(bpp: bytes) -> list[bytes]:
|
||||
"""Split a BoundProfilePackage into APDU chunks per SGP.22 §5.7.6."""
|
||||
root_value = None
|
||||
for tag, value, start, end in iter_tlv(bpp, with_positions=True):
|
||||
if tag == TAG_BPP:
|
||||
root_value = value
|
||||
val_start = start + _parse_tlv_header_len(bpp[start:end])
|
||||
break
|
||||
if root_value is None:
|
||||
raise RuntimeError("Invalid BoundProfilePackage")
|
||||
|
||||
chunks: list[bytes] = []
|
||||
for tag, value, start, end in iter_tlv(root_value, with_positions=True):
|
||||
if tag == TAG_BPP_COMMAND:
|
||||
chunks.append(bpp[0 : val_start + end])
|
||||
elif tag in (0xA0, 0xA2):
|
||||
chunks.append(bpp[val_start + start : val_start + end])
|
||||
elif tag in (0xA1, 0xA3):
|
||||
hdr_len = _parse_tlv_header_len(root_value[start:end])
|
||||
chunks.append(bpp[val_start + start : val_start + start + hdr_len])
|
||||
for _, _, cs, ce in iter_tlv(value, with_positions=True):
|
||||
chunks.append(value[cs:ce])
|
||||
return chunks
|
||||
|
||||
|
||||
def _parse_install_result(response: bytes) -> dict[str, Any] | None:
|
||||
"""Parse a ProfileInstallResult from an APDU response, or None if not present."""
|
||||
root = find_tag(response, TAG_PROFILE_INSTALL_RESULT)
|
||||
if not root:
|
||||
return None
|
||||
result_data = find_tag(root, TAG_INSTALL_RESULT_DATA)
|
||||
if not result_data:
|
||||
return None
|
||||
result: dict[str, Any] = {"seqNumber": 0, "success": False, "bppCommandId": None, "errorReason": None}
|
||||
notif_meta = find_tag(result_data, TAG_NOTIFICATION_METADATA)
|
||||
if notif_meta:
|
||||
seq_num = find_tag(notif_meta, TAG_STATUS)
|
||||
if seq_num:
|
||||
result["seqNumber"] = int.from_bytes(seq_num, "big")
|
||||
final_result = find_tag(result_data, 0xA2)
|
||||
if final_result:
|
||||
for tag, value in iter_tlv(final_result):
|
||||
if tag == 0xA0:
|
||||
result["success"] = True
|
||||
elif tag == 0xA1:
|
||||
bpp_cmd = find_tag(value, TAG_STATUS)
|
||||
if bpp_cmd:
|
||||
result["bppCommandId"] = int.from_bytes(bpp_cmd, "big")
|
||||
err = find_tag(value, 0x81)
|
||||
if err:
|
||||
result["errorReason"] = int.from_bytes(err, "big")
|
||||
return result
|
||||
|
||||
|
||||
def load_bpp(client: AtClient, b64_bpp: str) -> dict:
|
||||
bpp = b64d(b64_bpp)
|
||||
result = None
|
||||
for chunk in _split_bpp(bpp):
|
||||
response = es10x_command(client, chunk)
|
||||
if response:
|
||||
result = _parse_install_result(response) or result
|
||||
|
||||
if result is None:
|
||||
raise RuntimeError("Profile installation failed: no result from eUICC")
|
||||
if not result["success"] and result["errorReason"] is not None:
|
||||
msg = BPP_ERROR_MESSAGES.get(result["errorReason"])
|
||||
if not msg:
|
||||
cmd_name = BPP_COMMAND_NAMES.get(result["bppCommandId"], f"unknown({result['bppCommandId']})")
|
||||
err_name = BPP_ERROR_REASONS.get(result["errorReason"], f"unknown({result['errorReason']})")
|
||||
msg = f"Profile installation failed at {cmd_name}: {err_name}"
|
||||
raise RuntimeError(msg)
|
||||
if not result["success"]:
|
||||
raise RuntimeError("Profile installation failed: no result from eUICC")
|
||||
return result
|
||||
|
||||
|
||||
def parse_metadata(b64_metadata: str) -> dict:
|
||||
root = find_tag(b64d(b64_metadata), TAG_PROFILE_METADATA)
|
||||
if root is None:
|
||||
raise RuntimeError("Invalid profileMetadata")
|
||||
return decode_struct(root, PROFILE)
|
||||
|
||||
|
||||
def cancel_session(client: AtClient, transaction_id: bytes, reason: int = 127) -> str:
|
||||
content = encode_tlv(0x80, transaction_id) + encode_tlv(0x81, bytes([reason]))
|
||||
response = es10x_command(client, encode_tlv(TAG_CANCEL_SESSION, content))
|
||||
return b64e(response)
|
||||
|
||||
|
||||
def parse_lpa_activation_code(activation_code: str) -> tuple[str, str]:
|
||||
"""Parse 'LPA:1$smdp.example.com$MATCHING-ID' into (smdp_address, matching_id)."""
|
||||
if not activation_code.startswith("LPA:"):
|
||||
raise ValueError("Invalid activation code format")
|
||||
parts = activation_code[4:].split("$")
|
||||
if len(parts) != 3:
|
||||
raise ValueError("Invalid activation code format")
|
||||
return parts[1], parts[2]
|
||||
|
||||
|
||||
def _b64_field(data: dict, key: str) -> str:
|
||||
return base64_trim(data[key])
|
||||
|
||||
|
||||
def _cancel_session_safe(client: AtClient, smdp: str, tx_id: str, session: requests.Session) -> None:
|
||||
b64_cancel = ""
|
||||
try:
|
||||
b64_cancel = cancel_session(client, b64d(tx_id))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
es9p_request(smdp, "cancelSession", {"transactionId": tx_id, "cancelSessionResponse": b64_cancel}, "CancelSession", session=session)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def download_profile(client: AtClient, activation_code: str) -> str:
|
||||
"""Download and install an eSIM profile. Returns the ICCID of the installed profile."""
|
||||
if not system_time_valid():
|
||||
raise RuntimeError("System time is not set; TLS certificate validation requires a valid clock")
|
||||
smdp, matching_id = parse_lpa_activation_code(activation_code)
|
||||
challenge, euicc_info = get_challenge_and_info(client)
|
||||
session = requests.Session()
|
||||
tx_id = None
|
||||
|
||||
try:
|
||||
# step 1: initiate authentication
|
||||
auth = es9p_request(smdp, "initiateAuthentication", {
|
||||
"smdpAddress": smdp, "euiccChallenge": b64e(challenge),
|
||||
"euiccInfo1": b64e(euicc_info), "matchingId": matching_id,
|
||||
}, "Authentication", session=session)
|
||||
tx_id = _b64_field(auth, "transactionId")
|
||||
|
||||
# step 2: authenticate server
|
||||
b64_auth = authenticate_server(client,
|
||||
_b64_field(auth, "serverSigned1"), _b64_field(auth, "serverSignature1"),
|
||||
_b64_field(auth, "euiccCiPKIdToBeUsed"), _b64_field(auth, "serverCertificate"),
|
||||
matching_id)
|
||||
|
||||
# step 3: authenticate client + get metadata
|
||||
cli = es9p_request(smdp, "authenticateClient", {
|
||||
"transactionId": tx_id, "authenticateServerResponse": b64_auth,
|
||||
}, "Authentication", session=session)
|
||||
iccid = parse_metadata(_b64_field(cli, "profileMetadata"))["iccid"]
|
||||
|
||||
# step 4: prepare download
|
||||
b64_prep = prepare_download(client,
|
||||
_b64_field(cli, "smdpSigned2"), _b64_field(cli, "smdpSignature2"),
|
||||
_b64_field(cli, "smdpCertificate"))
|
||||
|
||||
# step 5: get and install bound profile package
|
||||
bpp = es9p_request(smdp, "getBoundProfilePackage", {
|
||||
"transactionId": tx_id, "prepareDownloadResponse": b64_prep,
|
||||
}, "GetBoundProfilePackage", session=session)
|
||||
load_bpp(client, _b64_field(bpp, "boundProfilePackage"))
|
||||
return iccid
|
||||
except Exception:
|
||||
if tx_id:
|
||||
_cancel_session_safe(client, smdp, tx_id, session)
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
class TiciLPA(LPABase):
|
||||
def __init__(self):
|
||||
if hasattr(self, '_client'):
|
||||
@@ -409,6 +747,12 @@ class TiciLPA(LPABase):
|
||||
def get_active_profile(self) -> Profile | None:
|
||||
return None
|
||||
|
||||
def process_notifications(self) -> None:
|
||||
if not system_time_valid():
|
||||
raise RuntimeError("System time is not set; TLS certificate validation requires a valid clock")
|
||||
with self._acquire_channel():
|
||||
process_notifications(self._client)
|
||||
|
||||
def delete_profile(self, iccid: str) -> None:
|
||||
if self.is_comma_profile(iccid):
|
||||
raise LPAError("refusing to delete a comma profile")
|
||||
@@ -420,7 +764,10 @@ class TiciLPA(LPABase):
|
||||
raise LPAError(f"DeleteProfile failed: {PROFILE_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})")
|
||||
|
||||
def download_profile(self, qr: str, nickname: str | None = None) -> None:
|
||||
return None
|
||||
with self._acquire_channel():
|
||||
iccid = download_profile(self._client, qr)
|
||||
if nickname and iccid:
|
||||
set_profile_nickname(self._client, iccid, nickname)
|
||||
|
||||
def nickname_profile(self, iccid: str, nickname: str) -> None:
|
||||
with self._acquire_channel():
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import pytest
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.updated.updated import Updater
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("device_type", "branch", "expected"), [
|
||||
("tizi", "release3", "release-tizi"),
|
||||
("tizi", "release3-staging", "release-tizi-staging"),
|
||||
("mici", "release3", "release-mici"),
|
||||
("mici", "release3-staging", "release-mici-staging"),
|
||||
])
|
||||
def test_target_branch_migration_from_current_branch(mocker, device_type, branch, expected):
|
||||
params = Params()
|
||||
params.remove("UpdaterTargetBranch")
|
||||
|
||||
mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
|
||||
mocker.patch.object(Updater, "get_branch", return_value=branch)
|
||||
|
||||
assert Updater().target_branch == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("device_type", "branch", "expected"), [
|
||||
("tizi", "release3", "release-tizi"),
|
||||
("tizi", "release3-staging", "release-tizi-staging"),
|
||||
("mici", "release3", "release-mici"),
|
||||
("mici", "release3-staging", "release-mici-staging"),
|
||||
])
|
||||
def test_target_branch_migration_from_param(mocker, device_type, branch, expected):
|
||||
params = Params()
|
||||
params.put("UpdaterTargetBranch", branch)
|
||||
|
||||
mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
|
||||
|
||||
try:
|
||||
assert Updater().target_branch == expected
|
||||
finally:
|
||||
params.remove("UpdaterTargetBranch")
|
||||
@@ -24,6 +24,10 @@ SP_BRANCH_MIGRATIONS = {
|
||||
("tizi", "staging-c3-new"): "staging",
|
||||
("tizi", "dev-c3-new"): "dev",
|
||||
("tizi", "master-dev-c3-new"): "master-dev",
|
||||
("tizi", "release3"): "release-tizi",
|
||||
("tizi", "release3-staging"): "release-tizi-staging",
|
||||
("mici", "release3"): "release-mici",
|
||||
("mici", "release3-staging"): "release-mici-staging",
|
||||
}
|
||||
|
||||
BUILD_METADATA_FILENAME = "build.json"
|
||||
|
||||
Reference in New Issue
Block a user