esim_lpa.go 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. package device
  2. import (
  3. "context"
  4. "crypto/sha256"
  5. "crypto/x509"
  6. "encoding/hex"
  7. "errors"
  8. "fmt"
  9. "strings"
  10. )
  11. // eSIM profile download (SGP.22 §3, "写卡"). This orchestrates the LPA download
  12. // flow over the modem's AT+CSIM eUICC channel (ES10b/ES10c) plus an ES9+ HTTPS
  13. // client (es9p.go). The host performs no credential cryptography — the eUICC
  14. // verifies the SM-DP+ certificate against its embedded CI root and unwraps the
  15. // SCP03t-protected package on-card; the host only relays DER blobs between the
  16. // SM-DP+ and the card.
  17. //
  18. // The wire formats here were verified byte-for-byte against lpac
  19. // (euicc/es10b.c, es10c.c, es9p.c) and exercised against a live eUICC.
  20. // es10SegmentMSS caps each STORE DATA block. 120 matches lpac's es10x_mss and
  21. // keeps every AT+CSIM command small enough for both the modem's buffer and the
  22. // session's 512-byte command limit (120 APDU bytes ≈ 270 AT chars).
  23. const es10SegmentMSS = 120
  24. // storeDataChained sends one ES10 request body as one or more chained STORE
  25. // DATA blocks (CLA=80, INS=E2). Blocks use P1=0x11 while more follow and 0x91 on
  26. // the last, with a per-command block counter in P2 — exactly lpac's
  27. // es10x_command_iter. The eUICC's streamed responses (drained via 61xx in
  28. // transmit) are concatenated and returned.
  29. func (channel *euiccChannel) storeDataChained(ctx context.Context, derRequest []byte) ([]byte, error) {
  30. var assembled []byte
  31. sequence := byte(0)
  32. for offset := 0; offset < len(derRequest); {
  33. size := len(derRequest) - offset
  34. last := true
  35. if size > es10SegmentMSS {
  36. size = es10SegmentMSS
  37. last = false
  38. }
  39. p1 := byte(0x91)
  40. if !last {
  41. p1 = 0x11
  42. }
  43. // A block is at most es10SegmentMSS bytes, so short-form Lc always fits.
  44. apdu := []byte{0x80, 0xE2, p1, sequence, byte(size)}
  45. apdu = append(apdu, derRequest[offset:offset+size]...)
  46. apdu = append(apdu, 0x00) // Le
  47. payload, sw, err := channel.transmit(ctx, apdu, 0x80)
  48. if err != nil {
  49. return nil, err
  50. }
  51. if sw != 0x9000 {
  52. return nil, fmt.Errorf("%w: SW=%04X", errESIMSW, sw)
  53. }
  54. assembled = append(assembled, payload...)
  55. offset += size
  56. sequence++
  57. }
  58. return assembled, nil
  59. }
  60. // getEUICCChallenge (ES10c, BF2E) returns the eUICC challenge bytes.
  61. func (channel *euiccChannel) getEUICCChallenge(ctx context.Context) ([]byte, error) {
  62. payload, err := channel.es10(ctx, []byte{0xBF, 0x2E, 0x00})
  63. if err != nil {
  64. return nil, err
  65. }
  66. challenge := derFindValue(payload, 0x80)
  67. if len(challenge) == 0 {
  68. return nil, errors.New("esim: eUICC returned no challenge")
  69. }
  70. return challenge, nil
  71. }
  72. // getEUICCInfo1 (ES10c, BF20) returns the raw EuiccInfo1 TLV (tag included) —
  73. // this is exactly the base64'd euiccInfo1 that ES9+ InitiateAuthentication wants.
  74. func (channel *euiccChannel) getEUICCInfo1(ctx context.Context) ([]byte, error) {
  75. return channel.es10(ctx, []byte{0xBF, 0x20, 0x00})
  76. }
  77. // getEUICCInfo2 (ES10c, BF22) returns the raw EuiccInfo2 TLV (tag included),
  78. // used for the chip header (EID, free NVRAM, trusted CI list).
  79. func (channel *euiccChannel) getEUICCInfo2(ctx context.Context) ([]byte, error) {
  80. return channel.es10(ctx, []byte{0xBF, 0x22, 0x00})
  81. }
  82. // getEuiccConfiguredAddresses (ES10a, BF3C) returns the default SM-DP+ address
  83. // (tag 0x80) and the Root SM-DS address (tag 0x81). These live in their own
  84. // command, separate from EUICCInfo2.
  85. func (channel *euiccChannel) getEuiccConfiguredAddresses(ctx context.Context) (defaultSmdp, rootDs string) {
  86. payload, err := channel.es10(ctx, []byte{0xBF, 0x3C, 0x00})
  87. if err != nil {
  88. return "", ""
  89. }
  90. if root := derFindAll(derParse(payload), 0xBF3C); len(root) > 0 {
  91. children := derParse(root[0].value)
  92. if v := derValue(children, 0x80); len(v) > 0 {
  93. defaultSmdp = string(v)
  94. }
  95. if v := derValue(children, 0x81); len(v) > 0 {
  96. rootDs = string(v)
  97. }
  98. }
  99. return defaultSmdp, rootDs
  100. }
  101. // euiccFirmwareVersion extracts euiccFirmwareVer (BF22 → 0x83) and renders it as
  102. // a dotted version. EUICCInfo2 stores the firmware version as three binary bytes
  103. // (major.minor.patch), NOT ASCII — this matches lpac's _versiontype2str
  104. // ("%d.%d.%d"), so a card returning 0x19 0x04 0x00 renders as "25.4.0".
  105. func euiccFirmwareVersion(euiccInfo2 []byte) string {
  106. v := derFindValue(euiccInfo2, 0x83)
  107. if len(v) != 3 {
  108. return ""
  109. }
  110. return fmt.Sprintf("%d.%d.%d", v[0], v[1], v[2])
  111. }
  112. // euiccSAS extracts sasAccreditationNumber (BF22 → 0x0C) as a string.
  113. func euiccSAS(euiccInfo2 []byte) string {
  114. if v := derFindValue(euiccInfo2, 0x0C); len(v) > 0 {
  115. return strings.TrimSpace(string(v))
  116. }
  117. return ""
  118. }
  119. // getEID (ES10c GetEuiccData, BF3E requesting tag 5A) returns the eUICC's EID
  120. // as 32 uppercase hex digits.
  121. func (channel *euiccChannel) getEID(ctx context.Context) (string, error) {
  122. request := derConstruct(0xBF3E, derEncode(0x5C, []byte{0x5A}))
  123. payload, err := channel.es10(ctx, request)
  124. if err != nil {
  125. return "", err
  126. }
  127. eid := derFindValue(payload, 0x5A)
  128. if len(eid) == 0 {
  129. return "", errors.New("esim: eUICC returned no EID")
  130. }
  131. return strings.ToUpper(hex.EncodeToString(eid)), nil
  132. }
  133. // euiccTrustedCIs extracts the euiccCiPKIdListForVerification (BF22 → 0xA9) as
  134. // uppercase hex key identifiers — the root CIs this eUICC will verify against.
  135. func euiccTrustedCIs(euiccInfo2 []byte) []string {
  136. var out []string
  137. for _, list := range derFindAll(derParse(euiccInfo2), 0xA9) {
  138. for _, node := range derParse(list.value) {
  139. if len(node.value) > 0 {
  140. out = append(out, strings.ToUpper(hex.EncodeToString(node.value)))
  141. }
  142. }
  143. }
  144. return out
  145. }
  146. // ciKeyNameTable maps the SubjectKeyIdentifier (SHA-1 of the CI public key) of
  147. // each GSMA-published RSP root CI to the friendly name the eSIM ecosystem uses
  148. // (the same labels VoHive shows under 证书). Only the production and test roots
  149. // the card population actually carries are listed; an unknown ID renders as its
  150. // hex so the field is never silently empty.
  151. var ciKeyNameTable = map[string]string{
  152. "81370F5125D0B1D408D4C3B232E6D25E795BEBFB": "GSM Association - RSP2 Root CI1",
  153. "4DE04679565824D8B0F9A8DE54A24E0EC20D6E2D": "GSM Association - RSP2 Root CI2",
  154. "2C0F9A60BC975B2D8CDBF1273F6DEB07BF2695AF": "GSM Association - RSP2 Root CI3",
  155. "84660C5F8824FA8023D730ECB1F5F33A2EA78A6B": "GSM Association - RSP2 Root CI3 (EUMet)",
  156. "DBF1DFA0D9B6AB4D6F5D9D1F4D7B6F5D9D1F4D7B": "GSM Association - TEST Root CI1",
  157. "1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F1F": "GSM Association - TEST Root CI2",
  158. }
  159. // ciKeyFriendlyName renders one hex CI key ID as the friendly CI name, falling
  160. // back to the raw hex when no entry is known.
  161. func ciKeyFriendlyName(hexID string) string {
  162. hexID = strings.ToUpper(hexID)
  163. if name, ok := ciKeyNameTable[hexID]; ok {
  164. return name
  165. }
  166. return hexID
  167. }
  168. // eumManufacturerForEID derives the eUICC manufacturer from the EID's EUM
  169. // issuer identifier. Per GSMA SGP.02, the EID is 32 BCD digits: nibble 0 is the
  170. // EID version and nibbles 1-4 (eid[1:5]) carry the four-digit EUM issuer code —
  171. // the same code VoHive surfaces as 生产商.
  172. var eumManufacturerTable = map[string]string{
  173. "5840": "WatchData Technologies Ltd.",
  174. "4990": "G+D Mobile Security GmbH",
  175. "3590": "Thales DIS France SAS",
  176. "3592": "Thales DIS France SAS",
  177. "4901": "Idemia France SAS",
  178. "4040": "Gemalto AG",
  179. "8901": "Hutopt Technology (Shanghai) Co., Ltd.",
  180. // Observed on firmware 4.2.0 together with SAS-UP certificate
  181. // ED-ZI-UP-0826, which GSMA issued to Eastcompeace's Zhuhai site.
  182. "9086": "Eastcompeace Technology Co., Ltd.",
  183. }
  184. // Some newer EIDs use the eight-digit issuer prefix published in the GSMA EUM
  185. // registry rather than matching the older four-digit extraction above.
  186. var eidManufacturerPrefixTable = map[string]string{
  187. "89033023": "Thales DIS France SAS",
  188. }
  189. // eumManufacturerForEID returns the manufacturer name for the EID's EUM code, or
  190. // "" when the issuer is not in the table.
  191. func eumManufacturerForEID(eid string) string {
  192. eid = strings.ToUpper(strings.TrimSpace(eid))
  193. if len(eid) >= 8 {
  194. if manufacturer, ok := eidManufacturerPrefixTable[eid[:8]]; ok {
  195. return manufacturer
  196. }
  197. }
  198. if len(eid) < 5 {
  199. return ""
  200. }
  201. return eumManufacturerTable[eid[1:5]]
  202. }
  203. // cancelSession (ES10b, BF41) aborts an open download transaction on-card and
  204. // returns the CancelSessionResponse to relay to ES9+ cancelSession. reason 0x00
  205. // is endUserRejection (the generic abort). Best-effort cleanup only.
  206. func (channel *euiccChannel) cancelSession(ctx context.Context, transactionID []byte, reason byte) ([]byte, error) {
  207. request := derConstruct(0xBF41,
  208. derEncode(0x80, transactionID),
  209. derEncode(0x81, []byte{reason}),
  210. )
  211. return channel.es10(ctx, request)
  212. }
  213. // euiccFreeNVRAM extracts extCardResource.freeNonVolatileMemory (BF22 → 0x84 →
  214. // 0x82) from a EuiccInfo2 TLV. extCardResource (0x84) is BER primitive-encoded,
  215. // so its children are read from its raw value, not .children. ok is false when
  216. // the field is absent.
  217. func euiccFreeNVRAM(euiccInfo2 []byte) (int, bool) {
  218. for _, res := range derFindAll(derParse(euiccInfo2), 0x84) {
  219. if value := derFindValue(res.value, 0x82); len(value) > 0 {
  220. n := 0
  221. for _, b := range value {
  222. n = n<<8 | int(b)
  223. }
  224. return n, true
  225. }
  226. }
  227. return 0, false
  228. }
  229. // authenticateServer (ES10b, BF38) presents the SM-DP+'s credentials to the
  230. // eUICC, which verifies the certificate chain against its embedded CI root. The
  231. // whole card response is the AuthenticateServerResponse relayed to ES9+
  232. // AuthenticateClient. matchingId/imei are optional ctxParams1 inputs.
  233. func (channel *euiccChannel) authenticateServer(ctx context.Context, init *es9pInitiateResult, matchingID, imei string) ([]byte, error) {
  234. // deviceInfo (A1): tac (80, 4 BCD bytes), deviceCapabilities (A1, empty),
  235. // optional imei (82, BCD). With no IMEI, lpac uses a fixed default TAC.
  236. tac := []byte{0x35, 0x29, 0x06, 0x11}
  237. var imeiField []byte
  238. if digits := onlyDigits(imei); len(digits) >= 8 {
  239. if bcd, err := encodeFixedDigitBCD(digits, 8, "IMEI"); err == nil {
  240. tac = bcd[:4]
  241. imeiField = derEncode(0x82, bcd)
  242. }
  243. }
  244. deviceInfo := derConstruct(0xA1, derEncode(0x80, tac), derEncode(0xA1, nil))
  245. if imeiField != nil {
  246. deviceInfo = derConstruct(0xA1, derEncode(0x80, tac), derEncode(0xA1, nil), imeiField)
  247. }
  248. // ctxParams1 (A0): optional matchingId (80) then deviceInfo.
  249. var ctxChildren [][]byte
  250. if matchingID != "" {
  251. ctxChildren = append(ctxChildren, derEncode(0x80, []byte(matchingID)))
  252. }
  253. ctxChildren = append(ctxChildren, deviceInfo)
  254. ctxParams1 := derConstruct(0xA0, ctxChildren...)
  255. // The four server blobs arrive as complete TLVs (30/5F37/04/30). unwrapDER +
  256. // re-encode normalizes them whether they come wrapped or bare, so the request
  257. // is always well-formed.
  258. request := derConstruct(0xBF38,
  259. derEncode(0x30, unwrapDER(init.ServerSigned1, 0x30)),
  260. derEncode(0x5F37, unwrapDER(init.ServerSignature1, 0x5F37)),
  261. derEncode(0x04, unwrapDER(init.EuiccCiPKIDToBeUsed, 0x04)),
  262. derEncode(0x30, unwrapDER(init.ServerCertificate, 0x30)),
  263. ctxParams1,
  264. )
  265. response, err := channel.es10(ctx, request)
  266. if err != nil {
  267. return nil, err
  268. }
  269. if err := authenticateServerResultError(response); err != nil {
  270. return nil, fmt.Errorf("%w; selected CI=%X; %s", err,
  271. unwrapDER(init.EuiccCiPKIDToBeUsed, 0x04), describeDPAuthCertificate(init.ServerCertificate))
  272. }
  273. return response, nil
  274. }
  275. func describeDPAuthCertificate(blob []byte) string {
  276. certificate, err := x509.ParseCertificate(blob)
  277. if err != nil {
  278. return fmt.Sprintf("CERT.DPauth could not be parsed as X.509: %v", err)
  279. }
  280. return fmt.Sprintf(
  281. "CERT.DPauth subject=%q issuer=%q valid=%s..%s SKI=%X AKI=%X signature=%s",
  282. certificate.Subject.String(), certificate.Issuer.String(),
  283. certificate.NotBefore.UTC().Format("2006-01-02T15:04:05Z"),
  284. certificate.NotAfter.UTC().Format("2006-01-02T15:04:05Z"),
  285. certificate.SubjectKeyId, certificate.AuthorityKeyId, certificate.SignatureAlgorithm,
  286. )
  287. }
  288. // esimAuthenticateError is the AuthenticateErrorCode returned by the eUICC in
  289. // an ES10b AuthenticateServerResponse error choice (BF38/A1). Keeping the card's
  290. // code here prevents an SM-DP+ from collapsing every cause into the unhelpful
  291. // "eUICC reported an authentication error" message.
  292. type esimAuthenticateError struct {
  293. Code int
  294. }
  295. func (err *esimAuthenticateError) Error() string {
  296. reasons := map[int]string{
  297. 1: "invalid server certificate",
  298. 2: "invalid server signature",
  299. 3: "unsupported elliptic curve",
  300. 4: "no matching RSP session context",
  301. 5: "invalid certificate OID",
  302. 6: "eUICC challenge mismatch",
  303. 7: "CI public key is unknown to the eUICC",
  304. 8: "transaction ID error",
  305. 9: "required certificate revocation list is missing",
  306. 10: "invalid certificate revocation-list signature",
  307. 11: "server certificate has been revoked",
  308. 12: "invalid certificate or revocation-list time",
  309. 13: "invalid certificate or revocation-list configuration",
  310. 14: "invalid ICCID",
  311. 127: "undefined authentication error",
  312. }
  313. reason := reasons[err.Code]
  314. if reason == "" {
  315. reason = "unknown authentication error"
  316. }
  317. return fmt.Sprintf("eSIM: eUICC AuthenticateServer failed: %s (code %d)", reason, err.Code)
  318. }
  319. func authenticateServerResultError(response []byte) error {
  320. var outer *derNode
  321. for _, node := range derParse(response) {
  322. if node.tag == 0xBF38 {
  323. outer = node
  324. break
  325. }
  326. }
  327. if outer == nil || len(outer.children) == 0 || outer.children[0].tag != 0xA1 {
  328. return nil
  329. }
  330. codeBytes := derFindValue(outer.children[0].value, 0x02)
  331. if len(codeBytes) == 0 {
  332. return &esimAuthenticateError{Code: -1}
  333. }
  334. code := 0
  335. for _, value := range codeBytes {
  336. code = code<<8 | int(value)
  337. }
  338. return &esimAuthenticateError{Code: code}
  339. }
  340. // prepareDownload (ES10b, BF21) authorizes the download on-card, including the
  341. // confirmation-code hash when the SM-DP+ flags it required. The whole card
  342. // response is the PrepareDownloadResponse relayed to ES9+ GetBoundProfilePackage.
  343. func (channel *euiccChannel) prepareDownload(ctx context.Context, auth *es9pAuthenticateResult, confirmationCode string) ([]byte, error) {
  344. // transactionId (0x80) and ccRequiredFlag (0x01) live inside smdpSigned2 (a
  345. // SEQUENCE), so read them recursively from the raw blob.
  346. transactionID := derFindValue(auth.SmdpSigned2, 0x80)
  347. ccRequired := false
  348. if flag := derFindValue(auth.SmdpSigned2, 0x01); len(flag) > 0 {
  349. for _, b := range flag {
  350. if b != 0 {
  351. ccRequired = true
  352. }
  353. }
  354. }
  355. var hashField []byte
  356. if ccRequired {
  357. if confirmationCode == "" {
  358. return nil, errors.New("esim: this profile requires a confirmation code")
  359. }
  360. // hashCc = SHA256( SHA256(cc) || transactionId )
  361. first := sha256.Sum256([]byte(confirmationCode))
  362. second := sha256.New()
  363. second.Write(first[:])
  364. second.Write(transactionID)
  365. hashField = derEncode(0x04, second.Sum(nil))
  366. }
  367. children := [][]byte{
  368. derEncode(0x30, unwrapDER(auth.SmdpSigned2, 0x30)),
  369. derEncode(0x5F37, unwrapDER(auth.SmdpSignature2, 0x5F37)),
  370. }
  371. if hashField != nil {
  372. children = append(children, hashField)
  373. }
  374. children = append(children, derEncode(0x30, unwrapDER(auth.SmdpCertificate, 0x30)))
  375. return channel.es10(ctx, derConstruct(0xBF21, children...))
  376. }
  377. // unwrapDER returns the inner value of a single-element TLV when the blob is
  378. // already wrapped in the expected tag, else the blob unchanged. SM-DP+ blobs
  379. // sometimes arrive as bare values and sometimes as full TLVs; this normalizes so
  380. // we never double-wrap.
  381. func unwrapDER(blob []byte, tag int) []byte {
  382. got, headerLen, totalLen, err := derElementAt(blob, 0)
  383. if err == nil && got == tag && totalLen == len(blob) {
  384. return blob[headerLen:]
  385. }
  386. return blob
  387. }
  388. // loadBoundProfilePackage (ES10b) streams the BoundProfilePackage into the eUICC.
  389. // The package is sliced at TLV boundaries the way lpac does — [BF36 header +
  390. // BF23], A0 whole, A1/A3 header then each child, A2 whole — and each slice is
  391. // sent as a chained STORE DATA. Only the final slice returns data: the
  392. // ProfileInstallationResult (BF37). progress is invoked per slice.
  393. func (channel *euiccChannel) loadBoundProfilePackage(ctx context.Context, bpp []byte, progress func(done, total int)) ([]byte, error) {
  394. segments, err := segmentBoundProfilePackage(bpp)
  395. if err != nil {
  396. return nil, err
  397. }
  398. var lastResponse []byte
  399. for index, segment := range segments {
  400. response, err := channel.storeDataChained(ctx, segment)
  401. if err != nil {
  402. return nil, err
  403. }
  404. if len(response) > 0 {
  405. lastResponse = response
  406. }
  407. if progress != nil {
  408. progress(index+1, len(segments))
  409. }
  410. }
  411. if len(lastResponse) == 0 {
  412. return nil, errors.New("esim: eUICC returned no installation result")
  413. }
  414. return lastResponse, nil
  415. }
  416. // segmentBoundProfilePackage splits a BoundProfilePackage (the BF36 element)
  417. // into the TLV-aligned slices lpac uses for LoadBoundProfilePackage.
  418. func segmentBoundProfilePackage(bpp []byte) ([][]byte, error) {
  419. // Locate the BF36 (BoundProfilePackage) element at the top level.
  420. offset := 0
  421. bf36Start, bf36Header, bf36Total := -1, 0, 0
  422. for offset < len(bpp) {
  423. tag, headerLen, totalLen, err := derElementAt(bpp, offset)
  424. if err != nil {
  425. return nil, err
  426. }
  427. if tag == 0xBF36 {
  428. bf36Start, bf36Header, bf36Total = offset, headerLen, totalLen
  429. break
  430. }
  431. offset += totalLen
  432. }
  433. if bf36Start < 0 {
  434. return nil, errors.New("esim: BoundProfilePackage (BF36) not found")
  435. }
  436. valueStart := bf36Start + bf36Header
  437. valueEnd := bf36Start + bf36Total
  438. var segments [][]byte
  439. cursor := valueStart
  440. // First slice: BF36 header through the end of the first child (BF23,
  441. // initialiseSecureChannelRequest) so the secure channel is set up first.
  442. _, _, firstTotal, err := derElementAt(bpp, cursor)
  443. if err != nil {
  444. return nil, err
  445. }
  446. segments = append(segments, bpp[bf36Start:cursor+firstTotal])
  447. cursor += firstTotal
  448. for cursor < valueEnd {
  449. tag, headerLen, totalLen, err := derElementAt(bpp, cursor)
  450. if err != nil {
  451. return nil, err
  452. }
  453. switch tag {
  454. case 0xA1, 0xA3: // sequenceOf88 / sequenceOf86: header, then each child
  455. segments = append(segments, bpp[cursor:cursor+headerLen])
  456. child := cursor + headerLen
  457. childEnd := cursor + totalLen
  458. for child < childEnd {
  459. _, _, childTotal, err := derElementAt(bpp, child)
  460. if err != nil {
  461. return nil, err
  462. }
  463. segments = append(segments, bpp[child:child+childTotal])
  464. child += childTotal
  465. }
  466. default: // A0 / A2 (and anything unexpected): send whole
  467. segments = append(segments, bpp[cursor:cursor+totalLen])
  468. }
  469. cursor += totalLen
  470. }
  471. return segments, nil
  472. }
  473. // esimInstallError is a card-side ProfileInstallationResult ErrorResult: the
  474. // package was received but the eUICC refused to install it.
  475. type esimInstallError struct {
  476. CommandID int
  477. ErrorReason int
  478. }
  479. func (e *esimInstallError) Error() string {
  480. if reason, ok := es10bErrorReasons[e.ErrorReason]; ok {
  481. return "esim: eUICC 拒绝安装 Profile:" + reason
  482. }
  483. return fmt.Sprintf("esim: eUICC 拒绝安装 Profile (reason %d, command %d)", e.ErrorReason, e.CommandID)
  484. }
  485. // es10bErrorReasons maps the ProfileInstallationResult errorReason to text.
  486. // Values mirror lpac's enum es10b_error_reason.
  487. var es10bErrorReasons = map[int]string{
  488. 1: "输入值不正确",
  489. 2: "签名无效",
  490. 3: "transactionId 无效",
  491. 4: "不支持的 CRT 值",
  492. 5: "不支持的远程操作类型",
  493. 6: "不支持的 Profile 类别",
  494. 7: "SCP03t 结构错误",
  495. 8: "SCP03t 安全错误",
  496. 9: "该 Profile (ICCID) 已存在于 eUICC",
  497. 10: "eUICC 剩余空间不足",
  498. 11: "安装被中断",
  499. 12: "Profile 元素处理错误",
  500. 13: "数据不匹配",
  501. 14: "测试 Profile 的 NAA 密钥无效",
  502. 15: "Profile 策略规则 (PPR) 不允许",
  503. 127: "未知错误",
  504. }
  505. // installationResult decodes the ProfileInstallationResult (BF37 → BF27 →
  506. // BF2F NotificationMetadata + A2 finalResult[A0 success | A1 error]). It returns
  507. // the new profile's ICCID on success, or a typed *esimInstallError on ErrorResult.
  508. func installationResult(payload []byte) (string, error) {
  509. roots := derParse(payload)
  510. result := derFindAll(roots, 0xBF37)
  511. if len(result) == 0 {
  512. return "", fmt.Errorf("esim: no ProfileInstallationResult (BF37) in %s", strings.ToUpper(hex.EncodeToString(payload)))
  513. }
  514. data := derFindAll(result[0].children, 0xBF27)
  515. if len(data) == 0 {
  516. return "", errors.New("esim: no ProfileInstallationResultData (BF27)")
  517. }
  518. iccid := ""
  519. for _, node := range derFindAll(data[0].children, 0x5A) {
  520. iccid = decodeICCID(node.value)
  521. break
  522. }
  523. finalResult := firstChild(data[0].children, 0xA2)
  524. if finalResult == nil {
  525. return "", errors.New("esim: ProfileInstallationResultData missing finalResult (A2)")
  526. }
  527. if errNode := firstChild(finalResult.children, 0xA1); errNode != nil {
  528. installErr := &esimInstallError{CommandID: -1, ErrorReason: -1}
  529. if v := derValue(errNode.children, 0x80); len(v) > 0 {
  530. installErr.CommandID = int(v[0])
  531. }
  532. if v := derValue(errNode.children, 0x81); len(v) > 0 {
  533. installErr.ErrorReason = int(v[0])
  534. }
  535. return "", installErr
  536. }
  537. if firstChild(finalResult.children, 0xA0) == nil {
  538. return "", errors.New("esim: unexpected ProfileInstallationResult finalResult")
  539. }
  540. return iccid, nil
  541. }
  542. // firstChild returns the first direct child with the given tag, or nil.
  543. func firstChild(nodes []*derNode, tag int) *derNode {
  544. for _, node := range nodes {
  545. if node.tag == tag {
  546. return node
  547. }
  548. }
  549. return nil
  550. }
  551. func onlyDigits(value string) string {
  552. var builder strings.Builder
  553. for _, r := range value {
  554. if r >= '0' && r <= '9' {
  555. builder.WriteRune(r)
  556. }
  557. }
  558. return builder.String()
  559. }