| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- package device
- import (
- "context"
- "errors"
- "strings"
- )
- // EsimDownloadParams are the SPA download form fields, mapped from the
- // snake_case query params by the HTTP layer.
- type EsimDownloadParams struct {
- SMDP string
- MatchingID string
- ConfirmationCode string
- AIDHex string
- IMEI string
- }
- // EsimProgress is one download step emitted to the SSE stream.
- type EsimProgress struct {
- Step string
- Msg string
- Pct int
- }
- // EsimDownloadResult reports a completed install.
- type EsimDownloadResult struct {
- ICCID string
- SpaceDelta int64 // bytes consumed (positive)
- Warning string
- }
- // ESIMDownloadProfile downloads and installs one eSIM profile (SGP.22 §3):
- // challenge/info → ES9+ InitiateAuthentication → ES10b AuthenticateServer →
- // ES9+ AuthenticateClient → ES10b PrepareDownload → ES9+ GetBoundProfilePackage
- // → ES10b LoadBoundProfilePackage → ES9+ HandleNotification. progress is invoked
- // with the SPA's expected step/pct sequence. The whole run holds the device's
- // eSIM lock so a concurrent list/switch cannot disturb the card mid-install.
- func (manager *Manager) ESIMDownloadProfile(ctx context.Context, id string, params EsimDownloadParams, progress func(EsimProgress)) (*EsimDownloadResult, error) {
- smdp := strings.TrimSpace(params.SMDP)
- if smdp == "" {
- return nil, errors.New("esim: SM-DP+ 地址不能为空")
- }
- report := func(step, msg string, pct int) {
- if progress != nil {
- progress(EsimProgress{Step: step, Msg: msg, Pct: pct})
- }
- }
- manager.esimMu.Lock()
- defer manager.esimMu.Unlock()
- report("preflight", "正在检查 eUICC 剩余空间...", 10)
- channel, err := manager.openEuiccAID(ctx, id, targetEuiccAID(params.AIDHex))
- if err != nil {
- return nil, err
- }
- defer channel.close(context.Background())
- // Free NVRAM before/after drives both the preflight check and space_delta.
- freeBefore := 0
- if info2, err := channel.getEUICCInfo2(ctx); err == nil {
- if n, ok := euiccFreeNVRAM(info2); ok {
- freeBefore = n
- }
- }
- challenge, err := channel.getEUICCChallenge(ctx)
- if err != nil {
- return nil, err
- }
- info1, err := channel.getEUICCInfo1(ctx)
- if err != nil {
- return nil, err
- }
- client := newES9PClient(smdp)
- report("auth_client", "正在向 SM-DP+ 进行客户端身份认证...", 30)
- init, err := client.initiateAuthentication(ctx, challenge, info1)
- if err != nil {
- return nil, err
- }
- transactionID := init.TransactionID
- transactionIDBytes := derFindValue(init.ServerSigned1, 0x80)
- // Best-effort session cleanup if anything fails after the transaction opens
- // (card-side CancelSession BF41, then server-side ES9+ cancelSession).
- finished := false
- defer func() {
- if !finished && len(transactionIDBytes) > 0 {
- if cancelResp, cerr := channel.cancelSession(context.Background(), transactionIDBytes, 0x00); cerr == nil {
- _ = client.cancelSession(context.Background(), transactionID, cancelResp)
- }
- }
- }()
- authResponse, err := channel.authenticateServer(ctx, init, params.MatchingID, params.IMEI)
- if err != nil {
- return nil, err
- }
- // A "cert not trusted"/"matchingID refused"/"EID mismatch" failure surfaces
- // here, from the SM-DP+'s functionExecutionStatus.
- auth, err := client.authenticateClient(ctx, transactionID, authResponse)
- if err != nil {
- return nil, err
- }
- report("download", "正在获取 Profile 数据包...", 55)
- prepareResponse, err := channel.prepareDownload(ctx, auth, params.ConfirmationCode)
- if err != nil {
- return nil, err
- }
- bpp, err := client.getBoundProfilePackage(ctx, transactionID, prepareResponse)
- if err != nil {
- return nil, err
- }
- report("install", "正在将 Profile 写入 eUICC...", 80)
- installResponse, err := channel.loadBoundProfilePackage(ctx, bpp, func(done, total int) {
- if total > 0 {
- report("install", "正在将 Profile 写入 eUICC...", 80+done*8/total)
- }
- })
- if err != nil {
- return nil, err
- }
- iccid, err := installationResult(installResponse)
- if err != nil {
- return nil, err
- }
- report("notify", "正在向运营商发送下载通知...", 90)
- warning := ""
- if err := client.handleNotification(ctx, installResponse); err != nil {
- warning = "Profile 已安装,但下载通知发送失败"
- }
- freeAfter := freeBefore
- if info2, err := channel.getEUICCInfo2(ctx); err == nil {
- if n, ok := euiccFreeNVRAM(info2); ok {
- freeAfter = n
- }
- }
- spaceDelta := freeBefore - freeAfter
- if spaceDelta <= 0 {
- spaceDelta = len(bpp) // fall back to the package size when NVRAM unreadable
- }
- // The HTTP layer owns the final "done" event (it attaches space_delta/warning).
- finished = true
- return &EsimDownloadResult{ICCID: iccid, SpaceDelta: int64(spaceDelta), Warning: warning}, nil
- }
- // ESIMDownloadErrorCode maps a download failure to a stable SPA error code.
- // Keep the matching deliberately tolerant because some SM-DP+ implementations
- // return only a free-form statusCodeData.message.
- func ESIMDownloadErrorCode(err error) string {
- var authenticateErr *esimAuthenticateError
- if errors.As(err, &authenticateErr) {
- return "euicc_authentication_failed"
- }
- var es9pErr *es9pError
- if errors.As(err, &es9pErr) {
- switch {
- case es9pErr.SubjectCode == "8.1" && es9pErr.ReasonCode == "4.8":
- return "euicc_insufficient_memory"
- case es9pErr.SubjectCode == "8.8.4" && es9pErr.ReasonCode == "3.7":
- return "euicc_ci_incompatible"
- case es9pErr.SubjectCode == "8.2.6" && es9pErr.ReasonCode == "3.8":
- return "activation_code_refused"
- case es9pErr.SubjectCode == "8.2.5" && es9pErr.ReasonCode == "3.7":
- return "profile_pool_empty"
- }
- }
- var installErr *esimInstallError
- if errors.As(err, &installErr) && installErr.ErrorReason == 10 {
- return "euicc_insufficient_memory"
- }
- lower := strings.ToLower(err.Error())
- if strings.Contains(lower, "insufficient") || strings.Contains(lower, "空间不足") {
- return "euicc_insufficient_memory"
- }
- if strings.Contains(lower, "cert.dpauth") &&
- (strings.Contains(lower, "root ca") || strings.Contains(lower, "public key supported by the euicc")) {
- return "euicc_ci_incompatible"
- }
- if strings.Contains(lower, "campaign resource pool is empty") ||
- strings.Contains(lower, "no more profile available") {
- return "profile_pool_empty"
- }
- if strings.Contains(lower, "matchingid") && strings.Contains(lower, "refused") || lower == "refused" {
- return "activation_code_refused"
- }
- return "download_failed"
- }
- // EsimChipInfo describes the eUICC for the SPA's eSIM chip header.
- type EsimChipInfo struct {
- EID string
- AID string
- FreeNvramBytes int
- HasFreeNvram bool
- TrustedCIs []string // raw hex SubjectKeyIdentifiers
- Certificates []string // friendly CI names (证书)
- FirmwareVer string // euiccFirmwareVer (固件)
- Manufacturer string // EUM issuer → 生产商
- DefaultSmdpAddress string // ES10a default SM-DP+
- RootDsAddress string // ES10a Root SM-DS
- SAS string // sasAccreditationNumber
- }
- // ESIMChipInfo reads the eUICC's EID, EUICCInfo2, and configured addresses for
- // the chip header. It takes the eSIM lock like the other card ops.
- func (manager *Manager) ESIMChipInfo(ctx context.Context, id string) (*EsimChipInfo, error) {
- manager.esimMu.Lock()
- defer manager.esimMu.Unlock()
- channel, err := manager.openEuicc(ctx, id)
- if err != nil {
- return nil, err
- }
- defer channel.close(context.Background())
- info, err := readEsimChipInfo(ctx, channel, isdRAID)
- if err != nil {
- return nil, err
- }
- return &info, nil
- }
- func readEsimChipInfo(ctx context.Context, channel *euiccChannel, aidHex string) (EsimChipInfo, error) {
- info := EsimChipInfo{AID: aidHex}
- if eid, err := channel.getEID(ctx); err == nil {
- info.EID = eid
- info.Manufacturer = eumManufacturerForEID(eid)
- }
- if info2, err := channel.getEUICCInfo2(ctx); err == nil {
- if n, ok := euiccFreeNVRAM(info2); ok {
- info.FreeNvramBytes = n
- info.HasFreeNvram = true
- }
- info.TrustedCIs = euiccTrustedCIs(info2)
- info.FirmwareVer = euiccFirmwareVersion(info2)
- info.SAS = euiccSAS(info2)
- for _, hexID := range info.TrustedCIs {
- info.Certificates = append(info.Certificates, ciKeyFriendlyName(hexID))
- }
- }
- if def, root := channel.getEuiccConfiguredAddresses(ctx); def != "" || root != "" {
- info.DefaultSmdpAddress = def
- info.RootDsAddress = root
- }
- // Report whatever we read (even partial); only a channel-open failure above
- // is fatal. A wholly-empty result means the eUICC exposed nothing usable.
- if info.EID == "" && !info.HasFreeNvram && len(info.TrustedCIs) == 0 {
- return EsimChipInfo{}, errors.New("esim: eUICC did not report chip info")
- }
- return info, nil
- }
- // ESIMInventory reads every independently addressable eUICC storage exposed by
- // the inserted card. It is entirely read-only: only SELECT, GetProfilesInfo,
- // GetEuiccData, GetEuiccInfo2 and GetEuiccConfiguredAddresses are issued.
- func (manager *Manager) ESIMInventory(ctx context.Context, id string) ([]EsimInventoryEntry, error) {
- manager.esimMu.Lock()
- defer manager.esimMu.Unlock()
- if manager.esimRecoveryActive(id) {
- return nil, errESIMRecovering
- }
- aids := manager.discoverEuiccAIDs(ctx, id)
- entries := make([]EsimInventoryEntry, 0, len(aids))
- var lastErr error
- for _, aid := range aids {
- channel, err := manager.openEuiccAID(ctx, id, aid)
- if err != nil {
- lastErr = err
- continue
- }
- profilePayload, profileErr := channel.es10(ctx, []byte{0xBF, 0x2D, 0x00})
- chip, chipErr := readEsimChipInfo(ctx, channel, aid)
- channel.close(context.Background())
- if profileErr != nil {
- lastErr = profileErr
- continue
- }
- if chipErr != nil {
- lastErr = chipErr
- continue
- }
- info := EsimInfo{EID: chip.EID, AID: aid, Profiles: parseProfilesInfo(profilePayload)}
- entries = append(entries, EsimInventoryEntry{Info: info, Chip: chip})
- }
- if len(entries) == 0 {
- if lastErr != nil {
- return nil, lastErr
- }
- return nil, ErrNoEUICC
- }
- return entries, nil
- }
|