commaai/openpilot · error · RuntimeError

Invalid smdpSigned2

Error message

Invalid smdpSigned2

What it means

prepare_download() decodes the SM-DP+ server's base64 smdpSigned2 blob and parses it as DER/BER-TLV looking for a root tag 0x30 (ASN.1 SEQUENCE). If no 0x30 element is found, the server response is structurally invalid from the client's perspective and RuntimeError('Invalid smdpSigned2') is raised.

Solutions

  1. Verify the argument mapping: prepare_download(client, b64_signed2, b64_sig2, b64_cert, cc) must receive exactly the server's smdpSigned2, smdpSignature2, smdpCertificate fields
  2. Decode and inspect the blob offline (openssl asn1parse / base64 -d) to confirm it is a DER SEQUENCE
  3. If the server payload genuinely lacks structure, escalate to the carrier — their ES9+ response is malformed
Defensive patterns

Strategy: validation

Validate before calling

from openpilot.common.esim.lpa import find_tag

raw = base64.b64decode(b64_signed2)
if find_tag(raw, 0x30) is None:
  raise SystemExit('smdpSigned2 is not a valid DER SEQUENCE — wrong field or server bug')

Type guard

def is_valid_smdp_signed2(b64: str) -> bool:
  try:
    return find_tag(base64.b64decode(b64), 0x30) is not None
  except Exception:
    return False

Prevention

When it happens

Trigger: handleNotification/authenticateClient exchange returned a smdpSigned2 that is not valid DER (wrong field passed, base64 of a different value, truncated payload), or a server response format that deviates from SGP.22 expectations.

Common situations: Field mixups when wiring the ES9+ JSON into prepare_download (passing smdpSignature2 or the cert in the signed2 slot); carrier server bugs or SGP.22 version differences; base64 decoding of a corrupted string.

Related errors


AI-assisted analysis of commaai/openpilot@516ec1e682 (2026-08-15). Data as JSON: /api/errors/0254790177f5d5fd. Report an issue: GitHub.

Appendix: source

Thrown at openpilot/common/esim/lpa.py:499

  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]

View on GitHub (pinned to 516ec1e682)