esim_download.go 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. package device
  2. import (
  3. "context"
  4. "errors"
  5. "strings"
  6. )
  7. // EsimDownloadParams are the SPA download form fields, mapped from the
  8. // snake_case query params by the HTTP layer.
  9. type EsimDownloadParams struct {
  10. SMDP string
  11. MatchingID string
  12. ConfirmationCode string
  13. AIDHex string
  14. IMEI string
  15. }
  16. // EsimProgress is one download step emitted to the SSE stream.
  17. type EsimProgress struct {
  18. Step string
  19. Msg string
  20. Pct int
  21. }
  22. // EsimDownloadResult reports a completed install.
  23. type EsimDownloadResult struct {
  24. ICCID string
  25. SpaceDelta int64 // bytes consumed (positive)
  26. Warning string
  27. }
  28. // ESIMDownloadProfile downloads and installs one eSIM profile (SGP.22 §3):
  29. // challenge/info → ES9+ InitiateAuthentication → ES10b AuthenticateServer →
  30. // ES9+ AuthenticateClient → ES10b PrepareDownload → ES9+ GetBoundProfilePackage
  31. // → ES10b LoadBoundProfilePackage → ES9+ HandleNotification. progress is invoked
  32. // with the SPA's expected step/pct sequence. The whole run holds the device's
  33. // eSIM lock so a concurrent list/switch cannot disturb the card mid-install.
  34. func (manager *Manager) ESIMDownloadProfile(ctx context.Context, id string, params EsimDownloadParams, progress func(EsimProgress)) (*EsimDownloadResult, error) {
  35. smdp := strings.TrimSpace(params.SMDP)
  36. if smdp == "" {
  37. return nil, errors.New("esim: SM-DP+ 地址不能为空")
  38. }
  39. report := func(step, msg string, pct int) {
  40. if progress != nil {
  41. progress(EsimProgress{Step: step, Msg: msg, Pct: pct})
  42. }
  43. }
  44. manager.esimMu.Lock()
  45. defer manager.esimMu.Unlock()
  46. report("preflight", "正在检查 eUICC 剩余空间...", 10)
  47. channel, err := manager.openEuiccAID(ctx, id, targetEuiccAID(params.AIDHex))
  48. if err != nil {
  49. return nil, err
  50. }
  51. defer channel.close(context.Background())
  52. // Free NVRAM before/after drives both the preflight check and space_delta.
  53. freeBefore := 0
  54. if info2, err := channel.getEUICCInfo2(ctx); err == nil {
  55. if n, ok := euiccFreeNVRAM(info2); ok {
  56. freeBefore = n
  57. }
  58. }
  59. challenge, err := channel.getEUICCChallenge(ctx)
  60. if err != nil {
  61. return nil, err
  62. }
  63. info1, err := channel.getEUICCInfo1(ctx)
  64. if err != nil {
  65. return nil, err
  66. }
  67. client := newES9PClient(smdp)
  68. report("auth_client", "正在向 SM-DP+ 进行客户端身份认证...", 30)
  69. init, err := client.initiateAuthentication(ctx, challenge, info1)
  70. if err != nil {
  71. return nil, err
  72. }
  73. transactionID := init.TransactionID
  74. transactionIDBytes := derFindValue(init.ServerSigned1, 0x80)
  75. // Best-effort session cleanup if anything fails after the transaction opens
  76. // (card-side CancelSession BF41, then server-side ES9+ cancelSession).
  77. finished := false
  78. defer func() {
  79. if !finished && len(transactionIDBytes) > 0 {
  80. if cancelResp, cerr := channel.cancelSession(context.Background(), transactionIDBytes, 0x00); cerr == nil {
  81. _ = client.cancelSession(context.Background(), transactionID, cancelResp)
  82. }
  83. }
  84. }()
  85. authResponse, err := channel.authenticateServer(ctx, init, params.MatchingID, params.IMEI)
  86. if err != nil {
  87. return nil, err
  88. }
  89. // A "cert not trusted"/"matchingID refused"/"EID mismatch" failure surfaces
  90. // here, from the SM-DP+'s functionExecutionStatus.
  91. auth, err := client.authenticateClient(ctx, transactionID, authResponse)
  92. if err != nil {
  93. return nil, err
  94. }
  95. report("download", "正在获取 Profile 数据包...", 55)
  96. prepareResponse, err := channel.prepareDownload(ctx, auth, params.ConfirmationCode)
  97. if err != nil {
  98. return nil, err
  99. }
  100. bpp, err := client.getBoundProfilePackage(ctx, transactionID, prepareResponse)
  101. if err != nil {
  102. return nil, err
  103. }
  104. report("install", "正在将 Profile 写入 eUICC...", 80)
  105. installResponse, err := channel.loadBoundProfilePackage(ctx, bpp, func(done, total int) {
  106. if total > 0 {
  107. report("install", "正在将 Profile 写入 eUICC...", 80+done*8/total)
  108. }
  109. })
  110. if err != nil {
  111. return nil, err
  112. }
  113. iccid, err := installationResult(installResponse)
  114. if err != nil {
  115. return nil, err
  116. }
  117. report("notify", "正在向运营商发送下载通知...", 90)
  118. warning := ""
  119. if err := client.handleNotification(ctx, installResponse); err != nil {
  120. warning = "Profile 已安装,但下载通知发送失败"
  121. }
  122. freeAfter := freeBefore
  123. if info2, err := channel.getEUICCInfo2(ctx); err == nil {
  124. if n, ok := euiccFreeNVRAM(info2); ok {
  125. freeAfter = n
  126. }
  127. }
  128. spaceDelta := freeBefore - freeAfter
  129. if spaceDelta <= 0 {
  130. spaceDelta = len(bpp) // fall back to the package size when NVRAM unreadable
  131. }
  132. // The HTTP layer owns the final "done" event (it attaches space_delta/warning).
  133. finished = true
  134. return &EsimDownloadResult{ICCID: iccid, SpaceDelta: int64(spaceDelta), Warning: warning}, nil
  135. }
  136. // ESIMDownloadErrorCode maps a download failure to a stable SPA error code.
  137. // Keep the matching deliberately tolerant because some SM-DP+ implementations
  138. // return only a free-form statusCodeData.message.
  139. func ESIMDownloadErrorCode(err error) string {
  140. var authenticateErr *esimAuthenticateError
  141. if errors.As(err, &authenticateErr) {
  142. return "euicc_authentication_failed"
  143. }
  144. var es9pErr *es9pError
  145. if errors.As(err, &es9pErr) {
  146. switch {
  147. case es9pErr.SubjectCode == "8.1" && es9pErr.ReasonCode == "4.8":
  148. return "euicc_insufficient_memory"
  149. case es9pErr.SubjectCode == "8.8.4" && es9pErr.ReasonCode == "3.7":
  150. return "euicc_ci_incompatible"
  151. case es9pErr.SubjectCode == "8.2.6" && es9pErr.ReasonCode == "3.8":
  152. return "activation_code_refused"
  153. case es9pErr.SubjectCode == "8.2.5" && es9pErr.ReasonCode == "3.7":
  154. return "profile_pool_empty"
  155. }
  156. }
  157. var installErr *esimInstallError
  158. if errors.As(err, &installErr) && installErr.ErrorReason == 10 {
  159. return "euicc_insufficient_memory"
  160. }
  161. lower := strings.ToLower(err.Error())
  162. if strings.Contains(lower, "insufficient") || strings.Contains(lower, "空间不足") {
  163. return "euicc_insufficient_memory"
  164. }
  165. if strings.Contains(lower, "cert.dpauth") &&
  166. (strings.Contains(lower, "root ca") || strings.Contains(lower, "public key supported by the euicc")) {
  167. return "euicc_ci_incompatible"
  168. }
  169. if strings.Contains(lower, "campaign resource pool is empty") ||
  170. strings.Contains(lower, "no more profile available") {
  171. return "profile_pool_empty"
  172. }
  173. if strings.Contains(lower, "matchingid") && strings.Contains(lower, "refused") || lower == "refused" {
  174. return "activation_code_refused"
  175. }
  176. return "download_failed"
  177. }
  178. // EsimChipInfo describes the eUICC for the SPA's eSIM chip header.
  179. type EsimChipInfo struct {
  180. EID string
  181. AID string
  182. FreeNvramBytes int
  183. HasFreeNvram bool
  184. TrustedCIs []string // raw hex SubjectKeyIdentifiers
  185. Certificates []string // friendly CI names (证书)
  186. FirmwareVer string // euiccFirmwareVer (固件)
  187. Manufacturer string // EUM issuer → 生产商
  188. DefaultSmdpAddress string // ES10a default SM-DP+
  189. RootDsAddress string // ES10a Root SM-DS
  190. SAS string // sasAccreditationNumber
  191. }
  192. // ESIMChipInfo reads the eUICC's EID, EUICCInfo2, and configured addresses for
  193. // the chip header. It takes the eSIM lock like the other card ops.
  194. func (manager *Manager) ESIMChipInfo(ctx context.Context, id string) (*EsimChipInfo, error) {
  195. manager.esimMu.Lock()
  196. defer manager.esimMu.Unlock()
  197. channel, err := manager.openEuicc(ctx, id)
  198. if err != nil {
  199. return nil, err
  200. }
  201. defer channel.close(context.Background())
  202. info, err := readEsimChipInfo(ctx, channel, isdRAID)
  203. if err != nil {
  204. return nil, err
  205. }
  206. return &info, nil
  207. }
  208. func readEsimChipInfo(ctx context.Context, channel *euiccChannel, aidHex string) (EsimChipInfo, error) {
  209. info := EsimChipInfo{AID: aidHex}
  210. if eid, err := channel.getEID(ctx); err == nil {
  211. info.EID = eid
  212. info.Manufacturer = eumManufacturerForEID(eid)
  213. }
  214. if info2, err := channel.getEUICCInfo2(ctx); err == nil {
  215. if n, ok := euiccFreeNVRAM(info2); ok {
  216. info.FreeNvramBytes = n
  217. info.HasFreeNvram = true
  218. }
  219. info.TrustedCIs = euiccTrustedCIs(info2)
  220. info.FirmwareVer = euiccFirmwareVersion(info2)
  221. info.SAS = euiccSAS(info2)
  222. for _, hexID := range info.TrustedCIs {
  223. info.Certificates = append(info.Certificates, ciKeyFriendlyName(hexID))
  224. }
  225. }
  226. if def, root := channel.getEuiccConfiguredAddresses(ctx); def != "" || root != "" {
  227. info.DefaultSmdpAddress = def
  228. info.RootDsAddress = root
  229. }
  230. // Report whatever we read (even partial); only a channel-open failure above
  231. // is fatal. A wholly-empty result means the eUICC exposed nothing usable.
  232. if info.EID == "" && !info.HasFreeNvram && len(info.TrustedCIs) == 0 {
  233. return EsimChipInfo{}, errors.New("esim: eUICC did not report chip info")
  234. }
  235. return info, nil
  236. }
  237. // ESIMInventory reads every independently addressable eUICC storage exposed by
  238. // the inserted card. It is entirely read-only: only SELECT, GetProfilesInfo,
  239. // GetEuiccData, GetEuiccInfo2 and GetEuiccConfiguredAddresses are issued.
  240. func (manager *Manager) ESIMInventory(ctx context.Context, id string) ([]EsimInventoryEntry, error) {
  241. manager.esimMu.Lock()
  242. defer manager.esimMu.Unlock()
  243. if manager.esimRecoveryActive(id) {
  244. return nil, errESIMRecovering
  245. }
  246. aids := manager.discoverEuiccAIDs(ctx, id)
  247. entries := make([]EsimInventoryEntry, 0, len(aids))
  248. var lastErr error
  249. for _, aid := range aids {
  250. channel, err := manager.openEuiccAID(ctx, id, aid)
  251. if err != nil {
  252. lastErr = err
  253. continue
  254. }
  255. profilePayload, profileErr := channel.es10(ctx, []byte{0xBF, 0x2D, 0x00})
  256. chip, chipErr := readEsimChipInfo(ctx, channel, aid)
  257. channel.close(context.Background())
  258. if profileErr != nil {
  259. lastErr = profileErr
  260. continue
  261. }
  262. if chipErr != nil {
  263. lastErr = chipErr
  264. continue
  265. }
  266. info := EsimInfo{EID: chip.EID, AID: aid, Profiles: parseProfilesInfo(profilePayload)}
  267. entries = append(entries, EsimInventoryEntry{Info: info, Chip: chip})
  268. }
  269. if len(entries) == 0 {
  270. if lastErr != nil {
  271. return nil, lastErr
  272. }
  273. return nil, ErrNoEUICC
  274. }
  275. return entries, nil
  276. }